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 . '>' . $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 .= ')(?: [^>]*)?>.*?\\1|(/?(?!br/|p>)[^ />]+)[^>]*?(/)?)>)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 = "(?