attributeFilters = new AttributeFilterCollection; $this->bundleGenerator = new BundleGenerator($this); $this->plugins = new PluginCollection($this); $this->registeredVars = ['urlConfig' => new UrlConfig]; $this->rendering = new Rendering($this); $this->rootRules = new Ruleset; $this->rulesGenerator = new RulesGenerator; $this->tags = new TagCollection; $this->templateChecker = new TemplateChecker; $this->templateNormalizer = new TemplateNormalizer; } public function __get($k) { if (\preg_match('#^[A-Z][A-Za-z_0-9]+$#D', $k)) return (isset($this->plugins[$k])) ? $this->plugins[$k] : $this->plugins->load($k); if (isset($this->registeredVars[$k])) return $this->registeredVars[$k]; throw new RuntimeException("Undefined property '" . __CLASS__ . '::$' . $k . "'"); } public function __isset($k) { if (\preg_match('#^[A-Z][A-Za-z_0-9]+$#D', $k)) return isset($this->plugins[$k]); return isset($this->registeredVars[$k]); } public function __set($k, $v) { if (\preg_match('#^[A-Z][A-Za-z_0-9]+$#D', $k)) $this->plugins[$k] = $v; else $this->registeredVars[$k] = $v; } public function __unset($k) { if (\preg_match('#^[A-Z][A-Za-z_0-9]+$#D', $k)) unset($this->plugins[$k]); else unset($this->registeredVars[$k]); } public function enableJavaScript() { if (!isset($this->javascript)) $this->javascript = new JavaScript($this); } public function finalize() { $return = []; $this->plugins->finalize(); foreach ($this->tags as $tag) $this->templateNormalizer->normalizeTag($tag); $return['renderer'] = $this->rendering->getRenderer(); $this->addTagRules(); $config = $this->asConfig(); if (isset($this->javascript)) $return['js'] = $this->javascript->getParser(ConfigHelper::filterConfig($config, 'JS')); $config = ConfigHelper::filterConfig($config, 'PHP'); ConfigHelper::optimizeArray($config); $return['parser'] = new Parser($config); return $return; } public function loadBundle($bundleName) { if (!\preg_match('#^[A-Z][A-Za-z0-9]+$#D', $bundleName)) throw new InvalidArgumentException("Invalid bundle name '" . $bundleName . "'"); $className = __CLASS__ . '\\Bundles\\' . $bundleName; $bundle = new $className; $bundle->configure($this); } public function saveBundle($className, $filepath, array $options = []) { $file = "bundleGenerator->generate($className, $options); return (\file_put_contents($filepath, $file) !== \false); } public function asConfig() { $properties = \get_object_vars($this); unset($properties['attributeFilters']); unset($properties['bundleGenerator']); unset($properties['javascript']); unset($properties['rendering']); unset($properties['rulesGenerator']); unset($properties['registeredVars']); unset($properties['templateChecker']); unset($properties['templateNormalizer']); unset($properties['stylesheet']); $config = ConfigHelper::toArray($properties); $bitfields = RulesHelper::getBitfields($this->tags, $this->rootRules); $config['rootContext'] = $bitfields['root']; $config['rootContext']['flags'] = $config['rootRules']['flags']; $config['registeredVars'] = ConfigHelper::toArray($this->registeredVars, \true); $config += [ 'plugins' => [], 'tags' => [] ]; $config['tags'] = \array_intersect_key($config['tags'], $bitfields['tags']); foreach ($bitfields['tags'] as $tagName => $tagBitfields) $config['tags'][$tagName] += $tagBitfields; unset($config['rootRules']); return $config; } protected function addTagRules() { $rules = $this->rulesGenerator->getRules($this->tags); $this->rootRules->merge($rules['root'], \false); foreach ($rules['tags'] as $tagName => $tagRules) $this->tags[$tagName]->rules->merge($tagRules, \false); } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator; use s9e\TextFormatter\Configurator; use s9e\TextFormatter\Configurator\RendererGenerators\PHP; class BundleGenerator { protected $configurator; public $serializer = 'serialize'; public $unserializer = 'unserialize'; public function __construct(Configurator $configurator) { $this->configurator = $configurator; } public function generate($className, array $options = []) { $options += ['autoInclude' => \true]; $objects = $this->configurator->finalize(); $parser = $objects['parser']; $renderer = $objects['renderer']; $namespace = ''; if (\preg_match('#(.*)\\\\([^\\\\]+)$#', $className, $m)) { $namespace = $m[1]; $className = $m[2]; } $php = []; $php[] = '/**'; $php[] = '* @package s9e\TextFormatter'; $php[] = '* @copyright Copyright (c) 2010-2019 The s9e Authors'; $php[] = '* @license http://www.opensource.org/licenses/mit-license.php The MIT License'; $php[] = '*/'; if ($namespace) { $php[] = 'namespace ' . $namespace . ';'; $php[] = ''; } $php[] = 'abstract class ' . $className . ' extends \\s9e\\TextFormatter\\Bundle'; $php[] = '{'; $php[] = ' /**'; $php[] = ' * @var s9e\\TextFormatter\\Parser Singleton instance used by parse()'; $php[] = ' */'; $php[] = ' protected static $parser;'; $php[] = ''; $php[] = ' /**'; $php[] = ' * @var s9e\\TextFormatter\\Renderer Singleton instance used by render()'; $php[] = ' */'; $php[] = ' protected static $renderer;'; $php[] = ''; $events = [ 'beforeParse' => 'Callback executed before parse(), receives the original text as argument', 'afterParse' => 'Callback executed after parse(), receives the parsed text as argument', 'beforeRender' => 'Callback executed before render(), receives the parsed text as argument', 'afterRender' => 'Callback executed after render(), receives the output as argument', 'beforeUnparse' => 'Callback executed before unparse(), receives the parsed text as argument', 'afterUnparse' => 'Callback executed after unparse(), receives the original text as argument' ]; foreach ($events as $eventName => $eventDesc) if (isset($options[$eventName])) { $php[] = ' /**'; $php[] = ' * @var ' . $eventDesc; $php[] = ' */'; $php[] = ' public static $' . $eventName . ' = ' . \var_export($options[$eventName], \true) . ';'; $php[] = ''; } $php[] = ' /**'; $php[] = ' * Return a new instance of s9e\\TextFormatter\\Parser'; $php[] = ' *'; $php[] = ' * @return s9e\\TextFormatter\\Parser'; $php[] = ' */'; $php[] = ' public static function getParser()'; $php[] = ' {'; if (isset($options['parserSetup'])) { $php[] = ' $parser = ' . $this->exportObject($parser) . ';'; $php[] = ' ' . $this->exportCallback($namespace, $options['parserSetup'], '$parser') . ';'; $php[] = ''; $php[] = ' return $parser;'; } else $php[] = ' return ' . $this->exportObject($parser) . ';'; $php[] = ' }'; $php[] = ''; $php[] = ' /**'; $php[] = ' * Return a new instance of s9e\\TextFormatter\\Renderer'; $php[] = ' *'; $php[] = ' * @return s9e\\TextFormatter\\Renderer'; $php[] = ' */'; $php[] = ' public static function getRenderer()'; $php[] = ' {'; if (!empty($options['autoInclude']) && $this->configurator->rendering->engine instanceof PHP && isset($this->configurator->rendering->engine->lastFilepath)) { $className = \get_class($renderer); $filepath = \realpath($this->configurator->rendering->engine->lastFilepath); $php[] = ' if (!class_exists(' . \var_export($className, \true) . ', false)'; $php[] = ' && file_exists(' . \var_export($filepath, \true) . '))'; $php[] = ' {'; $php[] = ' include ' . \var_export($filepath, \true) . ';'; $php[] = ' }'; $php[] = ''; } if (isset($options['rendererSetup'])) { $php[] = ' $renderer = ' . $this->exportObject($renderer) . ';'; $php[] = ' ' . $this->exportCallback($namespace, $options['rendererSetup'], '$renderer') . ';'; $php[] = ''; $php[] = ' return $renderer;'; } else $php[] = ' return ' . $this->exportObject($renderer) . ';'; $php[] = ' }'; $php[] = '}'; return \implode("\n", $php); } protected function exportCallback($namespace, callable $callback, $argument) { if (\is_array($callback) && \is_string($callback[0])) $callback = $callback[0] . '::' . $callback[1]; if (!\is_string($callback)) return 'call_user_func(' . \var_export($callback, \true) . ', ' . $argument . ')'; if ($callback[0] !== '\\') $callback = '\\' . $callback; if (\substr($callback, 0, 2 + \strlen($namespace)) === '\\' . $namespace . '\\') $callback = \substr($callback, 2 + \strlen($namespace)); return $callback . '(' . $argument . ')'; } protected function exportObject($obj) { $str = \call_user_func($this->serializer, $obj); $str = \var_export($str, \true); return $this->unserializer . '(' . $str . ')'; } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator; interface ConfigProvider { public function asConfig(); } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator; interface FilterableConfigValue { public function filterConfig($target); } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Helpers; use DOMAttr; use RuntimeException; abstract class AVTHelper { public static function parse($attrValue) { \preg_match_all('(\\{\\{|\\{(?:[^\'"}]|\'[^\']*\'|"[^"]*")+\\}|\\{|[^{]++)', $attrValue, $matches); $tokens = []; foreach ($matches[0] as $str) if ($str === '{{' || $str === '{') $tokens[] = ['literal', '{']; elseif ($str[0] === '{') $tokens[] = ['expression', \substr($str, 1, -1)]; else $tokens[] = ['literal', \str_replace('}}', '}', $str)]; return $tokens; } public static function replace(DOMAttr $attribute, callable $callback) { $tokens = self::parse($attribute->value); foreach ($tokens as $k => $token) $tokens[$k] = $callback($token); $attribute->value = \htmlspecialchars(self::serialize($tokens), \ENT_NOQUOTES, 'UTF-8'); } public static function serialize(array $tokens) { $attrValue = ''; foreach ($tokens as $token) if ($token[0] === 'literal') $attrValue .= \preg_replace('([{}])', '$0$0', $token[1]); elseif ($token[0] === 'expression') $attrValue .= '{' . $token[1] . '}'; else throw new RuntimeException('Unknown token type'); return $attrValue; } public static function toXSL($attrValue) { $xsl = ''; foreach (self::parse($attrValue) as $_f6b3b659) { list($type, $content) = $_f6b3b659; if ($type === 'expression') $xsl .= ''; elseif (\trim($content) !== $content) $xsl .= '' . \htmlspecialchars($content, \ENT_NOQUOTES, 'UTF-8') . ''; else $xsl .= \htmlspecialchars($content, \ENT_NOQUOTES, 'UTF-8'); } return $xsl; } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Helpers; class CharacterClassBuilder { protected $chars; public $delimiter = '/'; protected $ranges; public function fromList(array $chars) { $this->chars = $chars; $this->unescapeLiterals(); \sort($this->chars); $this->storeRanges(); $this->reorderDash(); $this->fixCaret(); $this->escapeSpecialChars(); return $this->buildCharacterClass(); } protected function buildCharacterClass() { $str = '['; foreach ($this->ranges as $_b7914274) { list($start, $end) = $_b7914274; if ($end > $start + 2) $str .= $this->chars[$start] . '-' . $this->chars[$end]; else $str .= \implode('', \array_slice($this->chars, $start, $end + 1 - $start)); } $str .= ']'; return $str; } protected function escapeSpecialChars() { $specialChars = ['\\', ']', $this->delimiter]; foreach (\array_intersect($this->chars, $specialChars) as $k => $v) $this->chars[$k] = '\\' . $v; } protected function fixCaret() { $k = \array_search('^', $this->chars, \true); if ($this->ranges[0][0] !== $k) return; if (isset($this->ranges[1])) { $range = $this->ranges[0]; $this->ranges[0] = $this->ranges[1]; $this->ranges[1] = $range; } else $this->chars[$k] = '\\^'; } protected function reorderDash() { $dashIndex = \array_search('-', $this->chars, \true); if ($dashIndex === \false) return; $k = \array_search([$dashIndex, $dashIndex], $this->ranges, \true); if ($k > 0) { unset($this->ranges[$k]); \array_unshift($this->ranges, [$dashIndex, $dashIndex]); } $commaIndex = \array_search(',', $this->chars); $range = [$commaIndex, $dashIndex]; $k = \array_search($range, $this->ranges, \true); if ($k !== \false) { $this->ranges[$k] = [$commaIndex, $commaIndex]; \array_unshift($this->ranges, [$dashIndex, $dashIndex]); } } protected function storeRanges() { $values = []; foreach ($this->chars as $char) if (\strlen($char) === 1) $values[] = \ord($char); else $values[] = \false; $i = \count($values) - 1; $ranges = []; while ($i >= 0) { $start = $i; $end = $i; while ($start > 0 && $values[$start - 1] === $values[$end] - ($end + 1 - $start)) --$start; $ranges[] = [$start, $end]; $i = $start - 1; } $this->ranges = \array_reverse($ranges); } protected function unescapeLiterals() { foreach ($this->chars as $k => $char) if ($char[0] === '\\' && \preg_match('(^\\\\[^a-z]$)Di', $char)) $this->chars[$k] = \substr($char, 1); } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Helpers; use RuntimeException; use Traversable; use s9e\TextFormatter\Configurator\ConfigProvider; use s9e\TextFormatter\Configurator\FilterableConfigValue; use s9e\TextFormatter\Configurator\JavaScript\Dictionary; abstract class ConfigHelper { public static function filterConfig(array $config, $target = 'PHP') { $filteredConfig = []; foreach ($config as $name => $value) { if ($value instanceof FilterableConfigValue) { $value = $value->filterConfig($target); if (!isset($value)) continue; } if (\is_array($value)) $value = self::filterConfig($value, $target); $filteredConfig[$name] = $value; } return $filteredConfig; } public static function generateQuickMatchFromList(array $strings) { foreach ($strings as $string) { $stringLen = \strlen($string); $substrings = []; for ($len = $stringLen; $len; --$len) { $pos = $stringLen - $len; do { $substrings[\substr($string, $pos, $len)] = 1; } while (--$pos >= 0); } if (isset($goodStrings)) { $goodStrings = \array_intersect_key($goodStrings, $substrings); if (empty($goodStrings)) break; } else $goodStrings = $substrings; } if (empty($goodStrings)) return \false; return \strval(\key($goodStrings)); } public static function optimizeArray(array &$config, array &$cache = []) { foreach ($config as $k => &$v) { if (!\is_array($v)) continue; self::optimizeArray($v, $cache); $cacheKey = \serialize($v); if (!isset($cache[$cacheKey])) $cache[$cacheKey] = $v; $config[$k] =& $cache[$cacheKey]; } unset($v); } public static function toArray($value, $keepEmpty = \false, $keepNull = \false) { $array = []; foreach ($value as $k => $v) { $isDictionary = $v instanceof Dictionary; if ($v instanceof ConfigProvider) $v = $v->asConfig(); elseif ($v instanceof Traversable || \is_array($v)) $v = self::toArray($v, $keepEmpty, $keepNull); elseif (\is_scalar($v) || \is_null($v)) ; else { $type = (\is_object($v)) ? 'an instance of ' . \get_class($v) : 'a ' . \gettype($v); throw new RuntimeException('Cannot convert ' . $type . ' to array'); } if (!isset($v) && !$keepNull) continue; if (!$keepEmpty && $v === []) continue; $array[$k] = ($isDictionary) ? new Dictionary($v) : $v; } return $array; } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Helpers; use DOMElement; use DOMXPath; class ElementInspector { protected static $htmlElements = [ 'a'=>['c'=>"\17\0\0\0\0\1",'c3'=>'@href','ac'=>"\0",'dd'=>"\10\0\0\0\0\1",'t'=>1,'fe'=>1], 'abbr'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 'address'=>['c'=>"\3\40",'ac'=>"\1",'dd'=>"\200\44",'b'=>1,'cp'=>['p']], 'article'=>['c'=>"\3\4",'ac'=>"\1",'dd'=>"\0\0\0\0\10",'b'=>1,'cp'=>['p']], 'aside'=>['c'=>"\3\4",'ac'=>"\1",'dd'=>"\0\0\0\0\10",'b'=>1,'cp'=>['p']], 'audio'=>['c'=>"\57",'c3'=>'@controls','c1'=>'@controls','ac'=>"\0\0\0\104",'ac26'=>'not(@src)','dd'=>"\0\0\0\0\0\2",'dd41'=>'@src','t'=>1], 'b'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0",'fe'=>1], 'base'=>['c'=>"\100",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1,'b'=>1], 'bdi'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 'bdo'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 'blockquote'=>['c'=>"\23",'ac'=>"\1",'dd'=>"\0",'b'=>1,'cp'=>['p']], 'body'=>['c'=>"\20\0\2",'ac'=>"\1",'dd'=>"\0",'b'=>1], 'br'=>['c'=>"\5",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1], 'button'=>['c'=>"\17\1",'ac'=>"\4",'dd'=>"\10"], 'caption'=>['c'=>"\0\2",'ac'=>"\1",'dd'=>"\0\0\0\200",'b'=>1], 'cite'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 'code'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0",'fe'=>1], 'col'=>['c'=>"\0\0\10",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1,'b'=>1], 'colgroup'=>['c'=>"\0\2",'ac'=>"\0\0\10",'ac19'=>'not(@span)','dd'=>"\0",'nt'=>1,'e'=>1,'e?'=>'@span','b'=>1], 'data'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 'datalist'=>['c'=>"\5",'ac'=>"\4\0\200\10",'dd'=>"\0"], 'dd'=>['c'=>"\0\100\100",'ac'=>"\1",'dd'=>"\0",'b'=>1,'cp'=>['dd','dt']], 'del'=>['c'=>"\5",'ac'=>"\0",'dd'=>"\0",'t'=>1], 'details'=>['c'=>"\33",'ac'=>"\1\0\0\2",'dd'=>"\0",'b'=>1,'cp'=>['p']], 'dfn'=>['c'=>"\7\0\0\0\40",'ac'=>"\4",'dd'=>"\0\0\0\0\40"], 'dialog'=>['c'=>"\21",'ac'=>"\1",'dd'=>"\0",'b'=>1], 'div'=>['c'=>"\3\100",'ac'=>"\1\0\300",'ac0'=>'not(ancestor::dl)','dd'=>"\0",'b'=>1,'cp'=>['p']], 'dl'=>['c'=>"\3",'c1'=>'dt and dd','ac'=>"\0\100\200",'dd'=>"\0",'nt'=>1,'b'=>1,'cp'=>['p']], 'dt'=>['c'=>"\0\100\100",'ac'=>"\1",'dd'=>"\200\4\0\40",'b'=>1,'cp'=>['dd','dt']], 'em'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0",'fe'=>1], 'embed'=>['c'=>"\57",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1], 'fieldset'=>['c'=>"\23\1",'ac'=>"\1\0\0\20",'dd'=>"\0",'b'=>1,'cp'=>['p']], 'figcaption'=>['c'=>"\0\0\0\0\0\4",'ac'=>"\1",'dd'=>"\0",'b'=>1,'cp'=>['p']], 'figure'=>['c'=>"\23",'ac'=>"\1\0\0\0\0\4",'dd'=>"\0",'b'=>1,'cp'=>['p']], 'footer'=>['c'=>"\3\40",'ac'=>"\1",'dd'=>"\0\0\0\0\10",'b'=>1,'cp'=>['p']], 'form'=>['c'=>"\3\0\0\0\20",'ac'=>"\1",'dd'=>"\0\0\0\0\20",'b'=>1,'cp'=>['p']], 'h1'=>['c'=>"\203",'ac'=>"\4",'dd'=>"\0",'b'=>1,'cp'=>['p']], 'h2'=>['c'=>"\203",'ac'=>"\4",'dd'=>"\0",'b'=>1,'cp'=>['p']], 'h3'=>['c'=>"\203",'ac'=>"\4",'dd'=>"\0",'b'=>1,'cp'=>['p']], 'h4'=>['c'=>"\203",'ac'=>"\4",'dd'=>"\0",'b'=>1,'cp'=>['p']], 'h5'=>['c'=>"\203",'ac'=>"\4",'dd'=>"\0",'b'=>1,'cp'=>['p']], 'h6'=>['c'=>"\203",'ac'=>"\4",'dd'=>"\0",'b'=>1,'cp'=>['p']], 'head'=>['c'=>"\0\0\2",'ac'=>"\100",'dd'=>"\0",'nt'=>1,'b'=>1], 'header'=>['c'=>"\3\40\0\40",'ac'=>"\1",'dd'=>"\0\0\0\0\10",'b'=>1,'cp'=>['p']], 'hr'=>['c'=>"\1",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1,'b'=>1,'cp'=>['p']], 'html'=>['c'=>"\0",'ac'=>"\0\0\2",'dd'=>"\0",'nt'=>1,'b'=>1], 'i'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0",'fe'=>1], 'iframe'=>['c'=>"\57",'ac'=>"\4",'dd'=>"\0"], 'img'=>['c'=>"\57\20\4",'c3'=>'@usemap','ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1], 'input'=>['c'=>"\17\20",'c3'=>'@type!="hidden"','c12'=>'@type!="hidden" or @type="hidden"','c1'=>'@type!="hidden"','ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1], 'ins'=>['c'=>"\7",'ac'=>"\0",'dd'=>"\0",'t'=>1], 'kbd'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 'label'=>['c'=>"\17\20\0\0\4",'ac'=>"\4",'dd'=>"\0\200\0\0\4"], 'legend'=>['c'=>"\0\0\0\20",'ac'=>"\204",'dd'=>"\0",'b'=>1], 'li'=>['c'=>"\0\0\0\0\200",'ac'=>"\1",'dd'=>"\0",'b'=>1,'cp'=>['li']], 'link'=>['c'=>"\105",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1], 'main'=>['c'=>"\3\0\0\0\10",'ac'=>"\1",'dd'=>"\0",'b'=>1,'cp'=>['p']], 'mark'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 'media element'=>['c'=>"\0\0\0\0\0\2",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'b'=>1], 'meta'=>['c'=>"\100",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1,'b'=>1], 'meter'=>['c'=>"\7\200\0\0\2",'ac'=>"\4",'dd'=>"\0\0\0\0\2"], 'nav'=>['c'=>"\3\4",'ac'=>"\1",'dd'=>"\0\0\0\0\10",'b'=>1,'cp'=>['p']], 'object'=>['c'=>"\47\1",'ac'=>"\0\0\0\0\1",'dd'=>"\0",'t'=>1], 'ol'=>['c'=>"\3",'c1'=>'li','ac'=>"\0\0\200\0\200",'dd'=>"\0",'nt'=>1,'b'=>1,'cp'=>['p']], 'optgroup'=>['c'=>"\0\0\1",'ac'=>"\0\0\200\10",'dd'=>"\0",'nt'=>1,'b'=>1,'cp'=>['optgroup','option']], 'option'=>['c'=>"\0\0\1\10",'ac'=>"\0",'dd'=>"\0",'b'=>1,'cp'=>['option']], 'output'=>['c'=>"\7\1",'ac'=>"\4",'dd'=>"\0"], 'p'=>['c'=>"\3",'ac'=>"\4",'dd'=>"\0",'b'=>1,'cp'=>['p']], 'param'=>['c'=>"\0\0\0\0\1",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1,'b'=>1], 'picture'=>['c'=>"\45",'ac'=>"\0\0\204",'dd'=>"\0",'nt'=>1], 'pre'=>['c'=>"\3",'ac'=>"\4",'dd'=>"\0",'pre'=>1,'b'=>1,'cp'=>['p']], 'progress'=>['c'=>"\7\200\0\1",'ac'=>"\4",'dd'=>"\0\0\0\1"], 'q'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 'rb'=>['c'=>"\0\10",'ac'=>"\4",'dd'=>"\0",'b'=>1], 'rp'=>['c'=>"\0\10\40",'ac'=>"\4",'dd'=>"\0",'b'=>1,'cp'=>['rp','rt']], 'rt'=>['c'=>"\0\10\40",'ac'=>"\4",'dd'=>"\0",'b'=>1,'cp'=>['rp','rt']], 'rtc'=>['c'=>"\0\10",'ac'=>"\4\0\40",'dd'=>"\0",'b'=>1], 'ruby'=>['c'=>"\7",'ac'=>"\4\10",'dd'=>"\0"], 's'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0",'fe'=>1], 'samp'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 'script'=>['c'=>"\105\0\200",'ac'=>"\0",'dd'=>"\0",'to'=>1], 'section'=>['c'=>"\3\4",'ac'=>"\1",'dd'=>"\0",'b'=>1,'cp'=>['p']], 'select'=>['c'=>"\17\1",'ac'=>"\0\0\201",'dd'=>"\0",'nt'=>1], 'small'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0",'fe'=>1], 'source'=>['c'=>"\0\0\4\4",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1,'b'=>1], 'span'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 'strong'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0",'fe'=>1], 'style'=>['c'=>"\101",'ac'=>"\0",'dd'=>"\0",'to'=>1,'b'=>1], 'sub'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 'summary'=>['c'=>"\0\0\0\2",'ac'=>"\204",'dd'=>"\0",'b'=>1], 'sup'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 'table'=>['c'=>"\3\0\0\200",'ac'=>"\0\2\200",'dd'=>"\0",'nt'=>1,'b'=>1,'cp'=>['p']], 'tbody'=>['c'=>"\0\2",'ac'=>"\0\0\200\0\100",'dd'=>"\0",'nt'=>1,'b'=>1,'cp'=>['tbody','td','th','thead','tr']], 'td'=>['c'=>"\20\0\20",'ac'=>"\1",'dd'=>"\0",'b'=>1,'cp'=>['td','th']], 'template'=>['c'=>"\0\0\10",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'b'=>1], 'textarea'=>['c'=>"\17\1",'ac'=>"\0",'dd'=>"\0",'pre'=>1,'to'=>1], 'tfoot'=>['c'=>"\0\2",'ac'=>"\0\0\200\0\100",'dd'=>"\0",'nt'=>1,'b'=>1,'cp'=>['tbody','td','th','thead','tr']], 'th'=>['c'=>"\0\0\20",'ac'=>"\1",'dd'=>"\200\4\0\40",'b'=>1,'cp'=>['td','th']], 'thead'=>['c'=>"\0\2",'ac'=>"\0\0\200\0\100",'dd'=>"\0",'nt'=>1,'b'=>1], 'time'=>['c'=>"\7",'ac'=>"\4",'ac2'=>'@datetime','dd'=>"\0"], 'title'=>['c'=>"\100",'ac'=>"\0",'dd'=>"\0",'to'=>1,'b'=>1], 'tr'=>['c'=>"\0\2\0\0\100",'ac'=>"\0\0\220",'dd'=>"\0",'nt'=>1,'b'=>1,'cp'=>['td','th','tr']], 'track'=>['c'=>"\0\0\0\100",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1,'b'=>1], 'u'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0",'fe'=>1], 'ul'=>['c'=>"\3",'c1'=>'li','ac'=>"\0\0\200\0\200",'dd'=>"\0",'nt'=>1,'b'=>1,'cp'=>['p']], 'var'=>['c'=>"\7",'ac'=>"\4",'dd'=>"\0"], 'video'=>['c'=>"\57",'c3'=>'@controls','ac'=>"\0\0\0\104",'ac26'=>'not(@src)','dd'=>"\0\0\0\0\0\2",'dd41'=>'@src','t'=>1], 'wbr'=>['c'=>"\5",'ac'=>"\0",'dd'=>"\0",'nt'=>1,'e'=>1,'v'=>1] ]; public static function closesParent(DOMElement $child, DOMElement $parent) { $parentName = $parent->nodeName; $childName = $child->nodeName; return !empty(self::$htmlElements[$childName]['cp']) && \in_array($parentName, self::$htmlElements[$childName]['cp'], \true); } public static function disallowsText(DOMElement $element) { return self::hasProperty($element, 'nt'); } public static function getAllowChildBitfield(DOMElement $element) { return self::getBitfield($element, 'ac'); } public static function getCategoryBitfield(DOMElement $element) { return self::getBitfield($element, 'c'); } public static function getDenyDescendantBitfield(DOMElement $element) { return self::getBitfield($element, 'dd'); } public static function isBlock(DOMElement $element) { return self::hasProperty($element, 'b'); } public static function isEmpty(DOMElement $element) { return self::hasProperty($element, 'e'); } public static function isFormattingElement(DOMElement $element) { return self::hasProperty($element, 'fe'); } public static function isTextOnly(DOMElement $element) { return self::hasProperty($element, 'to'); } public static function isTransparent(DOMElement $element) { return self::hasProperty($element, 't'); } public static function isVoid(DOMElement $element) { return self::hasProperty($element, 'v'); } public static function preservesWhitespace(DOMElement $element) { return self::hasProperty($element, 'pre'); } protected static function evaluate($query, DOMElement $element) { $xpath = new DOMXPath($element->ownerDocument); return $xpath->evaluate('boolean(' . $query . ')', $element); } protected static function getBitfield(DOMElement $element, $name) { $props = self::getProperties($element); $bitfield = self::toBin($props[$name]); foreach (\array_keys(\array_filter(\str_split($bitfield, 1))) as $bitNumber) { $conditionName = $name . $bitNumber; if (isset($props[$conditionName]) && !self::evaluate($props[$conditionName], $element)) $bitfield[$bitNumber] = '0'; } return self::toRaw($bitfield); } protected static function getProperties(DOMElement $element) { return (isset(self::$htmlElements[$element->nodeName])) ? self::$htmlElements[$element->nodeName] : self::$htmlElements['span']; } protected static function hasProperty(DOMElement $element, $propName) { $props = self::getProperties($element); return !empty($props[$propName]) && (!isset($props[$propName . '?']) || self::evaluate($props[$propName . '?'], $element)); } protected static function toBin($raw) { $bin = ''; foreach (\str_split($raw, 1) as $char) $bin .= \strrev(\substr('0000000' . \decbin(\ord($char)), -8)); return $bin; } protected static function toRaw($bin) { return \implode('', \array_map('chr', \array_map('bindec', \array_map('strrev', \str_split($bin, 8))))); } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Helpers; use DOMDocument; use DOMXPath; abstract class NodeLocator { public static function getAttributesByRegexp(DOMDocument $dom, $regexp) { return self::getNodesByRegexp($dom, $regexp, 'attribute'); } public static function getCSSNodes(DOMDocument $dom) { $regexp = '/^style$/i'; $nodes = \array_merge( self::getAttributesByRegexp($dom, $regexp), self::getElementsByRegexp($dom, '/^style$/i') ); return $nodes; } public static function getElementsByRegexp(DOMDocument $dom, $regexp) { return self::getNodesByRegexp($dom, $regexp, 'element'); } public static function getJSNodes(DOMDocument $dom) { $regexp = '/^(?:data-s9e-livepreview-postprocess$|on)/i'; $nodes = \array_merge( self::getAttributesByRegexp($dom, $regexp), self::getElementsByRegexp($dom, '/^script$/i') ); return $nodes; } public static function getObjectParamsByRegexp(DOMDocument $dom, $regexp) { $xpath = new DOMXPath($dom); $nodes = []; foreach (self::getAttributesByRegexp($dom, $regexp) as $attribute) if ($attribute->nodeType === \XML_ATTRIBUTE_NODE) { if (\strtolower($attribute->parentNode->localName) === 'embed') $nodes[] = $attribute; } elseif ($xpath->evaluate('count(ancestor::embed)', $attribute)) $nodes[] = $attribute; foreach ($xpath->query('//object//param') as $param) if (\preg_match($regexp, $param->getAttribute('name'))) $nodes[] = $param; return $nodes; } public static function getURLNodes(DOMDocument $dom) { $regexp = '/(?:^(?:action|background|c(?:ite|lassid|odebase)|data|formaction|href|icon|longdesc|manifest|p(?:ing|luginspage|oster|rofile)|usemap)|src)$/i'; $nodes = self::getAttributesByRegexp($dom, $regexp); foreach (self::getObjectParamsByRegexp($dom, '/^(?:dataurl|movie)$/i') as $param) { $node = $param->getAttributeNode('value'); if ($node) $nodes[] = $node; } return $nodes; } protected static function getNodes(DOMDocument $dom, $type) { $nodes = []; $prefix = ($type === 'attribute') ? '@' : ''; $xpath = new DOMXPath($dom); foreach ($xpath->query('//' . $prefix . '*') as $node) $nodes[] = [$node, $node->nodeName]; foreach ($xpath->query('//xsl:' . $type) as $node) $nodes[] = [$node, $node->getAttribute('name')]; foreach ($xpath->query('//xsl:copy-of') as $node) if (\preg_match('/^' . $prefix . '(\\w+)$/', $node->getAttribute('select'), $m)) $nodes[] = [$node, $m[1]]; return $nodes; } protected static function getNodesByRegexp(DOMDocument $dom, $regexp, $type) { $nodes = []; foreach (self::getNodes($dom, $type) as $_13697a20) { list($node, $name) = $_13697a20; if (\preg_match($regexp, $name)) $nodes[] = $node; } return $nodes; } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Helpers; use RuntimeException; abstract class RegexpBuilder { protected static $characterClassBuilder; public static function fromList(array $words, array $options = []) { if (empty($words)) return ''; $options += [ 'delimiter' => '/', 'caseInsensitive' => \false, 'specialChars' => [], 'unicode' => \true, 'useLookahead' => \false ]; if ($options['caseInsensitive']) { foreach ($words as &$word) $word = \strtr( $word, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz' ); unset($word); } $words = \array_unique($words); \sort($words); $initials = []; $esc = $options['specialChars']; $esc += [$options['delimiter'] => '\\' . $options['delimiter']]; $esc += [ '!' => '!', '-' => '-', ':' => ':', '<' => '<', '=' => '=', '>' => '>', '}' => '}' ]; $splitWords = []; foreach ($words as $word) { $regexp = ($options['unicode']) ? '(.)us' : '(.)s'; if (\preg_match_all($regexp, $word, $matches) === \false) throw new RuntimeException("Invalid UTF-8 string '" . $word . "'"); $splitWord = []; foreach ($matches[0] as $pos => $c) { if (!isset($esc[$c])) $esc[$c] = \preg_quote($c); if ($pos === 0) $initials[] = $esc[$c]; $splitWord[] = $esc[$c]; } $splitWords[] = $splitWord; } self::$characterClassBuilder = new CharacterClassBuilder; self::$characterClassBuilder->delimiter = $options['delimiter']; $regexp = self::assemble([self::mergeChains($splitWords)]); if ($options['useLookahead'] && \count($initials) > 1 && $regexp[0] !== '[') { $useLookahead = \true; foreach ($initials as $initial) if (!self::canBeUsedInCharacterClass($initial)) { $useLookahead = \false; break; } if ($useLookahead) $regexp = '(?=' . self::generateCharacterClass($initials) . ')' . $regexp; } return $regexp; } protected static function mergeChains(array $chains, $preventRemerge = \false) { if (!isset($chains[1])) return $chains[0]; $mergedChain = self::removeLongestCommonPrefix($chains); if (!isset($chains[0][0]) && !\array_filter($chains)) return $mergedChain; $suffix = self::removeLongestCommonSuffix($chains); if (isset($chains[1])) { self::optimizeDotChains($chains); self::optimizeCatchallChains($chains); } $endOfChain = \false; $remerge = \false; $groups = []; foreach ($chains as $chain) { if (!isset($chain[0])) { $endOfChain = \true; continue; } $head = $chain[0]; if (isset($groups[$head])) $remerge = \true; $groups[$head][] = $chain; } $characterClass = []; foreach ($groups as $head => $groupChains) { $head = (string) $head; if ($groupChains === [[$head]] && self::canBeUsedInCharacterClass($head)) $characterClass[$head] = $head; } \sort($characterClass); if (isset($characterClass[1])) { foreach ($characterClass as $char) unset($groups[$char]); $head = self::generateCharacterClass($characterClass); $groups[$head][] = [$head]; $groups = [$head => $groups[$head]] + $groups; } if ($remerge && !$preventRemerge) { $mergedChains = []; foreach ($groups as $head => $groupChains) $mergedChains[] = self::mergeChains($groupChains); self::mergeTails($mergedChains); $regexp = \implode('', self::mergeChains($mergedChains, \true)); if ($endOfChain) $regexp = self::makeRegexpOptional($regexp); $mergedChain[] = $regexp; } else { self::mergeTails($chains); $mergedChain[] = self::assemble($chains); } foreach ($suffix as $atom) $mergedChain[] = $atom; return $mergedChain; } protected static function mergeTails(array &$chains) { self::mergeTailsCC($chains); self::mergeTailsAltern($chains); $chains = \array_values($chains); } protected static function mergeTailsCC(array &$chains) { $groups = []; foreach ($chains as $k => $chain) if (isset($chain[1]) && !isset($chain[2]) && self::canBeUsedInCharacterClass($chain[0])) $groups[$chain[1]][$k] = $chain; foreach ($groups as $groupChains) { if (\count($groupChains) < 2) continue; $chains = \array_diff_key($chains, $groupChains); $chains[] = self::mergeChains(\array_values($groupChains)); } } protected static function mergeTailsAltern(array &$chains) { $groups = []; foreach ($chains as $k => $chain) if (!empty($chain)) { $tail = \array_slice($chain, -1); $groups[$tail[0]][$k] = $chain; } foreach ($groups as $tail => $groupChains) { if (\count($groupChains) < 2) continue; $mergedChain = self::mergeChains(\array_values($groupChains)); $oldLen = 0; foreach ($groupChains as $groupChain) $oldLen += \array_sum(\array_map('strlen', $groupChain)); if ($oldLen <= \array_sum(\array_map('strlen', $mergedChain))) continue; $chains = \array_diff_key($chains, $groupChains); $chains[] = $mergedChain; } } protected static function removeLongestCommonPrefix(array &$chains) { $pLen = 0; while (1) { $c = \null; foreach ($chains as $chain) { if (!isset($chain[$pLen])) break 2; if (!isset($c)) { $c = $chain[$pLen]; continue; } if ($chain[$pLen] !== $c) break 2; } ++$pLen; } if (!$pLen) return []; $prefix = \array_slice($chains[0], 0, $pLen); foreach ($chains as &$chain) $chain = \array_slice($chain, $pLen); unset($chain); return $prefix; } protected static function removeLongestCommonSuffix(array &$chains) { $chainsLen = \array_map('count', $chains); $maxLen = \min($chainsLen); if (\max($chainsLen) === $maxLen) --$maxLen; $sLen = 0; while ($sLen < $maxLen) { $c = \null; foreach ($chains as $k => $chain) { $pos = $chainsLen[$k] - ($sLen + 1); if (!isset($c)) { $c = $chain[$pos]; continue; } if ($chain[$pos] !== $c) break 2; } ++$sLen; } if (!$sLen) return []; $suffix = \array_slice($chains[0], -$sLen); foreach ($chains as &$chain) $chain = \array_slice($chain, 0, -$sLen); unset($chain); return $suffix; } protected static function assemble(array $chains) { $endOfChain = \false; $regexps = []; $characterClass = []; foreach ($chains as $chain) { if (empty($chain)) { $endOfChain = \true; continue; } if (!isset($chain[1]) && self::canBeUsedInCharacterClass($chain[0])) $characterClass[$chain[0]] = $chain[0]; else $regexps[] = \implode('', $chain); } if (!empty($characterClass)) { \sort($characterClass); $regexp = (isset($characterClass[1])) ? self::generateCharacterClass($characterClass) : $characterClass[0]; \array_unshift($regexps, $regexp); } if (empty($regexps)) return ''; if (isset($regexps[1])) { $regexp = \implode('|', $regexps); $regexp = ((self::canUseAtomicGrouping($regexp)) ? '(?>' : '(?:') . $regexp . ')'; } else $regexp = $regexps[0]; if ($endOfChain) $regexp = self::makeRegexpOptional($regexp); return $regexp; } protected static function makeRegexpOptional($regexp) { if (\preg_match('#^\\.\\+\\??$#', $regexp)) return \str_replace('+', '*', $regexp); if (\preg_match('#^(\\\\?.)((?:\\1\\?)+)$#Du', $regexp, $m)) return $m[1] . '?' . $m[2]; if (\preg_match('#^(?:[$^]|\\\\[bBAZzGQEK])$#', $regexp)) return ''; if (\preg_match('#^\\\\?.$#Dus', $regexp)) $isAtomic = \true; elseif (\preg_match('#^[^[(].#s', $regexp)) $isAtomic = \false; else { $def = RegexpParser::parse('#' . $regexp . '#'); $tokens = $def['tokens']; switch (\count($tokens)) { case 1: $startPos = $tokens[0]['pos']; $len = $tokens[0]['len']; $isAtomic = (bool) ($startPos === 0 && $len === \strlen($regexp)); if ($isAtomic && $tokens[0]['type'] === 'characterClass') { $regexp = \rtrim($regexp, '+*?'); if (!empty($tokens[0]['quantifiers']) && $tokens[0]['quantifiers'] !== '?') $regexp .= '*'; } break; case 2: if ($tokens[0]['type'] === 'nonCapturingSubpatternStart' && $tokens[1]['type'] === 'nonCapturingSubpatternEnd') { $startPos = $tokens[0]['pos']; $len = $tokens[1]['pos'] + $tokens[1]['len']; $isAtomic = (bool) ($startPos === 0 && $len === \strlen($regexp)); break; } default: $isAtomic = \false; } } if (!$isAtomic) $regexp = ((self::canUseAtomicGrouping($regexp)) ? '(?>' : '(?:') . $regexp . ')'; $regexp .= '?'; return $regexp; } protected static function generateCharacterClass(array $chars) { return self::$characterClassBuilder->fromList($chars); } protected static function canBeUsedInCharacterClass($char) { if (\preg_match('#^\\\\[aefnrtdDhHsSvVwW]$#D', $char)) return \true; if (\preg_match('#^\\\\[^A-Za-z0-9]$#Dus', $char)) return \true; if (\preg_match('#..#Dus', $char)) return \false; if (\preg_quote($char) !== $char && !\preg_match('#^[-!:<=>}]$#D', $char)) return \false; return \true; } protected static function optimizeDotChains(array &$chains) { $validAtoms = [ '\\d' => 1, '\\D' => 1, '\\h' => 1, '\\H' => 1, '\\s' => 1, '\\S' => 1, '\\v' => 1, '\\V' => 1, '\\w' => 1, '\\W' => 1, '\\^' => 1, '\\$' => 1, '\\.' => 1, '\\?' => 1, '\\[' => 1, '\\]' => 1, '\\(' => 1, '\\)' => 1, '\\+' => 1, '\\*' => 1, '\\\\' => 1 ]; do { $hasMoreDots = \false; foreach ($chains as $k1 => $dotChain) { $dotKeys = \array_keys($dotChain, '.?', \true); if (!empty($dotKeys)) { $dotChain[$dotKeys[0]] = '.'; $chains[$k1] = $dotChain; \array_splice($dotChain, $dotKeys[0], 1); $chains[] = $dotChain; if (isset($dotKeys[1])) $hasMoreDots = \true; } } } while ($hasMoreDots); foreach ($chains as $k1 => $dotChain) { $dotKeys = \array_keys($dotChain, '.', \true); if (empty($dotKeys)) continue; foreach ($chains as $k2 => $tmpChain) { if ($k2 === $k1) continue; foreach ($dotKeys as $dotKey) { if (!isset($tmpChain[$dotKey])) continue 2; if (!\preg_match('#^.$#Du', \preg_quote($tmpChain[$dotKey])) && !isset($validAtoms[$tmpChain[$dotKey]])) continue 2; $tmpChain[$dotKey] = '.'; } if ($tmpChain === $dotChain) unset($chains[$k2]); } } } protected static function optimizeCatchallChains(array &$chains) { $precedence = [ '.*' => 3, '.*?' => 2, '.+' => 1, '.+?' => 0 ]; $tails = []; foreach ($chains as $k => $chain) { if (!isset($chain[0])) continue; $head = $chain[0]; if (!isset($precedence[$head])) continue; $tail = \implode('', \array_slice($chain, 1)); if (!isset($tails[$tail]) || $precedence[$head] > $tails[$tail]['precedence']) $tails[$tail] = [ 'key' => $k, 'precedence' => $precedence[$head] ]; } $catchallChains = []; foreach ($tails as $tail => $info) $catchallChains[$info['key']] = $chains[$info['key']]; foreach ($catchallChains as $k1 => $catchallChain) { $headExpr = $catchallChain[0]; $tailExpr = \false; $match = \array_slice($catchallChain, 1); if (isset($catchallChain[1]) && isset($precedence[\end($catchallChain)])) $tailExpr = \array_pop($match); $matchCnt = \count($match); foreach ($chains as $k2 => $chain) { if ($k2 === $k1) continue; $start = 0; $end = \count($chain); if ($headExpr[1] === '+') { $found = \false; foreach ($chain as $start => $atom) if (self::matchesAtLeastOneCharacter($atom)) { $found = \true; break; } if (!$found) continue; } if ($tailExpr === \false) $end = $start; else { if ($tailExpr[1] === '+') { $found = \false; while (--$end > $start) if (self::matchesAtLeastOneCharacter($chain[$end])) { $found = \true; break; } if (!$found) continue; } $end -= $matchCnt; } while ($start <= $end) { if (\array_slice($chain, $start, $matchCnt) === $match) { unset($chains[$k2]); break; } ++$start; } } } } protected static function matchesAtLeastOneCharacter($expr) { if (\preg_match('#^[$*?^]$#', $expr)) return \false; if (\preg_match('#^.$#u', $expr)) return \true; if (\preg_match('#^.\\+#u', $expr)) return \true; if (\preg_match('#^\\\\[^bBAZzGQEK1-9](?![*?])#', $expr)) return \true; return \false; } protected static function canUseAtomicGrouping($expr) { if (\preg_match('#(?\\\\\\\\)*\\.#', $expr)) return \false; if (\preg_match('#(?\\\\\\\\)*[+*]#', $expr)) return \false; if (\preg_match('#(?\\\\\\\\)*\\(?(?\\\\\\\\)*\\\\[a-z0-9]#', $expr)) return \false; return \true; } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Helpers; use s9e\TextFormatter\Configurator\Collections\Ruleset; use s9e\TextFormatter\Configurator\Collections\TagCollection; abstract class RulesHelper { public static function getBitfields(TagCollection $tags, Ruleset $rootRules) { $rules = ['*root*' => \iterator_to_array($rootRules)]; foreach ($tags as $tagName => $tag) $rules[$tagName] = \iterator_to_array($tag->rules); $matrix = self::unrollRules($rules); self::pruneMatrix($matrix); $groupedTags = []; foreach (\array_keys($matrix) as $tagName) { if ($tagName === '*root*') continue; $k = ''; foreach ($matrix as $tagMatrix) { $k .= $tagMatrix['allowedChildren'][$tagName]; $k .= $tagMatrix['allowedDescendants'][$tagName]; } $groupedTags[$k][] = $tagName; } $bitTag = []; $bitNumber = 0; $tagsConfig = []; foreach ($groupedTags as $tagNames) { foreach ($tagNames as $tagName) { $tagsConfig[$tagName]['bitNumber'] = $bitNumber; $bitTag[$bitNumber] = $tagName; } ++$bitNumber; } foreach ($matrix as $tagName => $tagMatrix) { $allowedChildren = ''; $allowedDescendants = ''; foreach ($bitTag as $targetName) { $allowedChildren .= $tagMatrix['allowedChildren'][$targetName]; $allowedDescendants .= $tagMatrix['allowedDescendants'][$targetName]; } $tagsConfig[$tagName]['allowed'] = self::pack($allowedChildren, $allowedDescendants); } $return = [ 'root' => $tagsConfig['*root*'], 'tags' => $tagsConfig ]; unset($return['tags']['*root*']); return $return; } protected static function initMatrix(array $rules) { $matrix = []; $tagNames = \array_keys($rules); foreach ($rules as $tagName => $tagRules) { $matrix[$tagName]['allowedChildren'] = \array_fill_keys($tagNames, 0); $matrix[$tagName]['allowedDescendants'] = \array_fill_keys($tagNames, 0); } return $matrix; } protected static function applyTargetedRule(array &$matrix, $rules, $ruleName, $key, $value) { foreach ($rules as $tagName => $tagRules) { if (!isset($tagRules[$ruleName])) continue; foreach ($tagRules[$ruleName] as $targetName) $matrix[$tagName][$key][$targetName] = $value; } } protected static function unrollRules(array $rules) { $matrix = self::initMatrix($rules); $tagNames = \array_keys($rules); foreach ($rules as $tagName => $tagRules) { if (!empty($tagRules['ignoreTags'])) { $rules[$tagName]['denyChild'] = $tagNames; $rules[$tagName]['denyDescendant'] = $tagNames; } if (!empty($tagRules['requireParent'])) { $denyParents = \array_diff($tagNames, $tagRules['requireParent']); foreach ($denyParents as $parentName) $rules[$parentName]['denyChild'][] = $tagName; } } self::applyTargetedRule($matrix, $rules, 'allowChild', 'allowedChildren', 1); self::applyTargetedRule($matrix, $rules, 'allowDescendant', 'allowedDescendants', 1); self::applyTargetedRule($matrix, $rules, 'denyChild', 'allowedChildren', 0); self::applyTargetedRule($matrix, $rules, 'denyDescendant', 'allowedDescendants', 0); return $matrix; } protected static function pruneMatrix(array &$matrix) { $usableTags = ['*root*' => 1]; $parentTags = $usableTags; do { $nextTags = []; foreach (\array_keys($parentTags) as $tagName) $nextTags += \array_filter($matrix[$tagName]['allowedChildren']); $parentTags = \array_diff_key($nextTags, $usableTags); $parentTags = \array_intersect_key($parentTags, $matrix); $usableTags += $parentTags; } while (!empty($parentTags)); $matrix = \array_intersect_key($matrix, $usableTags); unset($usableTags['*root*']); foreach ($matrix as $tagName => &$tagMatrix) { $tagMatrix['allowedChildren'] = \array_intersect_key($tagMatrix['allowedChildren'], $usableTags); $tagMatrix['allowedDescendants'] = \array_intersect_key($tagMatrix['allowedDescendants'], $usableTags); } unset($tagMatrix); } protected static function pack($allowedChildren, $allowedDescendants) { $allowedChildren = \str_split($allowedChildren, 8); $allowedDescendants = \str_split($allowedDescendants, 8); $allowed = []; foreach (\array_keys($allowedChildren) as $k) $allowed[] = \bindec(\sprintf( '%1$08s%2$08s', \strrev($allowedDescendants[$k]), \strrev($allowedChildren[$k]) )); return $allowed; } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Helpers; use DOMAttr; use DOMCharacterData; use DOMDocument; use DOMElement; use DOMNode; use DOMProcessingInstruction; use DOMText; use DOMXPath; abstract class TemplateHelper { const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform'; public static function getAttributesByRegexp(DOMDocument $dom, $regexp) { return NodeLocator::getAttributesByRegexp($dom, $regexp); } public static function getCSSNodes(DOMDocument $dom) { return NodeLocator::getCSSNodes($dom); } public static function getElementsByRegexp(DOMDocument $dom, $regexp) { return NodeLocator::getElementsByRegexp($dom, $regexp); } public static function getJSNodes(DOMDocument $dom) { return NodeLocator::getJSNodes($dom); } public static function getObjectParamsByRegexp(DOMDocument $dom, $regexp) { return NodeLocator::getObjectParamsByRegexp($dom, $regexp); } public static function getParametersFromXSL($xsl) { $paramNames = []; $xpath = new DOMXPath(TemplateLoader::load($xsl)); $query = '//xsl:*/@match | //xsl:*/@select | //xsl:*/@test'; foreach ($xpath->query($query) as $attribute) { $expr = $attribute->value; $paramNames += \array_flip(self::getParametersFromExpression($attribute, $expr)); } $query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/@*[contains(., "{")]'; foreach ($xpath->query($query) as $attribute) foreach (AVTHelper::parse($attribute->value) as $token) if ($token[0] === 'expression') { $expr = $token[1]; $paramNames += \array_flip(self::getParametersFromExpression($attribute, $expr)); } \ksort($paramNames); return \array_keys($paramNames); } public static function getURLNodes(DOMDocument $dom) { return NodeLocator::getURLNodes($dom); } public static function highlightNode(DOMNode $node, $prepend, $append) { $dom = $node->ownerDocument->cloneNode(\true); $dom->formatOutput = \true; $xpath = new DOMXPath($dom); $node = $xpath->query($node->getNodePath())->item(0); $uniqid = \uniqid('_'); if ($node instanceof DOMAttr) $node->value .= $uniqid; elseif ($node instanceof DOMElement) $node->setAttribute($uniqid, ''); elseif ($node instanceof DOMCharacterData || $node instanceof DOMProcessingInstruction) $node->data .= $uniqid; $docXml = TemplateLoader::innerXML($dom->documentElement); $docXml = \trim(\str_replace("\n ", "\n", $docXml)); $nodeHtml = \htmlspecialchars(\trim($dom->saveXML($node))); $docHtml = \htmlspecialchars($docXml); $html = \str_replace($nodeHtml, $prepend . $nodeHtml . $append, $docHtml); $html = \str_replace(' ' . $uniqid . '=""', '', $html); $html = \str_replace($uniqid, '', $html); return $html; } public static function loadTemplate($template) { return TemplateLoader::load($template); } public static function replaceHomogeneousTemplates(array &$templates, $minCount = 3) { $expr = 'name()'; $tagNames = []; foreach ($templates as $tagName => $template) { $elName = \strtolower(\preg_replace('/^[^:]+:/', '', $tagName)); if ($template === '<' . $elName . '>') { $tagNames[] = $tagName; if (\strpos($tagName, ':') !== \false) $expr = 'local-name()'; } } if (\count($tagNames) < $minCount) return; $chars = \preg_replace('/[^A-Z]+/', '', \count_chars(\implode('', $tagNames), 3)); if ($chars > '') $expr = 'translate(' . $expr . ",'" . $chars . "','" . \strtolower($chars) . "')"; $template = ''; foreach ($tagNames as $tagName) $templates[$tagName] = $template; } public static function replaceTokens($template, $regexp, $fn) { return TemplateModifier::replaceTokens($template, $regexp, $fn); } public static function saveTemplate(DOMDocument $dom) { return TemplateLoader::save($dom); } protected static function getParametersFromExpression(DOMNode $node, $expr) { $varNames = XPathHelper::getVariables($expr); $paramNames = []; $xpath = new DOMXPath($node->ownerDocument); foreach ($varNames as $name) { $query = 'ancestor-or-self::*/preceding-sibling::xsl:variable[@name="' . $name . '"]'; if (!$xpath->query($query, $node)->length) $paramNames[] = $name; } return $paramNames; } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Helpers; use DOMElement; use DOMXPath; class TemplateInspector { const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform'; protected $allowChildBitfields = []; protected $allowsChildElements; protected $allowsText; protected $branches; protected $contentBitfield = "\0"; protected $defaultBranchBitfield; protected $denyDescendantBitfield = "\0"; protected $dom; protected $hasElements = \false; protected $hasRootText; protected $isBlock = \false; protected $isEmpty; protected $isFormattingElement; protected $isPassthrough = \false; protected $isTransparent = \false; protected $isVoid; protected $leafNodes = []; protected $preservesNewLines = \false; protected $rootBitfields = []; protected $rootNodes = []; protected $xpath; public function __construct($template) { $this->dom = TemplateHelper::loadTemplate($template); $this->xpath = new DOMXPath($this->dom); $this->defaultBranchBitfield = ElementInspector::getAllowChildBitfield($this->dom->createElement('div')); $this->analyseRootNodes(); $this->analyseBranches(); $this->analyseContent(); } public function allowsChild(TemplateInspector $child) { if (!$this->allowsDescendant($child)) return \false; foreach ($child->rootBitfields as $rootBitfield) foreach ($this->allowChildBitfields as $allowChildBitfield) if (!self::match($rootBitfield, $allowChildBitfield)) return \false; return ($this->allowsText || !$child->hasRootText); } public function allowsDescendant(TemplateInspector $descendant) { if (self::match($descendant->contentBitfield, $this->denyDescendantBitfield)) return \false; return ($this->allowsChildElements || !$descendant->hasElements); } public function allowsChildElements() { return $this->allowsChildElements; } public function allowsText() { return $this->allowsText; } public function closesParent(TemplateInspector $parent) { foreach ($this->rootNodes as $rootNode) foreach ($parent->leafNodes as $leafNode) if (ElementInspector::closesParent($rootNode, $leafNode)) return \true; return \false; } public function evaluate($expr, DOMElement $node = \null) { return $this->xpath->evaluate($expr, $node); } public function isBlock() { return $this->isBlock; } public function isFormattingElement() { return $this->isFormattingElement; } public function isEmpty() { return $this->isEmpty; } public function isPassthrough() { return $this->isPassthrough; } public function isTransparent() { return $this->isTransparent; } public function isVoid() { return $this->isVoid; } public function preservesNewLines() { return $this->preservesNewLines; } protected function analyseContent() { $query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]'; foreach ($this->xpath->query($query) as $node) { $this->contentBitfield |= ElementInspector::getCategoryBitfield($node); $this->hasElements = \true; } $this->isPassthrough = (bool) $this->evaluate('count(//xsl:apply-templates)'); } protected function analyseRootNodes() { $query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"][not(ancestor::*[namespace-uri() != "' . self::XMLNS_XSL . '"])]'; foreach ($this->xpath->query($query) as $node) { $this->rootNodes[] = $node; if ($this->elementIsBlock($node)) $this->isBlock = \true; $this->rootBitfields[] = ElementInspector::getCategoryBitfield($node); } $predicate = '[not(ancestor::*[namespace-uri() != "' . self::XMLNS_XSL . '"])]'; $predicate .= '[not(ancestor::xsl:attribute | ancestor::xsl:comment | ancestor::xsl:variable)]'; $query = '//text()[normalize-space() != ""]' . $predicate . '|//xsl:text[normalize-space() != ""]' . $predicate . '|//xsl:value-of' . $predicate; $this->hasRootText = (bool) $this->evaluate('count(' . $query . ')'); } protected function analyseBranches() { $this->branches = []; foreach ($this->xpath->query('//xsl:apply-templates') as $applyTemplates) { $query = 'ancestor::*[namespace-uri() != "' . self::XMLNS_XSL . '"]'; $this->branches[] = \iterator_to_array($this->xpath->query($query, $applyTemplates)); } $this->computeAllowsChildElements(); $this->computeAllowsText(); $this->computeBitfields(); $this->computeFormattingElement(); $this->computeIsEmpty(); $this->computeIsTransparent(); $this->computeIsVoid(); $this->computePreservesNewLines(); $this->storeLeafNodes(); } protected function anyBranchHasProperty($methodName) { foreach ($this->branches as $branch) foreach ($branch as $element) if (ElementInspector::$methodName($element)) return \true; return \false; } protected function computeBitfields() { if (empty($this->branches)) { $this->allowChildBitfields = ["\0"]; return; } foreach ($this->branches as $branch) { $branchBitfield = $this->defaultBranchBitfield; foreach ($branch as $element) { if (!ElementInspector::isTransparent($element)) $branchBitfield = "\0"; $branchBitfield |= ElementInspector::getAllowChildBitfield($element); $this->denyDescendantBitfield |= ElementInspector::getDenyDescendantBitfield($element); } $this->allowChildBitfields[] = $branchBitfield; } } protected function computeAllowsChildElements() { $this->allowsChildElements = ($this->anyBranchHasProperty('isTextOnly')) ? \false : !empty($this->branches); } protected function computeAllowsText() { foreach (\array_filter($this->branches) as $branch) if (ElementInspector::disallowsText(\end($branch))) { $this->allowsText = \false; return; } $this->allowsText = \true; } protected function computeFormattingElement() { foreach ($this->branches as $branch) foreach ($branch as $element) if (!ElementInspector::isFormattingElement($element) && !$this->isFormattingSpan($element)) { $this->isFormattingElement = \false; return; } $this->isFormattingElement = (bool) \count(\array_filter($this->branches)); } protected function computeIsEmpty() { $this->isEmpty = ($this->anyBranchHasProperty('isEmpty')) || empty($this->branches); } protected function computeIsTransparent() { foreach ($this->branches as $branch) foreach ($branch as $element) if (!ElementInspector::isTransparent($element)) { $this->isTransparent = \false; return; } $this->isTransparent = !empty($this->branches); } protected function computeIsVoid() { $this->isVoid = ($this->anyBranchHasProperty('isVoid')) || empty($this->branches); } protected function computePreservesNewLines() { foreach ($this->branches as $branch) { $style = ''; foreach ($branch as $element) $style .= $this->getStyle($element, \true); if (\preg_match('(.*white-space\\s*:\\s*(no|pre))is', $style, $m) && \strtolower($m[1]) === 'pre') { $this->preservesNewLines = \true; return; } } $this->preservesNewLines = \false; } protected function elementIsBlock(DOMElement $element) { $style = $this->getStyle($element); if (\preg_match('(\\bdisplay\\s*:\\s*block)i', $style)) return \true; if (\preg_match('(\\bdisplay\\s*:\\s*(?:inli|no)ne)i', $style)) return \false; return ElementInspector::isBlock($element); } protected function getStyle(DOMElement $node, $deep = \false) { $style = ''; if (ElementInspector::preservesWhitespace($node)) $style .= 'white-space:pre;'; $style .= $node->getAttribute('style'); $query = (($deep) ? './/' : './') . 'xsl:attribute[@name="style"]'; foreach ($this->xpath->query($query, $node) as $attribute) $style .= ';' . $attribute->textContent; return $style; } protected function isFormattingSpan(DOMElement $node) { if ($node->nodeName !== 'span') return \false; if ($node->getAttribute('class') === '' && $node->getAttribute('style') === '') return \false; foreach ($node->attributes as $attrName => $attribute) if ($attrName !== 'class' && $attrName !== 'style') return \false; return \true; } protected function storeLeafNodes() { foreach (\array_filter($this->branches) as $branch) $this->leafNodes[] = \end($branch); } protected static function match($bitfield1, $bitfield2) { return (\trim($bitfield1 & $bitfield2, "\0") !== ''); } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Helpers; use DOMDocument; use DOMElement; use DOMXPath; use RuntimeException; abstract class TemplateLoader { const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform'; public static function innerXML(DOMElement $element) { $xml = $element->ownerDocument->saveXML($element); $pos = 1 + \strpos($xml, '>'); $len = \strrpos($xml, '<') - $pos; return ($len < 1) ? '' : \substr($xml, $pos, $len); } public static function load($template) { $dom = self::loadAsXML($template) ?: self::loadAsXML(self::fixEntities($template)); if ($dom) return $dom; if (\strpos($template, 'message); } return self::loadAsHTML($template); } public static function save(DOMDocument $dom) { $xml = self::innerXML($dom->documentElement); if (\strpos($xml, 'xmlns:xsl') !== \false) $xml = \preg_replace('((<[^>]+?) xmlns:xsl="' . self::XMLNS_XSL . '")', '$1', $xml); return $xml; } protected static function fixEntities($template) { return \preg_replace_callback( '(&(?!quot;|amp;|apos;|lt;|gt;)\\w+;)', function ($m) { return \html_entity_decode($m[0], \ENT_NOQUOTES, 'UTF-8'); }, \preg_replace('(&(?![A-Za-z0-9]+;|#\\d+;|#x[A-Fa-f0-9]+;))', '&', $template) ); } protected static function loadAsHTML($template) { $dom = new DOMDocument; $html = '
' . $template . '
'; $useErrors = \libxml_use_internal_errors(\true); $dom->loadHTML($html, \LIBXML_NSCLEAN); self::removeInvalidAttributes($dom); \libxml_use_internal_errors($useErrors); $xml = '' . self::innerXML($dom->documentElement->firstChild->firstChild) . ''; $useErrors = \libxml_use_internal_errors(\true); $dom->loadXML($xml, \LIBXML_NSCLEAN); \libxml_use_internal_errors($useErrors); return $dom; } protected static function loadAsXML($template) { $xml = '' . $template . ''; $useErrors = \libxml_use_internal_errors(\true); $dom = new DOMDocument; $success = $dom->loadXML($xml, \LIBXML_NSCLEAN); self::removeInvalidAttributes($dom); \libxml_use_internal_errors($useErrors); return ($success) ? $dom : \false; } protected static function removeInvalidAttributes(DOMDocument $dom) { $xpath = new DOMXPath($dom); foreach ($xpath->query('//@*') as $attribute) if (!\preg_match('(^(?:[-\\w]+:)?(?!\\d)[-\\w]+$)D', $attribute->nodeName)) $attribute->parentNode->removeAttributeNode($attribute); } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Helpers; use s9e\TextFormatter\Configurator\Helpers\TemplateParser\Normalizer; use s9e\TextFormatter\Configurator\Helpers\TemplateParser\Optimizer; use s9e\TextFormatter\Configurator\Helpers\TemplateParser\Parser; class TemplateParser { const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform'; public static $voidRegexp = '/^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/Di'; public static function parse($template) { $parser = new Parser(new Normalizer(new Optimizer)); return $parser->parse($template); } public static function parseEqualityExpr($expr) { return XPathHelper::parseEqualityExpr($expr); } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Helpers\TemplateParser; use DOMDocument; use DOMElement; use DOMNode; use DOMXPath; abstract class IRProcessor { const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform'; protected $xpath; protected function appendElement(DOMElement $parentNode, $name, $value = '') { return $parentNode->appendChild($parentNode->ownerDocument->createElement($name, $value)); } protected function createXPath(DOMDocument $dom) { $this->xpath = new DOMXPath($dom); } protected function evaluate($expr, DOMNode $node = \null) { return (isset($node)) ? $this->xpath->evaluate($expr, $node) : $this->xpath->evaluate($expr); } protected function query($query, DOMNode $node = \null) { return (isset($node)) ? $this->xpath->query($query, $node) : $this->xpath->query($query); } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Helpers; use RuntimeException; use s9e\TextFormatter\Utils\XPath; abstract class XPathHelper { public static function getVariables($expr) { $expr = \preg_replace('/(["\']).*?\\1/s', '$1$1', $expr); \preg_match_all('/\\$(\\w+)/', $expr, $matches); $varNames = \array_unique($matches[1]); \sort($varNames); return $varNames; } public static function isExpressionNumeric($expr) { $expr = \strrev(\preg_replace('(\\((?!\\s*(?!vid(?!\\w))\\w))', ' ', \strrev($expr))); $expr = \str_replace(')', ' ', $expr); if (\preg_match('(^\\s*([$@][-\\w]++|-?\\.\\d++|-?\\d++(?:\\.\\d++)?)(?>\\s*(?>[-+*]|div)\\s*(?1))++\\s*$)', $expr)) return \true; return \false; } public static function minify($expr) { $old = $expr; $strings = []; $expr = \preg_replace_callback( '/"[^"]*"|\'[^\']*\'/', function ($m) use (&$strings) { $uniqid = '(' . \sha1(\uniqid()) . ')'; $strings[$uniqid] = $m[0]; return $uniqid; }, \trim($expr) ); if (\preg_match('/[\'"]/', $expr)) throw new RuntimeException("Cannot parse XPath expression '" . $old . "'"); $expr = \preg_replace('/\\s+/', ' ', $expr); $expr = \preg_replace('/([-a-z_0-9]) ([^-a-z_0-9])/i', '$1$2', $expr); $expr = \preg_replace('/([^-a-z_0-9]) ([-a-z_0-9])/i', '$1$2', $expr); $expr = \preg_replace('/(?!- -)([^-a-z_0-9]) ([^-a-z_0-9])/i', '$1$2', $expr); $expr = \preg_replace('/ - ([a-z_0-9])/i', ' -$1', $expr); $expr = \preg_replace('/((?:^|[ \\(])\\d+) div ?/', '$1div', $expr); $expr = \preg_replace('/([^-a-z_0-9]div) (?=[$0-9@])/', '$1', $expr); $expr = \strtr($expr, $strings); return $expr; } public static function parseEqualityExpr($expr) { $eq = '(?(?@[-\\w]+|\\$\\w+|\\.)(?\\s*=\\s*)(?:(?(?"[^"]*"|\'[^\']*\')|0|[1-9][0-9]*)|(?concat\\(\\s*(?&string)\\s*(?:,\\s*(?&string)\\s*)+\\)))|(?:(?(?&literal))|(?(?&concat)))(?&operator)(?(?&key)))'; $regexp = '(^(?J)\\s*' . $eq . '\\s*(?:or\\s*(?&equality)\\s*)*$)'; if (!\preg_match($regexp, $expr)) return \false; \preg_match_all("((?J)$eq)", $expr, $matches, \PREG_SET_ORDER); $map = []; foreach ($matches as $m) { $key = $m['key']; $value = (!empty($m['concat'])) ? self::evaluateConcat($m['concat']) : self::evaluateLiteral($m['literal']); $map[$key][] = $value; } return $map; } protected static function evaluateConcat($expr) { \preg_match_all('(\'[^\']*\'|"[^"]*")', $expr, $strings); $value = ''; foreach ($strings[0] as $string) $value .= \substr($string, 1, -1); return $value; } protected static function evaluateLiteral($expr) { if ($expr[0] === '"' || $expr[0] === "'") $expr = \substr($expr, 1, -1); return $expr; } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Items; use DOMDocument; use s9e\TextFormatter\Configurator\Helpers\TemplateInspector; use s9e\TextFormatter\Configurator\Helpers\TemplateHelper; use s9e\TextFormatter\Configurator\TemplateNormalizer; class Template { protected $inspector; protected $isNormalized = \false; protected $template; public function __construct($template) { $this->template = $template; } public function __call($methodName, $args) { return \call_user_func_array([$this->getInspector(), $methodName], $args); } public function __toString() { return $this->template; } public function asDOM() { $xml = '' . $this->__toString() . ''; $dom = new TemplateDocument($this); $dom->loadXML($xml); return $dom; } public function getCSSNodes() { return TemplateHelper::getCSSNodes($this->asDOM()); } public function getInspector() { if (!isset($this->inspector)) $this->inspector = new TemplateInspector($this->__toString()); return $this->inspector; } public function getJSNodes() { return TemplateHelper::getJSNodes($this->asDOM()); } public function getURLNodes() { return TemplateHelper::getURLNodes($this->asDOM()); } public function getParameters() { return TemplateHelper::getParametersFromXSL($this->__toString()); } public function isNormalized($bool = \null) { if (isset($bool)) $this->isNormalized = $bool; return $this->isNormalized; } public function normalize(TemplateNormalizer $templateNormalizer) { $this->inspector = \null; $this->template = $templateNormalizer->normalizeTemplate($this->template); $this->isNormalized = \true; } public function replaceTokens($regexp, $fn) { $this->inspector = \null; $this->template = TemplateHelper::replaceTokens($this->template, $regexp, $fn); $this->isNormalized = \false; } public function setContent($template) { $this->inspector = \null; $this->template = (string) $template; $this->isNormalized = \false; } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\JavaScript; use InvalidArgumentException; class FunctionProvider { public static $cache = [ 'addslashes'=>'function(str) { return str.replace(/["\'\\\\]/g, \'\\\\$&\').replace(/\\u0000/g, \'\\\\0\'); }', 'dechex'=>'function(str) { return parseInt(str).toString(16); }', 'intval'=>'function(str) { return parseInt(str) || 0; }', 'ltrim'=>'function(str) { return str.replace(/^[ \\n\\r\\t\\0\\x0B]+/g, \'\'); }', 'mb_strtolower'=>'function(str) { return str.toLowerCase(); }', 'mb_strtoupper'=>'function(str) { return str.toUpperCase(); }', 'mt_rand'=>'function(min, max) { return (min + Math.floor(Math.random() * (max + 1 - min))); }', 'rawurlencode'=>'function(str) { return encodeURIComponent(str).replace( /[!\'()*]/g, /** * @param {!string} c */ function(c) { return \'%\' + c.charCodeAt(0).toString(16).toUpperCase(); } ); }', 'rtrim'=>'function(str) { return str.replace(/[ \\n\\r\\t\\0\\x0B]+$/g, \'\'); }', 'str_rot13'=>'function(str) { return str.replace( /[a-z]/gi, function(c) { return String.fromCharCode(c.charCodeAt(0) + ((c.toLowerCase() < \'n\') ? 13 : -13)); } ); }', 'stripslashes'=>'function(str) { // NOTE: this will not correctly transform \\0 into a NULL byte. I consider this a feature // rather than a bug. There\'s no reason to use NULL bytes in a text. return str.replace(/\\\\([\\s\\S]?)/g, \'\\\\1\'); }', 'strrev'=>'function(str) { return str.split(\'\').reverse().join(\'\'); }', 'strtolower'=>'function(str) { return str.toLowerCase(); }', 'strtotime'=>'function(str) { return Date.parse(str) / 1000; }', 'strtoupper'=>'function(str) { return str.toUpperCase(); }', 'trim'=>'function(str) { return str.replace(/^[ \\n\\r\\t\\0\\x0B]+/g, \'\').replace(/[ \\n\\r\\t\\0\\x0B]+$/g, \'\'); }', 'ucfirst'=>'function(str) { return str[0].toUpperCase() + str.substr(1); }', 'ucwords'=>'function(str) { return str.replace( /(?:^|\\s)[a-z]/g, function(m) { return m.toUpperCase() } ); }', 'urldecode'=>'function(str) { return decodeURIComponent(str); }', 'urlencode'=>'function(str) { return encodeURIComponent(str); }' ]; public static function get($funcName) { if (isset(self::$cache[$funcName])) return self::$cache[$funcName]; if (\preg_match('(^[a-z_0-9]+$)D', $funcName)) { $filepath = __DIR__ . '/Configurator/JavaScript/functions/' . $funcName . '.js'; if (\file_exists($filepath)) return \file_get_contents($filepath); } throw new InvalidArgumentException("Unknown function '" . $funcName . "'"); } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator; interface RendererGenerator { public function getRenderer(Rendering $rendering); } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP; abstract class AbstractOptimizer { protected $cnt; protected $i; protected $changed; protected $tokens; public function optimize($php) { $this->reset($php); $this->optimizeTokens(); if ($this->changed) $php = $this->serialize(); unset($this->tokens); return $php; } abstract protected function optimizeTokens(); protected function reset($php) { $this->tokens = \token_get_all('i = 0; $this->cnt = \count($this->tokens); $this->changed = \false; } protected function serialize() { unset($this->tokens[0]); $php = ''; foreach ($this->tokens as $token) $php .= (\is_string($token)) ? $token : $token[1]; return $php; } protected function skipToString($str) { while (++$this->i < $this->cnt && $this->tokens[$this->i] !== $str); } protected function skipWhitespace() { while (++$this->i < $this->cnt && $this->tokens[$this->i][0] === \T_WHITESPACE); } protected function unindentBlock($start, $end) { $this->i = $start; do { if ($this->tokens[$this->i][0] === \T_WHITESPACE || $this->tokens[$this->i][0] === \T_DOC_COMMENT) $this->tokens[$this->i][1] = \preg_replace("/^\t/m", '', $this->tokens[$this->i][1]); } while (++$this->i <= $end); } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP; class BranchOutputOptimizer { protected $cnt; protected $i; protected $tokens; public function optimize(array $tokens) { $this->tokens = $tokens; $this->i = 0; $this->cnt = \count($this->tokens); $php = ''; while (++$this->i < $this->cnt) if ($this->tokens[$this->i][0] === \T_IF) $php .= $this->serializeIfBlock($this->parseIfBlock()); else $php .= $this->serializeToken($this->tokens[$this->i]); unset($this->tokens); return $php; } protected function captureOutput() { $expressions = []; while ($this->skipOutputAssignment()) { do { $expressions[] = $this->captureOutputExpression(); } while ($this->tokens[$this->i++] === '.'); } return $expressions; } protected function captureOutputExpression() { $parens = 0; $php = ''; do { if ($this->tokens[$this->i] === ';') break; elseif ($this->tokens[$this->i] === '.' && !$parens) break; elseif ($this->tokens[$this->i] === '(') ++$parens; elseif ($this->tokens[$this->i] === ')') --$parens; $php .= $this->serializeToken($this->tokens[$this->i]); } while (++$this->i < $this->cnt); return $php; } protected function captureStructure() { $php = ''; do { $php .= $this->serializeToken($this->tokens[$this->i]); } while ($this->tokens[++$this->i] !== '{'); ++$this->i; return $php; } protected function isBranchToken() { return \in_array($this->tokens[$this->i][0], [\T_ELSE, \T_ELSEIF, \T_IF], \true); } protected function mergeIfBranches(array $branches) { $lastBranch = \end($branches); if ($lastBranch['structure'] === 'else') { $before = $this->optimizeBranchesHead($branches); $after = $this->optimizeBranchesTail($branches); } else $before = $after = []; $source = ''; foreach ($branches as $branch) $source .= $this->serializeBranch($branch); return [ 'before' => $before, 'source' => $source, 'after' => $after ]; } protected function mergeOutput(array $left, array $right) { if (empty($left)) return $right; if (empty($right)) return $left; $k = \count($left) - 1; if (\substr($left[$k], -1) === "'" && $right[0][0] === "'") { $right[0] = \substr($left[$k], 0, -1) . \substr($right[0], 1); unset($left[$k]); } return \array_merge($left, $right); } protected function optimizeBranchesHead(array &$branches) { $before = $this->optimizeBranchesOutput($branches, 'head'); foreach ($branches as &$branch) { if ($branch['body'] !== '' || !empty($branch['tail'])) continue; $branch['tail'] = \array_reverse($branch['head']); $branch['head'] = []; } unset($branch); return $before; } protected function optimizeBranchesOutput(array &$branches, $which) { $expressions = []; while (isset($branches[0][$which][0])) { $expr = $branches[0][$which][0]; foreach ($branches as $branch) if (!isset($branch[$which][0]) || $branch[$which][0] !== $expr) break 2; $expressions[] = $expr; foreach ($branches as &$branch) \array_shift($branch[$which]); unset($branch); } return $expressions; } protected function optimizeBranchesTail(array &$branches) { return $this->optimizeBranchesOutput($branches, 'tail'); } protected function parseBranch() { $structure = $this->captureStructure(); $head = $this->captureOutput(); $body = ''; $tail = []; $braces = 0; do { $tail = $this->mergeOutput($tail, \array_reverse($this->captureOutput())); if ($this->tokens[$this->i] === '}' && !$braces) break; $body .= $this->serializeOutput(\array_reverse($tail)); $tail = []; if ($this->tokens[$this->i][0] === \T_IF) { $child = $this->parseIfBlock(); if ($body === '') $head = $this->mergeOutput($head, $child['before']); else $body .= $this->serializeOutput($child['before']); $body .= $child['source']; $tail = $child['after']; } else { $body .= $this->serializeToken($this->tokens[$this->i]); if ($this->tokens[$this->i] === '{') ++$braces; elseif ($this->tokens[$this->i] === '}') --$braces; } } while (++$this->i < $this->cnt); return [ 'structure' => $structure, 'head' => $head, 'body' => $body, 'tail' => $tail ]; } protected function parseIfBlock() { $branches = []; do { $branches[] = $this->parseBranch(); } while (++$this->i < $this->cnt && $this->isBranchToken()); --$this->i; return $this->mergeIfBranches($branches); } protected function serializeBranch(array $branch) { if ($branch['structure'] === 'else' && $branch['body'] === '' && empty($branch['head']) && empty($branch['tail'])) return ''; return $branch['structure'] . '{' . $this->serializeOutput($branch['head']) . $branch['body'] . $this->serializeOutput(\array_reverse($branch['tail'])) . '}'; } protected function serializeIfBlock(array $block) { return $this->serializeOutput($block['before']) . $block['source'] . $this->serializeOutput(\array_reverse($block['after'])); } protected function serializeOutput(array $expressions) { if (empty($expressions)) return ''; return '$this->out.=' . \implode('.', $expressions) . ';'; } protected function serializeToken($token) { return (\is_array($token)) ? $token[1] : $token; } protected function skipOutputAssignment() { if ($this->tokens[$this->i ][0] !== \T_VARIABLE || $this->tokens[$this->i ][1] !== '$this' || $this->tokens[$this->i + 1][0] !== \T_OBJECT_OPERATOR || $this->tokens[$this->i + 2][0] !== \T_STRING || $this->tokens[$this->i + 2][1] !== 'out' || $this->tokens[$this->i + 3][0] !== \T_CONCAT_EQUAL) return \false; $this->i += 4; return \true; } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP; class Optimizer { public $branchOutputOptimizer; protected $cnt; protected $i; public $maxLoops = 10; protected $tokens; public function __construct() { $this->branchOutputOptimizer = new BranchOutputOptimizer; } public function optimize($php) { $this->tokens = \token_get_all('cnt = \count($this->tokens); $this->i = 0; foreach ($this->tokens as &$token) if (\is_array($token)) unset($token[2]); unset($token); $passes = [ 'optimizeOutConcatEqual', 'optimizeConcatenations', 'optimizeHtmlspecialchars' ]; $remainingLoops = $this->maxLoops; do { $continue = \false; foreach ($passes as $pass) { $this->$pass(); $cnt = \count($this->tokens); if ($this->cnt !== $cnt) { $this->tokens = \array_values($this->tokens); $this->cnt = $cnt; $continue = \true; } } } while ($continue && --$remainingLoops); $php = $this->branchOutputOptimizer->optimize($this->tokens); unset($this->tokens); return $php; } protected function isBetweenHtmlspecialcharCalls() { return ($this->tokens[$this->i + 1] === [\T_STRING, 'htmlspecialchars'] && $this->tokens[$this->i + 2] === '(' && $this->tokens[$this->i - 1] === ')' && $this->tokens[$this->i - 2][0] === \T_LNUMBER && $this->tokens[$this->i - 3] === ','); } protected function isHtmlspecialcharSafeVar() { return ($this->tokens[$this->i ] === [\T_VARIABLE, '$node'] && $this->tokens[$this->i + 1] === [\T_OBJECT_OPERATOR, '->'] && ($this->tokens[$this->i + 2] === [\T_STRING, 'localName'] || $this->tokens[$this->i + 2] === [\T_STRING, 'nodeName']) && $this->tokens[$this->i + 3] === ',' && $this->tokens[$this->i + 4][0] === \T_LNUMBER && $this->tokens[$this->i + 5] === ')'); } protected function isOutputAssignment() { return ($this->tokens[$this->i ] === [\T_VARIABLE, '$this'] && $this->tokens[$this->i + 1] === [\T_OBJECT_OPERATOR, '->'] && $this->tokens[$this->i + 2] === [\T_STRING, 'out'] && $this->tokens[$this->i + 3] === [\T_CONCAT_EQUAL, '.=']); } protected function isPrecededByOutputVar() { return ($this->tokens[$this->i - 1] === [\T_STRING, 'out'] && $this->tokens[$this->i - 2] === [\T_OBJECT_OPERATOR, '->'] && $this->tokens[$this->i - 3] === [\T_VARIABLE, '$this']); } protected function mergeConcatenatedHtmlSpecialChars() { if (!$this->isBetweenHtmlspecialcharCalls()) return \false; $escapeMode = $this->tokens[$this->i - 2][1]; $startIndex = $this->i - 3; $endIndex = $this->i + 2; $this->i = $endIndex; $parens = 0; while (++$this->i < $this->cnt) { if ($this->tokens[$this->i] === ',' && !$parens) break; if ($this->tokens[$this->i] === '(') ++$parens; elseif ($this->tokens[$this->i] === ')') --$parens; } if ($this->tokens[$this->i + 1] !== [\T_LNUMBER, $escapeMode]) return \false; $this->tokens[$startIndex] = '.'; $this->i = $startIndex; while (++$this->i <= $endIndex) unset($this->tokens[$this->i]); return \true; } protected function mergeConcatenatedStrings() { if ($this->tokens[$this->i - 1][0] !== \T_CONSTANT_ENCAPSED_STRING || $this->tokens[$this->i + 1][0] !== \T_CONSTANT_ENCAPSED_STRING || $this->tokens[$this->i - 1][1][0] !== $this->tokens[$this->i + 1][1][0]) return \false; $this->tokens[$this->i + 1][1] = \substr($this->tokens[$this->i - 1][1], 0, -1) . \substr($this->tokens[$this->i + 1][1], 1); unset($this->tokens[$this->i - 1]); unset($this->tokens[$this->i]); ++$this->i; return \true; } protected function optimizeOutConcatEqual() { $this->i = 3; while ($this->skipTo([\T_CONCAT_EQUAL, '.='])) { if (!$this->isPrecededByOutputVar()) continue; while ($this->skipPast(';')) { if (!$this->isOutputAssignment()) break; $this->tokens[$this->i - 1] = '.'; unset($this->tokens[$this->i++]); unset($this->tokens[$this->i++]); unset($this->tokens[$this->i++]); unset($this->tokens[$this->i++]); } } } protected function optimizeConcatenations() { $this->i = 1; while ($this->skipTo('.')) $this->mergeConcatenatedStrings() || $this->mergeConcatenatedHtmlSpecialChars(); } protected function optimizeHtmlspecialchars() { $this->i = 0; while ($this->skipPast([\T_STRING, 'htmlspecialchars'])) if ($this->tokens[$this->i] === '(') { ++$this->i; $this->replaceHtmlspecialcharsLiteral() || $this->removeHtmlspecialcharsSafeVar(); } } protected function removeHtmlspecialcharsSafeVar() { if (!$this->isHtmlspecialcharSafeVar()) return \false; unset($this->tokens[$this->i - 2]); unset($this->tokens[$this->i - 1]); unset($this->tokens[$this->i + 3]); unset($this->tokens[$this->i + 4]); unset($this->tokens[$this->i + 5]); $this->i += 6; return \true; } protected function replaceHtmlspecialcharsLiteral() { if ($this->tokens[$this->i ][0] !== \T_CONSTANT_ENCAPSED_STRING || $this->tokens[$this->i + 1] !== ',' || $this->tokens[$this->i + 2][0] !== \T_LNUMBER || $this->tokens[$this->i + 3] !== ')') return \false; $this->tokens[$this->i][1] = \var_export( \htmlspecialchars( \stripslashes(\substr($this->tokens[$this->i][1], 1, -1)), $this->tokens[$this->i + 2][1] ), \true ); unset($this->tokens[$this->i - 2]); unset($this->tokens[$this->i - 1]); unset($this->tokens[++$this->i]); unset($this->tokens[++$this->i]); unset($this->tokens[++$this->i]); return \true; } protected function skipPast($token) { return ($this->skipTo($token) && ++$this->i < $this->cnt); } protected function skipTo($token) { while (++$this->i < $this->cnt) if ($this->tokens[$this->i] === $token) return \true; return \false; } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP; use Closure; use RuntimeException; use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder; class Quick { public static function getSource(array $compiledTemplates) { $map = ['dynamic' => [], 'php' => [], 'static' => []]; $tagNames = []; $unsupported = []; unset($compiledTemplates['br']); unset($compiledTemplates['e']); unset($compiledTemplates['i']); unset($compiledTemplates['p']); unset($compiledTemplates['s']); foreach ($compiledTemplates as $tagName => $php) { $renderings = self::getRenderingStrategy($php); if (empty($renderings)) { $unsupported[] = $tagName; continue; } foreach ($renderings as $i => $_562c18b7) { list($strategy, $replacement) = $_562c18b7; $match = (($i) ? '/' : '') . $tagName; $map[$strategy][$match] = $replacement; } if (!isset($renderings[1])) $tagNames[] = $tagName; } $php = []; $php[] = ' /** {@inheritdoc} */'; $php[] = ' public $enableQuickRenderer=true;'; $php[] = ' /** {@inheritdoc} */'; $php[] = ' protected $static=' . self::export($map['static']) . ';'; $php[] = ' /** {@inheritdoc} */'; $php[] = ' protected $dynamic=' . self::export($map['dynamic']) . ';'; $quickSource = ''; if (!empty($map['php'])) $quickSource = SwitchStatement::generate('$id', $map['php']); $regexp = '(<(?:(?!/)('; $regexp .= ($tagNames) ? RegexpBuilder::fromList($tagNames) : '(?!)'; $regexp .= ')(?: [^>]*)?>.*?)[^ />]+)[^>]*?(/)?)>)s'; $php[] = ' /** {@inheritdoc} */'; $php[] = ' protected $quickRegexp=' . \var_export($regexp, \true) . ';'; if (!empty($unsupported)) { $regexp = '(<(?:[!?]|' . RegexpBuilder::fromList($unsupported) . '[ />]))'; $php[] = ' /** {@inheritdoc} */'; $php[] = ' protected $quickRenderingTest=' . \var_export($regexp, \true) . ';'; } $php[] = ' /** {@inheritdoc} */'; $php[] = ' protected function renderQuickTemplate($id, $xml)'; $php[] = ' {'; $php[] = ' $attributes=$this->matchAttributes($xml);'; $php[] = " \$html='';" . $quickSource; $php[] = ''; $php[] = ' return $html;'; $php[] = ' }'; return \implode("\n", $php); } protected static function export(array $arr) { $exportKeys = (\array_keys($arr) !== \range(0, \count($arr) - 1)); \ksort($arr); $entries = []; foreach ($arr as $k => $v) $entries[] = (($exportKeys) ? \var_export($k, \true) . '=>' : '') . ((\is_array($v)) ? self::export($v) : \var_export($v, \true)); return '[' . \implode(',', $entries) . ']'; } public static function getRenderingStrategy($php) { $phpRenderings = self::getQuickRendering($php); if (empty($phpRenderings)) return []; $renderings = self::getStringRenderings($php); foreach ($phpRenderings as $i => $phpRendering) if (!isset($renderings[$i]) || \strpos($phpRendering, '$this->attributes[]') !== \false) $renderings[$i] = ['php', $phpRendering]; return $renderings; } protected static function getQuickRendering($php) { if (\preg_match('(\\$this->at\\((?!\\$node\\);))', $php)) return []; $tokens = \token_get_all(' -1, 'branches' => [], 'head' => '', 'passthrough' => 0, 'statement' => '', 'tail' => '' ]; $braces = 0; $i = 0; do { if ($tokens[$i ][0] === \T_VARIABLE && $tokens[$i ][1] === '$this' && $tokens[$i + 1][0] === \T_OBJECT_OPERATOR && $tokens[$i + 2][0] === \T_STRING && $tokens[$i + 2][1] === 'at' && $tokens[$i + 3] === '(' && $tokens[$i + 4][0] === \T_VARIABLE && $tokens[$i + 4][1] === '$node' && $tokens[$i + 5] === ')' && $tokens[$i + 6] === ';') { if (++$branch['passthrough'] > 1) return []; $i += 6; continue; } $key = ($branch['passthrough']) ? 'tail' : 'head'; $branch[$key] .= (\is_array($tokens[$i])) ? $tokens[$i][1] : $tokens[$i]; if ($tokens[$i] === '{') { ++$braces; continue; } if ($tokens[$i] === '}') { --$braces; if ($branch['braces'] === $braces) { $branch[$key] = \substr($branch[$key], 0, -1); $branch =& $branch['parent']; $j = $i; while ($tokens[++$j][0] === \T_WHITESPACE); if ($tokens[$j][0] !== \T_ELSEIF && $tokens[$j][0] !== \T_ELSE) { $passthroughs = self::getBranchesPassthrough($branch['branches']); if ($passthroughs === [0]) { foreach ($branch['branches'] as $child) $branch['head'] .= $child['statement'] . '{' . $child['head'] . '}'; $branch['branches'] = []; continue; } if ($passthroughs === [1]) { ++$branch['passthrough']; continue; } return []; } } continue; } if ($branch['passthrough']) continue; if ($tokens[$i][0] === \T_IF || $tokens[$i][0] === \T_ELSEIF || $tokens[$i][0] === \T_ELSE) { $branch[$key] = \substr($branch[$key], 0, -\strlen($tokens[$i][1])); $branch['branches'][] = [ 'braces' => $braces, 'branches' => [], 'head' => '', 'parent' => &$branch, 'passthrough' => 0, 'statement' => '', 'tail' => '' ]; $branch =& $branch['branches'][\count($branch['branches']) - 1]; do { $branch['statement'] .= (\is_array($tokens[$i])) ? $tokens[$i][1] : $tokens[$i]; } while ($tokens[++$i] !== '{'); ++$braces; } } while (++$i < $cnt); list($head, $tail) = self::buildPHP($branch['branches']); $head = $branch['head'] . $head; $tail .= $branch['tail']; self::convertPHP($head, $tail, (bool) $branch['passthrough']); if (\preg_match('((?)', $head . $tail)) return []; return ($branch['passthrough']) ? [$head, $tail] : [$head]; } protected static function convertPHP(&$head, &$tail, $passthrough) { $saveAttributes = (bool) \preg_match('(\\$node->(?:get|has)Attribute)', $tail); \preg_match_all( "(\\\$node->getAttribute\\('([^']+)'\\))", \preg_replace_callback( '(if\\(\\$node->hasAttribute\\(([^\\)]+)[^}]+)', function ($m) { return \str_replace('$node->getAttribute(' . $m[1] . ')', '', $m[0]); }, $head . $tail ), $matches ); $attrNames = \array_unique($matches[1]); self::replacePHP($head); self::replacePHP($tail); if (!$passthrough && \strpos($head, '$node->textContent') !== \false) $head = '$textContent=$this->getQuickTextContent($xml);' . \str_replace('$node->textContent', '$textContent', $head); if (!empty($attrNames)) { \ksort($attrNames); $head = "\$attributes+=['" . \implode("'=>null,'", $attrNames) . "'=>null];" . $head; } if ($saveAttributes) { $head .= '$this->attributes[]=$attributes;'; $tail = '$attributes=array_pop($this->attributes);' . $tail; } } protected static function replacePHP(&$php) { $getAttribute = "\\\$node->getAttribute\\(('[^']+')\\)"; $string = "'(?:[^\\\\']|\\\\.)*+'"; $replacements = [ '$this->out' => '$html', '(htmlspecialchars\\(' . $getAttribute . ',' . \ENT_NOQUOTES . '\\))' => "str_replace('"','\"',\$attributes[\$1])", '(htmlspecialchars\\((' . $getAttribute . '(?:\\.' . $getAttribute . ')*),' . \ENT_COMPAT . '\\))' => function ($m) use ($getAttribute) { return \preg_replace('(' . $getAttribute . ')', '$attributes[$1]', $m[1]); }, '(htmlspecialchars\\(strtr\\(' . $getAttribute . ",('[^\"&\\\\';<>aglmopqtu]+'),('[^\"&\\\\'<>]+')\\)," . \ENT_COMPAT . '\\))' => 'strtr($attributes[$1],$2,$3)', '(' . $getAttribute . '(!?=+)' . $getAttribute . ')' => '$attributes[$1]$2$attributes[$3]', '(' . $getAttribute . '===(' . $string . '))s' => function ($m) { return '$attributes[' . $m[1] . ']===' . \htmlspecialchars($m[2], \ENT_COMPAT); }, '((' . $string . ')===' . $getAttribute . ')s' => function ($m) { return \htmlspecialchars($m[1], \ENT_COMPAT) . '===$attributes[' . $m[2] . ']'; }, '(strpos\\(' . $getAttribute . ',(' . $string . ')\\)([!=]==(?:0|false)))s' => function ($m) { return 'strpos($attributes[' . $m[1] . "]," . \htmlspecialchars($m[2], \ENT_COMPAT) . ')' . $m[3]; }, '(strpos\\((' . $string . '),' . $getAttribute . '\\)([!=]==(?:0|false)))s' => function ($m) { return 'strpos(' . \htmlspecialchars($m[1], \ENT_COMPAT) . ',$attributes[' . $m[2] . '])' . $m[3]; }, '(' . $getAttribute . '(?=(?:==|[-+*])\\d+))' => '$attributes[$1]', '(\\b(\\d+(?:==|[-+*]))' . $getAttribute . ')' => '$1$attributes[$2]', '(empty\\(' . $getAttribute . '\\))' => 'empty($attributes[$1])', "(\\\$node->hasAttribute\\(('[^']+')\\))" => 'isset($attributes[$1])', 'if($node->attributes->length)' => 'if($this->hasNonNullValues($attributes))', '(' . $getAttribute . ')' => 'htmlspecialchars_decode($attributes[$1])' ]; foreach ($replacements as $match => $replace) if ($replace instanceof Closure) $php = \preg_replace_callback($match, $replace, $php); elseif ($match[0] === '(') $php = \preg_replace($match, $replace, $php); else $php = \str_replace($match, $replace, $php); } protected static function buildPHP(array $branches) { $return = ['', '']; foreach ($branches as $branch) { $return[0] .= $branch['statement'] . '{' . $branch['head']; $return[1] .= $branch['statement'] . '{'; if ($branch['branches']) { list($head, $tail) = self::buildPHP($branch['branches']); $return[0] .= $head; $return[1] .= $tail; } $return[0] .= '}'; $return[1] .= $branch['tail'] . '}'; } return $return; } protected static function getBranchesPassthrough(array $branches) { $values = []; foreach ($branches as $branch) $values[] = $branch['passthrough']; if ($branch['statement'] !== 'else') $values[] = 0; return \array_unique($values); } protected static function getDynamicRendering($php) { $rendering = ''; $literal = "(?'((?>[^'\\\\]+|\\\\['\\\\])*)')"; $attribute = "(?htmlspecialchars\\(\\\$node->getAttribute\\('([^']+)'\\),2\\))"; $value = "(?$literal|$attribute)"; $output = "(?\\\$this->out\\.=$value(?:\\.(?&value))*;)"; $copyOfAttribute = "(?if\\(\\\$node->hasAttribute\\('([^']+)'\\)\\)\\{\\\$this->out\\.=' \\g-1=\"'\\.htmlspecialchars\\(\\\$node->getAttribute\\('\\g-1'\\),2\\)\\.'\"';\\})"; $regexp = '(^(' . $output . '|' . $copyOfAttribute . ')*$)'; if (!\preg_match($regexp, $php, $m)) return \false; $copiedAttributes = []; $usedAttributes = []; $regexp = '(' . $output . '|' . $copyOfAttribute . ')A'; $offset = 0; while (\preg_match($regexp, $php, $m, 0, $offset)) if ($m['output']) { $offset += 12; while (\preg_match('(' . $value . ')A', $php, $m, 0, $offset)) { if ($m['literal']) { $str = \stripslashes(\substr($m[0], 1, -1)); $rendering .= \preg_replace('([\\\\$](?=\\d))', '\\\\$0', $str); } else { $attrName = \end($m); if (!isset($usedAttributes[$attrName])) $usedAttributes[$attrName] = \uniqid($attrName, \true); $rendering .= $usedAttributes[$attrName]; } $offset += 1 + \strlen($m[0]); } } else { $attrName = \end($m); if (!isset($copiedAttributes[$attrName])) $copiedAttributes[$attrName] = \uniqid($attrName, \true); $rendering .= $copiedAttributes[$attrName]; $offset += \strlen($m[0]); } $attrNames = \array_keys($copiedAttributes + $usedAttributes); \sort($attrNames); $remainingAttributes = \array_combine($attrNames, $attrNames); $regexp = '(^[^ ]+'; $index = 0; foreach ($attrNames as $attrName) { $regexp .= '(?> (?!' . RegexpBuilder::fromList($remainingAttributes) . '=)[^=]+="[^"]*")*'; unset($remainingAttributes[$attrName]); $regexp .= '('; if (isset($copiedAttributes[$attrName])) self::replacePlaceholder($rendering, $copiedAttributes[$attrName], ++$index); else $regexp .= '?>'; $regexp .= ' ' . $attrName . '="'; if (isset($usedAttributes[$attrName])) { $regexp .= '('; self::replacePlaceholder($rendering, $usedAttributes[$attrName], ++$index); } $regexp .= '[^"]*'; if (isset($usedAttributes[$attrName])) $regexp .= ')'; $regexp .= '")?'; } $regexp .= '.*)s'; return [$regexp, $rendering]; } protected static function getStaticRendering($php) { if ($php === '') return ''; $regexp = "(^\\\$this->out\.='((?>[^'\\\\]|\\\\['\\\\])*+)';\$)"; if (\preg_match($regexp, $php, $m)) return \stripslashes($m[1]); return \false; } protected static function getStringRenderings($php) { $chunks = \explode('$this->at($node);', $php); if (\count($chunks) > 2) return []; $renderings = []; foreach ($chunks as $k => $chunk) { $rendering = self::getStaticRendering($chunk); if ($rendering !== \false) $renderings[$k] = ['static', $rendering]; elseif ($k === 0) { $rendering = self::getDynamicRendering($chunk); if ($rendering !== \false) $renderings[$k] = ['dynamic', $rendering]; } } return $renderings; } protected static function replacePlaceholder(&$str, $uniqid, $index) { $str = \preg_replace_callback( '(' . \preg_quote($uniqid) . '(.))', function ($m) use ($index) { if (\is_numeric($m[1])) return '${' . $index . '}' . $m[1]; else return '$' . $index . $m[1]; }, $str ); } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP; use DOMElement; use DOMXPath; use RuntimeException; use s9e\TextFormatter\Configurator\Helpers\AVTHelper; use s9e\TextFormatter\Configurator\Helpers\TemplateParser; class Serializer { public $convertor; protected $isVoid; public $useMultibyteStringFunctions = \false; protected $xpath; public function __construct() { $this->convertor = new XPathConvertor; } public function convertCondition($expr) { $this->convertor->useMultibyteStringFunctions = $this->useMultibyteStringFunctions; return $this->convertor->convertCondition($expr); } public function convertXPath($expr) { $this->convertor->useMultibyteStringFunctions = $this->useMultibyteStringFunctions; return $this->convertor->convertXPath($expr); } public function serialize(DOMElement $ir) { $this->xpath = new DOMXPath($ir->ownerDocument); $this->isVoid = []; foreach ($this->xpath->query('//element') as $element) $this->isVoid[$element->getAttribute('id')] = $element->getAttribute('void'); return $this->serializeChildren($ir); } protected function convertAttributeValueTemplate($attrValue) { $phpExpressions = []; foreach (AVTHelper::parse($attrValue) as $token) if ($token[0] === 'literal') $phpExpressions[] = \var_export($token[1], \true); else $phpExpressions[] = $this->convertXPath($token[1]); return \implode('.', $phpExpressions); } protected function escapeLiteral($text, $context) { if ($context === 'raw') return $text; $escapeMode = ($context === 'attribute') ? \ENT_COMPAT : \ENT_NOQUOTES; return \htmlspecialchars($text, $escapeMode); } protected function escapePHPOutput($php, $context) { if ($context === 'raw') return $php; $escapeMode = ($context === 'attribute') ? \ENT_COMPAT : \ENT_NOQUOTES; return 'htmlspecialchars(' . $php . ',' . $escapeMode . ')'; } protected function hasMultipleCases(DOMElement $switch) { return $this->xpath->evaluate('count(case[@test]) > 1', $switch); } protected function serializeApplyTemplates(DOMElement $applyTemplates) { $php = '$this->at($node'; if ($applyTemplates->hasAttribute('select')) $php .= ',' . \var_export($applyTemplates->getAttribute('select'), \true); $php .= ');'; return $php; } protected function serializeAttribute(DOMElement $attribute) { $attrName = $attribute->getAttribute('name'); $phpAttrName = $this->convertAttributeValueTemplate($attrName); $phpAttrName = 'htmlspecialchars(' . $phpAttrName . ',' . \ENT_QUOTES . ')'; return "\$this->out.=' '." . $phpAttrName . ".'=\"';" . $this->serializeChildren($attribute) . "\$this->out.='\"';"; } protected function serializeChildren(DOMElement $ir) { $php = ''; foreach ($ir->childNodes as $node) if ($node instanceof DOMElement) { $methodName = 'serialize' . \ucfirst($node->localName); $php .= $this->$methodName($node); } return $php; } protected function serializeCloseTag(DOMElement $closeTag) { $php = "\$this->out.='>';"; $id = $closeTag->getAttribute('id'); if ($closeTag->hasAttribute('set')) $php .= '$t' . $id . '=1;'; if ($closeTag->hasAttribute('check')) $php = 'if(!isset($t' . $id . ')){' . $php . '}'; if ($this->isVoid[$id] === 'maybe') $php .= 'if(!$v' . $id . '){'; return $php; } protected function serializeComment(DOMElement $comment) { return "\$this->out.='';"; } protected function serializeCopyOfAttributes(DOMElement $copyOfAttributes) { return 'foreach($node->attributes as $attribute){' . "\$this->out.=' ';\$this->out.=\$attribute->name;\$this->out.='=\"';\$this->out.=htmlspecialchars(\$attribute->value," . \ENT_COMPAT . ");\$this->out.='\"';" . '}'; } protected function serializeElement(DOMElement $element) { $php = ''; $elName = $element->getAttribute('name'); $id = $element->getAttribute('id'); $isVoid = $element->getAttribute('void'); $isDynamic = (bool) (\strpos($elName, '{') !== \false); $phpElName = $this->convertAttributeValueTemplate($elName); $phpElName = 'htmlspecialchars(' . $phpElName . ',' . \ENT_QUOTES . ')'; if ($isDynamic) { $varName = '$e' . $id; $php .= $varName . '=' . $phpElName . ';'; $phpElName = $varName; } if ($isVoid === 'maybe') $php .= '$v' . $id . '=preg_match(' . \var_export(TemplateParser::$voidRegexp, \true) . ',' . $phpElName . ');'; $php .= "\$this->out.='<'." . $phpElName . ';'; $php .= $this->serializeChildren($element); if ($isVoid !== 'yes') $php .= "\$this->out.='';"; if ($isVoid === 'maybe') $php .= '}'; return $php; } protected function serializeHash(DOMElement $switch) { $statements = []; foreach ($this->xpath->query('case[@branch-values]', $switch) as $case) foreach (\unserialize($case->getAttribute('branch-values')) as $value) $statements[$value] = $this->serializeChildren($case); if (!isset($case)) throw new RuntimeException; $defaultCase = $this->xpath->query('case[not(@branch-values)]', $switch)->item(0); $defaultCode = ($defaultCase instanceof DOMElement) ? $this->serializeChildren($defaultCase) : ''; $expr = $this->convertXPath($switch->getAttribute('branch-key')); return SwitchStatement::generate($expr, $statements, $defaultCode); } protected function serializeOutput(DOMElement $output) { $context = $output->getAttribute('escape'); $php = '$this->out.='; if ($output->getAttribute('type') === 'xpath') $php .= $this->escapePHPOutput($this->convertXPath($output->textContent), $context); else $php .= \var_export($this->escapeLiteral($output->textContent, $context), \true); $php .= ';'; return $php; } protected function serializeSwitch(DOMElement $switch) { if ($switch->hasAttribute('branch-key') && $this->hasMultipleCases($switch)) return $this->serializeHash($switch); $php = ''; $if = 'if'; foreach ($this->xpath->query('case', $switch) as $case) { if ($case->hasAttribute('test')) $php .= $if . '(' . $this->convertCondition($case->getAttribute('test')) . ')'; else $php .= 'else'; $php .= '{' . $this->serializeChildren($case) . '}'; $if = 'elseif'; } return $php; } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP; class SwitchStatement { protected $branchesCode; protected $defaultCode; public function __construct(array $branchesCode, $defaultCode = '') { \ksort($branchesCode); $this->branchesCode = $branchesCode; $this->defaultCode = $defaultCode; } public static function generate($expr, array $branchesCode, $defaultCode = '') { $switch = new static($branchesCode, $defaultCode); return $switch->getSource($expr); } protected function getSource($expr) { $php = 'switch(' . $expr . '){'; foreach ($this->getValuesPerCodeBranch() as $branchCode => $values) { foreach ($values as $value) $php .= 'case' . \var_export((string) $value, \true) . ':'; $php .= $branchCode . 'break;'; } if ($this->defaultCode > '') $php .= 'default:' . $this->defaultCode; $php = \preg_replace('(break;$)', '', $php) . '}'; return $php; } protected function getValuesPerCodeBranch() { $values = []; foreach ($this->branchesCode as $value => $branchCode) $values[$branchCode][] = $value; return $values; } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP; use LogicException; use RuntimeException; class XPathConvertor { public $pcreVersion; protected $regexp; public $useMultibyteStringFunctions = \false; public function __construct() { $this->pcreVersion = \PCRE_VERSION; } public function convertCondition($expr) { $expr = \trim($expr); if (\preg_match('#^@([-\\w]+)$#', $expr, $m)) return '$node->hasAttribute(' . \var_export($m[1], \true) . ')'; if ($expr === '@*') return '$node->attributes->length'; if (\preg_match('#^not\\(@([-\\w]+)\\)$#', $expr, $m)) return '!$node->hasAttribute(' . \var_export($m[1], \true) . ')'; if (\preg_match('#^\\$(\\w+)$#', $expr, $m)) return '$this->params[' . \var_export($m[1], \true) . "]!==''"; if (\preg_match('#^not\\(\\$(\\w+)\\)$#', $expr, $m)) return '$this->params[' . \var_export($m[1], \true) . "]===''"; if (\preg_match('#^([$@][-\\w]+)\\s*([<>])\\s*(\\d+)$#', $expr, $m)) return $this->convertXPath($m[1]) . $m[2] . $m[3]; if (!\preg_match('#[=<>]|\\bor\\b|\\band\\b|^[-\\w]+\\s*\\(#', $expr)) $expr = 'boolean(' . $expr . ')'; return $this->convertXPath($expr); } public function convertXPath($expr) { $expr = \trim($expr); $this->generateXPathRegexp(); if (\preg_match($this->regexp, $expr, $m)) { $methodName = \null; foreach ($m as $k => $v) { if (\is_numeric($k) || $v === '' || $v === \null || !\method_exists($this, $k)) continue; $methodName = $k; break; } if (isset($methodName)) { $args = [$m[$methodName]]; $i = 0; while (isset($m[$methodName . $i])) { $args[$i] = $m[$methodName . $i]; ++$i; } return \call_user_func_array([$this, $methodName], $args); } } if (!\preg_match('#[=<>]|\\bor\\b|\\band\\b|^[-\\w]+\\s*\\(#', $expr)) $expr = 'string(' . $expr . ')'; return '$this->xpath->evaluate(' . $this->exportXPath($expr) . ',$node)'; } protected function attr($attrName) { return '$node->getAttribute(' . \var_export($attrName, \true) . ')'; } protected function dot() { return '$node->textContent'; } protected function param($paramName) { return '$this->params[' . \var_export($paramName, \true) . ']'; } protected function string($string) { return \var_export(\substr($string, 1, -1), \true); } protected function lname() { return '$node->localName'; } protected function name() { return '$node->nodeName'; } protected function number($sign, $number) { $number = \ltrim($number, '0') ?: 0; if (!$number) $sign = ''; return "'" . $sign . $number . "'"; } protected function strlen($expr) { if ($expr === '') $expr = '.'; $php = $this->convertXPath($expr); return ($this->useMultibyteStringFunctions) ? 'mb_strlen(' . $php . ",'utf-8')" : "strlen(preg_replace('(.)us','.'," . $php . '))'; } protected function contains($haystack, $needle) { return '(strpos(' . $this->convertXPath($haystack) . ',' . $this->convertXPath($needle) . ')!==false)'; } protected function startswith($string, $substring) { return '(strpos(' . $this->convertXPath($string) . ',' . $this->convertXPath($substring) . ')===0)'; } protected function not($expr) { return '!(' . $this->convertCondition($expr) . ')'; } protected function notcontains($haystack, $needle) { return '(strpos(' . $this->convertXPath($haystack) . ',' . $this->convertXPath($needle) . ')===false)'; } protected function substr($exprString, $exprPos, $exprLen = \null) { if (!$this->useMultibyteStringFunctions) { $expr = 'substring(' . $exprString . ',' . $exprPos; if (isset($exprLen)) $expr .= ',' . $exprLen; $expr .= ')'; return '$this->xpath->evaluate(' . $this->exportXPath($expr) . ',$node)'; } $php = 'mb_substr(' . $this->convertXPath($exprString) . ','; if (\is_numeric($exprPos)) $php .= \max(0, $exprPos - 1); else $php .= 'max(0,' . $this->convertXPath($exprPos) . '-1)'; $php .= ','; if (isset($exprLen)) if (\is_numeric($exprLen)) if (\is_numeric($exprPos) && $exprPos < 1) $php .= \max(0, $exprPos + $exprLen - 1); else $php .= \max(0, $exprLen); else $php .= 'max(0,' . $this->convertXPath($exprLen) . ')'; else $php .= 'null'; $php .= ",'utf-8')"; return $php; } protected function substringafter($expr, $str) { return 'substr(strstr(' . $this->convertXPath($expr) . ',' . $this->convertXPath($str) . '),' . (\strlen($str) - 2) . ')'; } protected function substringbefore($expr1, $expr2) { return 'strstr(' . $this->convertXPath($expr1) . ',' . $this->convertXPath($expr2) . ',true)'; } protected function cmp($expr1, $operator, $expr2) { $operands = []; $operators = [ '=' => '===', '!=' => '!==', '>' => '>', '>=' => '>=', '<' => '<', '<=' => '<=' ]; foreach ([$expr1, $expr2] as $expr) if (\is_numeric($expr)) { $operators['='] = '=='; $operators['!='] = '!='; $operands[] = \preg_replace('(^0(.+))', '$1', $expr); } else $operands[] = $this->convertXPath($expr); return \implode($operators[$operator], $operands); } protected function bool($expr1, $operator, $expr2) { $operators = [ 'and' => '&&', 'or' => '||' ]; return $this->convertCondition($expr1) . $operators[$operator] . $this->convertCondition($expr2); } protected function parens($expr) { return '(' . $this->convertXPath($expr) . ')'; } protected function translate($str, $from, $to) { \preg_match_all('(.)su', \substr($from, 1, -1), $matches); $from = $matches[0]; \preg_match_all('(.)su', \substr($to, 1, -1), $matches); $to = $matches[0]; $from = \array_unique($from); $to = \array_intersect_key($to, $from); $to += \array_fill_keys(\array_keys(\array_diff_key($from, $to)), ''); $php = 'strtr(' . $this->convertXPath($str) . ','; if ([1] === \array_unique(\array_map('strlen', $from)) && [1] === \array_unique(\array_map('strlen', $to))) $php .= \var_export(\implode('', $from), \true) . ',' . \var_export(\implode('', $to), \true); else { $elements = []; foreach ($from as $k => $str) $elements[] = \var_export($str, \true) . '=>' . \var_export($to[$k], \true); $php .= '[' . \implode(',', $elements) . ']'; } $php .= ')'; return $php; } protected function math($expr1, $operator, $expr2) { if (!\is_numeric($expr1)) $expr1 = $this->convertXPath($expr1); if (!\is_numeric($expr2)) $expr2 = $this->convertXPath($expr2); if ($operator === 'div') $operator = '/'; return $expr1 . $operator . $expr2; } protected function exportXPath($expr) { $phpTokens = []; foreach ($this->tokenizeXPathForExport($expr) as $_f6b3b659) { list($type, $content) = $_f6b3b659; $methodName = 'exportXPath' . \ucfirst($type); $phpTokens[] = $this->$methodName($content); } return \implode('.', $phpTokens); } protected function exportXPathCurrent() { return '$node->getNodePath()'; } protected function exportXPathFragment($fragment) { return \var_export($fragment, \true); } protected function exportXPathParam($param) { $paramName = \ltrim($param, '$'); return '$this->getParamAsXPath(' . \var_export($paramName, \true) . ')'; } protected function generateXPathRegexp() { if (isset($this->regexp)) return; $patterns = [ 'attr' => ['@', '(?[-\\w]+)'], 'dot' => '\\.', 'name' => 'name\\(\\)', 'lname' => 'local-name\\(\\)', 'param' => ['\\$', '(?\\w+)'], 'string' => '"[^"]*"|\'[^\']*\'', 'number' => ['(?-?)', '(?\\d++)'], 'strlen' => ['string-length', '\\(', '(?(?&value)?)', '\\)'], 'contains' => [ 'contains', '\\(', '(?(?&value))', ',', '(?(?&value))', '\\)' ], 'translate' => [ 'translate', '\\(', '(?(?&value))', ',', '(?(?&string))', ',', '(?(?&string))', '\\)' ], 'substr' => [ 'substring', '\\(', '(?(?&value))', ',', '(?(?&value))', '(?:, (?(?&value)))?', '\\)' ], 'substringafter' => [ 'substring-after', '\\(', '(?(?&value))', ',', '(?(?&string))', '\\)' ], 'substringbefore' => [ 'substring-before', '\\(', '(?(?&value))', ',', '(?(?&value))', '\\)' ], 'startswith' => [ 'starts-with', '\\(', '(?(?&value))', ',', '(?(?&value))', '\\)' ], 'math' => [ '(?(?&attr)|(?&number)|(?¶m))', '(?[-+*]|div)', '(?(?&math)|(?&math0))' ], 'notcontains' => [ 'not', '\\(', 'contains', '\\(', '(?(?&value))', ',', '(?(?&value))', '\\)', '\\)' ] ]; $exprs = []; if (\version_compare($this->pcreVersion, '8.13', '>=')) { $exprs[] = '(?(?(?&value)) (?!?=) (?(?&value)))'; $exprs[] = '(?\\( (?(?&bool)|(?&cmp)|(?&math)) \\))'; $exprs[] = '(?(?(?&cmp)|(?¬)|(?&value)|(?&parens)) (?and|or) (?(?&bool)|(?&cmp)|(?¬)|(?&value)|(?&parens)))'; $exprs[] = '(?not \\( (?(?&bool)|(?&value)) \\))'; $patterns['math'][0] = \str_replace('))', ')|(?&parens))', $patterns['math'][0]); $patterns['math'][1] = \str_replace('))', ')|(?&parens))', $patterns['math'][1]); } $valueExprs = []; foreach ($patterns as $name => $pattern) { if (\is_array($pattern)) $pattern = \implode(' ', $pattern); if (\strpos($pattern, '?&') === \false || \version_compare($this->pcreVersion, '8.13', '>=')) $valueExprs[] = '(?<' . $name . '>' . $pattern . ')'; } \array_unshift($exprs, '(?' . \implode('|', $valueExprs) . ')'); $regexp = '#^(?:' . \implode('|', $exprs) . ')$#S'; $regexp = \str_replace(' ', '\\s*', $regexp); $this->regexp = $regexp; } protected function matchXPathForExport($expr) { $tokenExprs = [ '(?\\bcurrent\\(\\))', '(?\\$\\w+)', '(?"[^"]*"|\'[^\']*\'|.)' ]; \preg_match_all('(' . \implode('|', $tokenExprs) . ')s', $expr, $matches, \PREG_SET_ORDER); $i = \count($matches); while (--$i > 0) if (isset($matches[$i]['fragment'], $matches[$i - 1]['fragment'])) { $matches[$i - 1]['fragment'] .= $matches[$i]['fragment']; unset($matches[$i]); } return \array_values($matches); } protected function tokenizeXPathForExport($expr) { $tokens = []; foreach ($this->matchXPathForExport($expr) as $match) foreach (\array_reverse($match) as $k => $v) if (!\is_numeric($k)) { $tokens[] = [$k, $v]; break; } return $tokens; } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\RulesGenerators\Interfaces; use s9e\TextFormatter\Configurator\Helpers\TemplateInspector; interface BooleanRulesGenerator { public function generateBooleanRules(TemplateInspector $src); } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\RulesGenerators\Interfaces; use s9e\TextFormatter\Configurator\Helpers\TemplateInspector; interface TargetedRulesGenerator { public function generateTargetedRules(TemplateInspector $src, TemplateInspector $trg); } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator; use DOMElement; use s9e\TextFormatter\Configurator\Items\Tag; abstract class TemplateCheck { const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform'; abstract public function check(DOMElement $template, Tag $tag); } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\TemplateNormalizations; use DOMAttr; use DOMComment; use DOMElement; use DOMNode; use DOMXPath; abstract class AbstractNormalization { const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform'; public $onlyOnce = \false; protected $ownerDocument; protected $queries = []; protected $xpath; public function normalize(DOMElement $template) { $this->ownerDocument = $template->ownerDocument; $this->xpath = new DOMXPath($this->ownerDocument); foreach ($this->getNodes() as $node) $this->normalizeNode($node); $this->reset(); } protected function createElement($nodeName, $textContent = '') { $methodName = 'createElement'; $args = [$nodeName]; if ($textContent !== '') $args[] = \htmlspecialchars($textContent, \ENT_NOQUOTES, 'UTF-8'); $prefix = \strstr($nodeName, ':', \true); if ($prefix > '') { $methodName .= 'NS'; \array_unshift($args, $this->ownerDocument->lookupNamespaceURI($prefix)); } return \call_user_func_array([$this->ownerDocument, $methodName], $args); } protected function createText($content) { return (\trim($content) === '') ? $this->createElement('xsl:text', $content) : $this->ownerDocument->createTextNode($content); } protected function createTextNode($content) { return $this->ownerDocument->createTextNode($content); } protected function getNodes() { $query = \implode(' | ', $this->queries); return ($query === '') ? [] : $this->xpath($query); } protected function isXsl(DOMNode $node, $localName = \null) { return ($node->namespaceURI === self::XMLNS_XSL && (!isset($localName) || $localName === $node->localName)); } protected function lowercase($str) { return \strtr($str, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); } protected function normalizeAttribute(DOMAttr $attribute) { } protected function normalizeElement(DOMElement $element) { } protected function normalizeNode(DOMNode $node) { if (!$node->parentNode) return; if ($node instanceof DOMElement) $this->normalizeElement($node); elseif ($node instanceof DOMAttr) $this->normalizeAttribute($node); } protected function reset() { $this->ownerDocument = \null; $this->xpath = \null; } protected function xpath($query, DOMNode $node = \null) { $query = \str_replace('$XSL', '"' . self::XMLNS_XSL . '"', $query); return \iterator_to_array($this->xpath->query($query, $node)); } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Traits; trait CollectionProxy { public function __call($methodName, $args) { return \call_user_func_array([$this->collection, $methodName], $args); } public function offsetExists($offset) { return isset($this->collection[$offset]); } public function offsetGet($offset) { return $this->collection[$offset]; } public function offsetSet($offset, $value) { $this->collection[$offset] = $value; } public function offsetUnset($offset) { unset($this->collection[$offset]); } public function count() { return \count($this->collection); } public function current() { return $this->collection->current(); } public function key() { return $this->collection->key(); } public function next() { return $this->collection->next(); } public function rewind() { $this->collection->rewind(); } public function valid() { return $this->collection->valid(); } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Traits; use InvalidArgumentException; use RuntimeException; use Traversable; use s9e\TextFormatter\Configurator\Collections\Collection; use s9e\TextFormatter\Configurator\Collections\NormalizedCollection; trait Configurable { public function __get($propName) { $methodName = 'get' . \ucfirst($propName); if (\method_exists($this, $methodName)) return $this->$methodName(); if (!\property_exists($this, $propName)) throw new RuntimeException("Property '" . $propName . "' does not exist"); return $this->$propName; } public function __set($propName, $propValue) { $methodName = 'set' . \ucfirst($propName); if (\method_exists($this, $methodName)) { $this->$methodName($propValue); return; } if (!isset($this->$propName)) { $this->$propName = $propValue; return; } if ($this->$propName instanceof NormalizedCollection) { if (!\is_array($propValue) && !($propValue instanceof Traversable)) throw new InvalidArgumentException("Property '" . $propName . "' expects an array or a traversable object to be passed"); $this->$propName->clear(); foreach ($propValue as $k => $v) $this->$propName->set($k, $v); return; } if (\is_object($this->$propName)) { if (!($propValue instanceof $this->$propName)) throw new InvalidArgumentException("Cannot replace property '" . $propName . "' of class '" . \get_class($this->$propName) . "' with instance of '" . \get_class($propValue) . "'"); } else { $oldType = \gettype($this->$propName); $newType = \gettype($propValue); if ($oldType === 'boolean') if ($propValue === 'false') { $newType = 'boolean'; $propValue = \false; } elseif ($propValue === 'true') { $newType = 'boolean'; $propValue = \true; } if ($oldType !== $newType) { $tmp = $propValue; \settype($tmp, $oldType); \settype($tmp, $newType); if ($tmp !== $propValue) throw new InvalidArgumentException("Cannot replace property '" . $propName . "' of type " . $oldType . ' with value of type ' . $newType); \settype($propValue, $oldType); } } $this->$propName = $propValue; } public function __isset($propName) { $methodName = 'isset' . \ucfirst($propName); if (\method_exists($this, $methodName)) return $this->$methodName(); return isset($this->$propName); } public function __unset($propName) { $methodName = 'unset' . \ucfirst($propName); if (\method_exists($this, $methodName)) { $this->$methodName(); return; } if (!isset($this->$propName)) return; if ($this->$propName instanceof Collection) { $this->$propName->clear(); return; } throw new RuntimeException("Property '" . $propName . "' cannot be unset"); } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Traits; trait TemplateSafeness { protected $markedSafe = []; protected function isSafe($context) { return !empty($this->markedSafe[$context]); } public function isSafeAsURL() { return $this->isSafe('AsURL'); } public function isSafeInCSS() { return $this->isSafe('InCSS'); } public function isSafeInJS() { return $this->isSafe('InJS'); } public function markAsSafeAsURL() { $this->markedSafe['AsURL'] = \true; return $this; } public function markAsSafeInCSS() { $this->markedSafe['InCSS'] = \true; return $this; } public function markAsSafeInJS() { $this->markedSafe['InJS'] = \true; return $this; } public function resetSafeness() { $this->markedSafe = []; return $this; } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Validators; use InvalidArgumentException; abstract class AttributeName { public static function isValid($name) { return (bool) \preg_match('#^(?!xmlns$)[a-z_][-a-z_0-9]*$#Di', $name); } public static function normalize($name) { if (!static::isValid($name)) throw new InvalidArgumentException("Invalid attribute name '" . $name . "'"); return \strtolower($name); } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Validators; use InvalidArgumentException; abstract class TagName { public static function isValid($name) { return (bool) \preg_match('#^(?:(?!xmlns|xsl|s9e)[a-z_][a-z_0-9]*:)?[a-z_][-a-z_0-9]*$#Di', $name); } public static function normalize($name) { if (!static::isValid($name)) throw new InvalidArgumentException("Invalid tag name '" . $name . "'"); if (\strpos($name, ':') === \false) $name = \strtoupper($name); return $name; } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Collections; use Countable; use Iterator; use s9e\TextFormatter\Configurator\ConfigProvider; use s9e\TextFormatter\Configurator\Helpers\ConfigHelper; class Collection implements ConfigProvider, Countable, Iterator { protected $items = []; public function clear() { $this->items = []; } public function asConfig() { return ConfigHelper::toArray($this->items, \true); } public function count() { return \count($this->items); } public function current() { return \current($this->items); } public function key() { return \key($this->items); } public function next() { return \next($this->items); } public function rewind() { \reset($this->items); } public function valid() { return (\key($this->items) !== \null); } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Helpers\TemplateParser; use DOMDocument; use DOMElement; use DOMNode; use s9e\TextFormatter\Configurator\Helpers\XPathHelper; class Normalizer extends IRProcessor { protected $optimizer; public $voidRegexp = '/^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/Di'; public function __construct(Optimizer $optimizer) { $this->optimizer = $optimizer; } public function normalize(DOMDocument $ir) { $this->createXPath($ir); $this->addDefaultCase($ir); $this->addElementIds($ir); $this->addCloseTagElements($ir); $this->markVoidElements($ir); $this->optimizer->optimize($ir); $this->markConditionalCloseTagElements($ir); $this->setOutputContext($ir); $this->markBranchTables($ir); } protected function addCloseTagElements(DOMDocument $ir) { $exprs = [ '//applyTemplates[not(ancestor::attribute)]', '//comment', '//element', '//output[not(ancestor::attribute)]' ]; foreach ($this->query(\implode('|', $exprs)) as $node) { $parentElementId = $this->getParentElementId($node); if (isset($parentElementId)) $node->parentNode ->insertBefore($ir->createElement('closeTag'), $node) ->setAttribute('id', $parentElementId); if ($node->nodeName === 'element') { $id = $node->getAttribute('id'); $this->appendElement($node, 'closeTag')->setAttribute('id', $id); } } } protected function addDefaultCase(DOMDocument $ir) { foreach ($this->query('//switch[not(case[not(@test)])]') as $switch) $this->appendElement($switch, 'case'); } protected function addElementIds(DOMDocument $ir) { $id = 0; foreach ($this->query('//element') as $element) $element->setAttribute('id', ++$id); } protected function getOutputContext(DOMNode $output) { $contexts = [ 'boolean(ancestor::attribute)' => 'attribute', '@disable-output-escaping="yes"' => 'raw', 'count(ancestor::element[@name="script"])' => 'raw' ]; foreach ($contexts as $expr => $context) if ($this->evaluate($expr, $output)) return $context; return 'text'; } protected function getParentElementId(DOMNode $node) { $parentNode = $node->parentNode; while (isset($parentNode)) { if ($parentNode->nodeName === 'element') return $parentNode->getAttribute('id'); $parentNode = $parentNode->parentNode; } } protected function markBranchTables(DOMDocument $ir) { foreach ($this->query('//switch[case[2][@test]]') as $switch) $this->markSwitchTable($switch); } protected function markSwitchTable(DOMElement $switch) { $cases = []; $maps = []; foreach ($this->query('./case[@test]', $switch) as $i => $case) { $map = XPathHelper::parseEqualityExpr($case->getAttribute('test')); if ($map === \false) return; $maps += $map; $cases[$i] = [$case, \end($map)]; } if (\count($maps) !== 1) return; $switch->setAttribute('branch-key', \key($maps)); foreach ($cases as $_6920557c) { list($case, $values) = $_6920557c; \sort($values); $case->setAttribute('branch-values', \serialize($values)); } } protected function markConditionalCloseTagElements(DOMDocument $ir) { foreach ($this->query('//closeTag') as $closeTag) { $id = $closeTag->getAttribute('id'); $query = 'ancestor::switch/following-sibling::*/descendant-or-self::closeTag[@id = "' . $id . '"]'; foreach ($this->query($query, $closeTag) as $following) { $following->setAttribute('check', ''); $closeTag->setAttribute('set', ''); } } } protected function markVoidElements(DOMDocument $ir) { foreach ($this->query('//element') as $element) { $elName = $element->getAttribute('name'); if (\strpos($elName, '{') !== \false) $element->setAttribute('void', 'maybe'); elseif (\preg_match($this->voidRegexp, $elName)) $element->setAttribute('void', 'yes'); } } protected function setOutputContext(DOMDocument $ir) { foreach ($this->query('//output') as $output) $output->setAttribute('escape', $this->getOutputContext($output)); } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Helpers\TemplateParser; use DOMDocument; use DOMElement; use DOMNode; class Optimizer extends IRProcessor { public function optimize(DOMDocument $ir) { $this->createXPath($ir); $xml = $ir->saveXML(); $remainingLoops = 10; do { $old = $xml; $this->optimizeCloseTagElements($ir); $xml = $ir->saveXML(); } while (--$remainingLoops > 0 && $xml !== $old); $this->removeCloseTagSiblings($ir); $this->removeContentFromVoidElements($ir); $this->mergeConsecutiveLiteralOutputElements($ir); $this->removeEmptyDefaultCases($ir); } protected function cloneCloseTagElementsIntoSwitch(DOMDocument $ir) { $query = '//switch[name(following-sibling::*[1]) = "closeTag"]'; foreach ($this->query($query) as $switch) { $closeTag = $switch->nextSibling; foreach ($this->query('case', $switch) as $case) if (!$case->lastChild || $case->lastChild->nodeName !== 'closeTag') $case->appendChild($closeTag->cloneNode()); } } protected function cloneCloseTagElementsOutOfSwitch(DOMDocument $ir) { $query = '//switch[case/closeTag][not(case[name(*[1]) != "closeTag"])]'; foreach ($this->query($query) as $switch) { $case = $this->query('case/closeTag', $switch)->item(0); $switch->parentNode->insertBefore($case->cloneNode(), $switch); } } protected function mergeConsecutiveLiteralOutputElements(DOMDocument $ir) { foreach ($this->query('//output[@type="literal"]') as $output) { $disableOutputEscaping = $output->getAttribute('disable-output-escaping'); while ($this->nextSiblingIsLiteralOutput($output, $disableOutputEscaping)) { $output->nodeValue = \htmlspecialchars($output->nodeValue . $output->nextSibling->nodeValue); $output->parentNode->removeChild($output->nextSibling); } } } protected function nextSiblingIsLiteralOutput(DOMElement $node, $disableOutputEscaping) { return isset($node->nextSibling) && $node->nextSibling->nodeName === 'output' && $node->nextSibling->getAttribute('type') === 'literal' && $node->nextSibling->getAttribute('disable-output-escaping') === $disableOutputEscaping; } protected function optimizeCloseTagElements(DOMDocument $ir) { $this->cloneCloseTagElementsIntoSwitch($ir); $this->cloneCloseTagElementsOutOfSwitch($ir); $this->removeRedundantCloseTagElementsInSwitch($ir); $this->removeRedundantCloseTagElements($ir); } protected function removeCloseTagSiblings(DOMDocument $ir) { $query = '//switch[not(case[not(closeTag)])]/following-sibling::closeTag'; $this->removeNodes($ir, $query); } protected function removeContentFromVoidElements(DOMDocument $ir) { foreach ($this->query('//element[@void="yes"]') as $element) { $id = $element->getAttribute('id'); $query = './/closeTag[@id="' . $id . '"]/following-sibling::*'; $this->removeNodes($ir, $query, $element); } } protected function removeEmptyDefaultCases(DOMDocument $ir) { $query = '//case[not(@test)][not(*)][. = ""]'; $this->removeNodes($ir, $query); } protected function removeNodes(DOMDocument $ir, $query, DOMNode $contextNode = \null) { foreach ($this->query($query, $contextNode) as $node) if ($node->parentNode instanceof DOMElement) $node->parentNode->removeChild($node); } protected function removeRedundantCloseTagElements(DOMDocument $ir) { foreach ($this->query('//closeTag') as $closeTag) { $id = $closeTag->getAttribute('id'); $query = 'following-sibling::*/descendant-or-self::closeTag[@id="' . $id . '"]'; $this->removeNodes($ir, $query, $closeTag); } } protected function removeRedundantCloseTagElementsInSwitch(DOMDocument $ir) { $query = '//switch[name(following-sibling::*[1]) = "closeTag"]'; foreach ($this->query($query) as $switch) foreach ($this->query('case', $switch) as $case) while ($case->lastChild && $case->lastChild->nodeName === 'closeTag') $case->removeChild($case->lastChild); } } /* * @package s9e\TextFormatter * @copyright Copyright (c) 2010-2019 The s9e Authors * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace s9e\TextFormatter\Configurator\Helpers\TemplateParser; use DOMDocument; use DOMElement; use DOMXPath; use RuntimeException; use s9e\TextFormatter\Configurator\Helpers\AVTHelper; use s9e\TextFormatter\Configurator\Helpers\TemplateHelper; class Parser extends IRProcessor { protected $normalizer; public function __construct(Normalizer $normalizer) { $this->normalizer = $normalizer; } public function parse($template) { $dom = TemplateHelper::loadTemplate($template); $ir = new DOMDocument; $ir->loadXML('