PDF rausgenommen

This commit is contained in:
aschwarz
2023-01-23 11:03:31 +01:00
parent 82d562a322
commit a6523903eb
28078 changed files with 4247552 additions and 2 deletions

View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2010-2017 The s9e Authors
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,39 @@
{
"name": "s9e/text-formatter",
"version": "0.13.1",
"type": "library",
"description": "Multi-purpose text formatting and markup library. Plugins offer support for BBCodes, Markdown, emoticons, HTML, embedding media (YouTube, etc...), enhanced typography and more.",
"homepage": "https://github.com/s9e/TextFormatter/",
"keywords": ["bbcode","bbcodes","blog","censor","embed","emoji","emoticons","engine","forum","html","markdown","markup","media","parser","shortcodes"],
"license": "MIT",
"require": {
"php": ">=5.4.7",
"ext-dom": "*",
"ext-filter": "*",
"lib-pcre": ">=7.2"
},
"require-dev": {
"php": ">=5.4.7",
"matthiasmullie/minify": "*",
"s9e/regexp-builder": ">=1.3.0"
},
"suggest": {
"ext-curl": "Improves the performance of the MediaEmbed plugin and some JavaScript minifiers",
"ext-intl": "Allows international URLs to be accepted by the URL filter",
"ext-json": "Enables the generation of a JavaScript parser",
"ext-mbstring": "Improves the performance of the PHP renderer",
"ext-tokenizer": "Improves the performance of the PHP renderer",
"ext-xsl": "Enables the XSLT renderer",
"ext-zlib": "Enables gzip compression when scraping content via the MediaEmbed plugin"
},
"autoload": {
"psr-4": {
"s9e\\TextFormatter\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"s9e\\TextFormatter\\Tests\\": "tests"
}
}
}

View File

@ -0,0 +1,58 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter;
abstract class Bundle
{
public static function getCachedParser()
{
if (!isset(static::$parser))
static::$parser = static::getParser();
return static::$parser;
}
public static function getCachedRenderer()
{
if (!isset(static::$renderer))
static::$renderer = static::getRenderer();
return static::$renderer;
}
public static function parse($text)
{
if (isset(static::$beforeParse))
$text = \call_user_func(static::$beforeParse, $text);
$xml = static::getCachedParser()->parse($text);
if (isset(static::$afterParse))
$xml = \call_user_func(static::$afterParse, $xml);
return $xml;
}
public static function render($xml, array $params = [])
{
$renderer = static::getCachedRenderer();
if (!empty($params))
$renderer->setParameters($params);
if (isset(static::$beforeRender))
$xml = \call_user_func(static::$beforeRender, $xml);
$output = $renderer->render($xml);
if (isset(static::$afterRender))
$output = \call_user_func(static::$afterRender, $output);
return $output;
}
public static function reset()
{
static::$parser = \null;
static::$renderer = \null;
}
public static function unparse($xml)
{
if (isset(static::$beforeUnparse))
$xml = \call_user_func(static::$beforeUnparse, $xml);
$text = Unparser::unparse($xml);
if (isset(static::$afterUnparse))
$text = \call_user_func(static::$afterUnparse, $text);
return $text;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator;
use s9e\TextFormatter\Configurator;
abstract class Bundle
{
abstract public function configure(Configurator $configurator);
public static function getConfigurator()
{
$configurator = new Configurator;
$bundle = new static;
$bundle->configure($configurator);
return $configurator;
}
public static function getOptions()
{
return [];
}
}

View File

@ -0,0 +1,113 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Bundles;
use s9e\TextFormatter\Configurator;
use s9e\TextFormatter\Configurator\Bundle;
class Fatdown extends Bundle
{
public function configure(Configurator $configurator)
{
$configurator->urlConfig->allowScheme('ftp');
$configurator->Litedown->decodeHtmlEntities = \true;
$configurator->Autoemail;
$configurator->Autolink;
$configurator->Escaper;
$configurator->FancyPants;
$configurator->HTMLComments;
$configurator->HTMLEntities;
$configurator->PipeTables;
$htmlAliases = [
'a' => ['URL', 'href' => 'url'],
'hr' => 'HR',
'em' => 'EM',
's' => 'S',
'strong' => 'STRONG',
'sup' => 'SUP'
];
foreach ($htmlAliases as $elName => $alias)
if (\is_array($alias))
{
$configurator->HTMLElements->aliasElement($elName, $alias[0]);
unset($alias[0]);
foreach ($alias as $attrName => $alias)
$configurator->HTMLElements->aliasAttribute($elName, $attrName, $alias);
}
else
$configurator->HTMLElements->aliasElement($elName, $alias);
$htmlElements = [
'abbr' => ['title'],
'b',
'br',
'code',
'dd',
'del',
'div' => ['class'],
'dl',
'dt',
'i',
'img' => ['alt', 'height', 'src', 'title', 'width'],
'ins',
'li',
'ol',
'pre',
'rb',
'rp',
'rt',
'rtc',
'ruby',
'span' => ['class'],
'strong',
'sub',
'sup',
'table',
'tbody',
'td' => ['colspan', 'rowspan'],
'tfoot',
'th' => ['colspan', 'rowspan', 'scope'],
'thead',
'tr',
'u',
'ul'
];
foreach ($htmlElements as $k => $v)
{
if (\is_numeric($k))
{
$elName = $v;
$attrNames = [];
}
else
{
$elName = $k;
$attrNames = $v;
}
$configurator->HTMLElements->allowElement($elName);
foreach ($attrNames as $attrName)
$configurator->HTMLElements->allowAttribute($elName, $attrName);
}
$configurator->tags['html:dd']->rules->createParagraphs(\false);
$configurator->tags['html:dt']->rules->createParagraphs(\false);
$configurator->tags['html:td']->rules->createParagraphs(\false);
$configurator->tags['html:th']->rules->createParagraphs(\false);
$configurator->plugins->load('MediaEmbed', ['createMediaBBCode' => \false]);
$sites = [
'bandcamp',
'dailymotion',
'facebook',
'liveleak',
'soundcloud',
'spotify',
'twitch',
'vimeo',
'vine',
'youtube'
];
foreach ($sites as $site)
$configurator->MediaEmbed->add($site);
}
}

View File

@ -0,0 +1,96 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Bundles;
use s9e\TextFormatter\Configurator;
use s9e\TextFormatter\Configurator\Bundle;
class Forum extends Bundle
{
public function configure(Configurator $configurator)
{
$configurator->rootRules->enableAutoLineBreaks();
$configurator->BBCodes->addFromRepository('B');
$configurator->BBCodes->addFromRepository('CENTER');
$configurator->BBCodes->addFromRepository('CODE');
$configurator->BBCodes->addFromRepository('COLOR');
$configurator->BBCodes->addFromRepository('EMAIL');
$configurator->BBCodes->addFromRepository('FONT');
$configurator->BBCodes->addFromRepository('I');
$configurator->BBCodes->addFromRepository('IMG');
$configurator->BBCodes->addFromRepository('LIST');
$configurator->BBCodes->addFromRepository('*');
$configurator->BBCodes->add('LI');
$configurator->BBCodes->addFromRepository('OL');
$configurator->BBCodes->addFromRepository('QUOTE', 'default', [
'authorStr' => '<xsl:value-of select="@author"/> <xsl:value-of select="$L_WROTE"/>'
]);
$configurator->BBCodes->addFromRepository('S');
$configurator->BBCodes->addFromRepository('SIZE');
$configurator->BBCodes->addFromRepository('SPOILER', 'default', [
'hideStr' => '{L_HIDE}',
'showStr' => '{L_SHOW}',
'spoilerStr' => '{L_SPOILER}',
]);
$configurator->BBCodes->addFromRepository('TABLE');
$configurator->BBCodes->addFromRepository('TD');
$configurator->BBCodes->addFromRepository('TH');
$configurator->BBCodes->addFromRepository('TR');
$configurator->BBCodes->addFromRepository('U');
$configurator->BBCodes->addFromRepository('UL');
$configurator->BBCodes->addFromRepository('URL');
$configurator->rendering->parameters = [
'L_WROTE' => 'wrote:',
'L_HIDE' => 'Hide',
'L_SHOW' => 'Show',
'L_SPOILER' => 'Spoiler'
];
$emoticons = [
':)' => '1F642',
':-)' => '1F642',
';)' => '1F609',
';-)' => '1F609',
':D' => '1F600',
':-D' => '1F600',
':(' => '2639',
':-(' => '2639',
':-*' => '1F618',
':P' => '1F61B',
':-P' => '1F61B',
':p' => '1F61B',
':-p' => '1F61B',
';P' => '1F61C',
';-P' => '1F61C',
';p' => '1F61C',
';-p' => '1F61C',
':?' => '1F615',
':-?' => '1F615',
':|' => '1F610',
':-|' => '1F610',
':o' => '1F62E',
':lol:' => '1F602'
];
foreach ($emoticons as $code => $hex)
$configurator->Emoji->addAlias($code, \html_entity_decode('&#x' . $hex . ';'));
$configurator->MediaEmbed->createIndividualBBCodes = \true;
$configurator->MediaEmbed->add('bandcamp');
$configurator->MediaEmbed->add('dailymotion');
$configurator->MediaEmbed->add('facebook');
$configurator->MediaEmbed->add('indiegogo');
$configurator->MediaEmbed->add('instagram');
$configurator->MediaEmbed->add('kickstarter');
$configurator->MediaEmbed->add('liveleak');
$configurator->MediaEmbed->add('soundcloud');
$configurator->MediaEmbed->add('twitch');
$configurator->MediaEmbed->add('twitter');
$configurator->MediaEmbed->add('vimeo');
$configurator->MediaEmbed->add('vine');
$configurator->MediaEmbed->add('wshh');
$configurator->MediaEmbed->add('youtube');
$configurator->Autoemail;
$configurator->Autolink;
}
}

View File

@ -0,0 +1,24 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Bundles;
use DOMDocument;
use s9e\TextFormatter\Configurator;
use s9e\TextFormatter\Configurator\Bundle;
class MediaPack extends Bundle
{
public function configure(Configurator $configurator)
{
if (!isset($configurator->MediaEmbed))
{
$pluginOptions = ['createMediaBBCode' => isset($configurator->BBCodes)];
$configurator->plugins->load('MediaEmbed', $pluginOptions);
}
foreach ($configurator->MediaEmbed->defaultSites as $siteId => $siteConfig)
$configurator->MediaEmbed->add($siteId);
}
}

View File

@ -0,0 +1,22 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Collections;
use s9e\TextFormatter\Configurator\Validators\AttributeName;
class AttributeList extends NormalizedList
{
public function normalizeValue($attrName)
{
return AttributeName::normalize($attrName);
}
public function asConfig()
{
$list = \array_unique($this->items);
\sort($list);
return $list;
}
}

View File

@ -0,0 +1,33 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Collections;
use InvalidArgumentException;
use ReflectionClass;
use s9e\TextFormatter\Configurator\JavaScript\Minifier;
class MinifierList extends NormalizedList
{
public function normalizeValue($minifier)
{
if (\is_string($minifier))
$minifier = $this->getMinifierInstance($minifier);
elseif (\is_array($minifier) && !empty($minifier[0]))
$minifier = $this->getMinifierInstance($minifier[0], \array_slice($minifier, 1));
if (!($minifier instanceof Minifier))
throw new InvalidArgumentException('Invalid minifier ' . \var_export($minifier, \true));
return $minifier;
}
protected function getMinifierInstance($name, array $args = [])
{
$className = 's9e\\TextFormatter\\Configurator\\JavaScript\\Minifiers\\' . $name;
if (!\class_exists($className))
throw new InvalidArgumentException('Invalid minifier ' . \var_export($name, \true));
$reflection = new ReflectionClass($className);
$minifier = (empty($args)) ? $reflection->newInstance() : $reflection->newInstanceArgs($args);
return $minifier;
}
}

View File

@ -0,0 +1,22 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Collections;
use s9e\TextFormatter\Configurator\Validators\TagName;
class TagList extends NormalizedList
{
public function normalizeValue($attrName)
{
return TagName::normalize($attrName);
}
public function asConfig()
{
$list = \array_unique($this->items);
\sort($list);
return $list;
}
}

View File

@ -0,0 +1,12 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Exceptions;
use RuntimeException;
class InvalidTemplateException extends RuntimeException
{
}

View File

@ -0,0 +1,11 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Exceptions;
class InvalidXslException extends InvalidTemplateException
{
}

View File

@ -0,0 +1,31 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Exceptions;
use DOMNode;
use s9e\TextFormatter\Configurator\Helpers\TemplateHelper;
class UnsafeTemplateException extends InvalidTemplateException
{
protected $node;
public function __construct($msg, DOMNode $node)
{
parent::__construct($msg);
$this->node = $node;
}
public function getNode()
{
return $this->node;
}
public function highlightNode($prepend = '<span style="background-color:#ff0">', $append = '</span>')
{
return TemplateHelper::highlightNode($this->node, $prepend, $append);
}
public function setNode(DOMNode $node)
{
$this->node = $node;
}
}

View File

@ -0,0 +1,23 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Helpers;
abstract class ContextSafeness
{
public static function getDisallowedCharactersAsURL()
{
return [':'];
}
public static function getDisallowedCharactersInCSS()
{
return ['(', ')', ':', '\\', '"', "'", ';', '{', '}'];
}
public static function getDisallowedCharactersInJS()
{
return ['(', ')', '"', "'", '\\', "\r", "\n", "\xE2\x80\xA8", "\xE2\x80\xA9", ':', '%', '='];
}
}

View File

@ -0,0 +1,232 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Helpers;
use RuntimeException;
abstract class RegexpParser
{
public static function getAllowedCharacterRegexp($regexp)
{
$def = self::parse($regexp);
if (\strpos($def['modifiers'], 'm') !== \false)
return '//';
if (\substr($def['regexp'], 0, 1) !== '^'
|| \substr($def['regexp'], -1) !== '$')
return '//';
$def['tokens'][] = [
'pos' => \strlen($def['regexp']),
'len' => 0,
'type' => 'end'
];
$patterns = [];
$literal = '';
$pos = 0;
$skipPos = 0;
$depth = 0;
foreach ($def['tokens'] as $token)
{
if ($token['type'] === 'option')
$skipPos = \max($skipPos, $token['pos'] + $token['len']);
if (\strpos($token['type'], 'AssertionStart') !== \false)
{
$endToken = $def['tokens'][$token['endToken']];
$skipPos = \max($skipPos, $endToken['pos'] + $endToken['len']);
}
if ($token['pos'] >= $skipPos)
{
if ($token['type'] === 'characterClass')
$patterns[] = '[' . $token['content'] . ']';
if ($token['pos'] > $pos)
{
$tmp = \substr($def['regexp'], $pos, $token['pos'] - $pos);
$literal .= $tmp;
if (!$depth)
{
$tmp = \str_replace('\\\\', '', $tmp);
if (\preg_match('/(?<!\\\\)\\|(?!\\^)/', $tmp))
return '//';
if (\preg_match('/(?<![$\\\\])\\|/', $tmp))
return '//';
}
}
}
if (\substr($token['type'], -5) === 'Start')
++$depth;
elseif (\substr($token['type'], -3) === 'End')
--$depth;
$pos = \max($skipPos, $token['pos'] + $token['len']);
}
if (\preg_match('#(?<!\\\\)(?:\\\\\\\\)*\\.#', $literal))
{
if (\strpos($def['modifiers'], 's') !== \false
|| \strpos($literal, "\n") !== \false)
return '//';
$patterns[] = '.';
$literal = \preg_replace('#(?<!\\\\)((?:\\\\\\\\)*)\\.#', '$1', $literal);
}
$literal = \preg_replace('#(?<!\\\\)((?:\\\\\\\\)*)[*+?]#', '$1', $literal);
$literal = \preg_replace('#(?<!\\\\)((?:\\\\\\\\)*)\\{[^}]+\\}#', '$1', $literal);
$literal = \preg_replace('#(?<!\\\\)((?:\\\\\\\\)*)\\\\[bBAZzG1-9]#', '$1', $literal);
$literal = \preg_replace('#(?<!\\\\)((?:\\\\\\\\)*)[$^|]#', '$1', $literal);
$literal = \preg_replace('#(?<!\\\\)((?:\\\\\\\\)*)([-^\\]])#', '$1\\\\$2', $literal);
if (\strpos($def['modifiers'], 'D') === \false)
$literal .= "\n";
if ($literal !== '')
$patterns[] = '[' . $literal . ']';
if (empty($patterns))
return '/^$/D';
$regexp = $def['delimiter'] . \implode('|', $patterns) . $def['delimiter'];
if (\strpos($def['modifiers'], 'i') !== \false)
$regexp .= 'i';
if (\strpos($def['modifiers'], 'u') !== \false)
$regexp .= 'u';
return $regexp;
}
public static function getCaptureNames($regexp)
{
$map = [''];
$regexpInfo = self::parse($regexp);
foreach ($regexpInfo['tokens'] as $tok)
if ($tok['type'] === 'capturingSubpatternStart')
$map[] = (isset($tok['name'])) ? $tok['name'] : '';
return $map;
}
public static function parse($regexp)
{
if (!\preg_match('#^(.)(.*?)\\1([a-zA-Z]*)$#Ds', $regexp, $m))
throw new RuntimeException('Could not parse regexp delimiters');
$ret = [
'delimiter' => $m[1],
'modifiers' => $m[3],
'regexp' => $m[2],
'tokens' => []
];
$regexp = $m[2];
$openSubpatterns = [];
$pos = 0;
$regexpLen = \strlen($regexp);
while ($pos < $regexpLen)
{
switch ($regexp[$pos])
{
case '\\':
$pos += 2;
break;
case '[':
if (!\preg_match('#\\[(.*?(?<!\\\\)(?:\\\\\\\\)*+)\\]((?:[+*][+?]?|\\?)?)#A', $regexp, $m, 0, $pos))
throw new RuntimeException('Could not find matching bracket from pos ' . $pos);
$ret['tokens'][] = [
'pos' => $pos,
'len' => \strlen($m[0]),
'type' => 'characterClass',
'content' => $m[1],
'quantifiers' => $m[2]
];
$pos += \strlen($m[0]);
break;
case '(':
if (\preg_match('#\\(\\?([a-z]*)\\)#iA', $regexp, $m, 0, $pos))
{
$ret['tokens'][] = [
'pos' => $pos,
'len' => \strlen($m[0]),
'type' => 'option',
'options' => $m[1]
];
$pos += \strlen($m[0]);
break;
}
if (\preg_match("#(?J)\\(\\?(?:P?<(?<name>[a-z_0-9]+)>|'(?<name>[a-z_0-9]+)')#A", $regexp, $m, \PREG_OFFSET_CAPTURE, $pos))
{
$tok = [
'pos' => $pos,
'len' => \strlen($m[0][0]),
'type' => 'capturingSubpatternStart',
'name' => $m['name'][0]
];
$pos += \strlen($m[0][0]);
}
elseif (\preg_match('#\\(\\?([a-z]*):#iA', $regexp, $m, 0, $pos))
{
$tok = [
'pos' => $pos,
'len' => \strlen($m[0]),
'type' => 'nonCapturingSubpatternStart',
'options' => $m[1]
];
$pos += \strlen($m[0]);
}
elseif (\preg_match('#\\(\\?>#iA', $regexp, $m, 0, $pos))
{
$tok = [
'pos' => $pos,
'len' => \strlen($m[0]),
'type' => 'nonCapturingSubpatternStart',
'subtype' => 'atomic'
];
$pos += \strlen($m[0]);
}
elseif (\preg_match('#\\(\\?(<?[!=])#A', $regexp, $m, 0, $pos))
{
$assertions = [
'=' => 'lookahead',
'<=' => 'lookbehind',
'!' => 'negativeLookahead',
'<!' => 'negativeLookbehind'
];
$tok = [
'pos' => $pos,
'len' => \strlen($m[0]),
'type' => $assertions[$m[1]] . 'AssertionStart'
];
$pos += \strlen($m[0]);
}
elseif (\preg_match('#\\(\\?#A', $regexp, $m, 0, $pos))
throw new RuntimeException('Unsupported subpattern type at pos ' . $pos);
else
{
$tok = [
'pos' => $pos,
'len' => 1,
'type' => 'capturingSubpatternStart'
];
++$pos;
}
$openSubpatterns[] = \count($ret['tokens']);
$ret['tokens'][] = $tok;
break;
case ')':
if (empty($openSubpatterns))
throw new RuntimeException('Could not find matching pattern start for right parenthesis at pos ' . $pos);
$k = \array_pop($openSubpatterns);
$startToken =& $ret['tokens'][$k];
$startToken['endToken'] = \count($ret['tokens']);
$startToken['content'] = \substr(
$regexp,
$startToken['pos'] + $startToken['len'],
$pos - ($startToken['pos'] + $startToken['len'])
);
$spn = \strspn($regexp, '+*?', 1 + $pos);
$quantifiers = \substr($regexp, 1 + $pos, $spn);
$ret['tokens'][] = [
'pos' => $pos,
'len' => 1 + $spn,
'type' => \substr($startToken['type'], 0, -5) . 'End',
'quantifiers' => $quantifiers
];
unset($startToken);
$pos += 1 + $spn;
break;
default:
++$pos;
}
}
if (!empty($openSubpatterns))
throw new RuntimeException('Could not find matching pattern end for left parenthesis at pos ' . $ret['tokens'][$openSubpatterns[0]]['pos']);
return $ret;
}
}

View File

@ -0,0 +1,17 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
class AlnumFilter extends RegexpFilter
{
public function __construct()
{
parent::__construct('/^[0-9A-Za-z]+$/D');
$this->markAsSafeAsURL();
$this->markAsSafeInCSS();
}
}

View File

@ -0,0 +1,31 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
use InvalidArgumentException;
use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
class ChoiceFilter extends RegexpFilter
{
public function __construct(array $values = \null, $caseSensitive = \false)
{
parent::__construct();
if (isset($values))
$this->setValues($values, $caseSensitive);
}
public function setValues(array $values, $caseSensitive = \false)
{
if (!\is_bool($caseSensitive))
throw new InvalidArgumentException('Argument 2 passed to ' . __METHOD__ . ' must be a boolean');
$regexp = RegexpBuilder::fromList($values, ['delimiter' => '/']);
$regexp = '/^' . $regexp . '$/D';
if (!$caseSensitive)
$regexp .= 'i';
if (!\preg_match('#^[[:ascii:]]*$#D', $regexp))
$regexp .= 'u';
$this->setRegexp($regexp);
}
}

View File

@ -0,0 +1,16 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
class ColorFilter extends RegexpFilter
{
public function __construct()
{
parent::__construct('/^(?>#[0-9a-f]{3,6}|rgb\\(\\d{1,3}, *\\d{1,3}, *\\d{1,3}\\)|[a-z]+)$/Di');
$this->markAsSafeInCSS();
}
}

View File

@ -0,0 +1,17 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
use s9e\TextFormatter\Configurator\Items\AttributeFilter;
class EmailFilter extends AttributeFilter
{
public function __construct()
{
parent::__construct('s9e\\TextFormatter\\Parser\\AttributeFilters\\EmailFilter::filter');
$this->setJS('EmailFilter.filter');
}
}

View File

@ -0,0 +1,17 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
use s9e\TextFormatter\Configurator\Items\AttributeFilter;
class FalseFilter extends AttributeFilter
{
public function __construct()
{
parent::__construct('s9e\\TextFormatter\\Parser\\AttributeFilters\\FalseFilter::filter');
$this->setJS('FalseFilter.filter');
}
}

View File

@ -0,0 +1,20 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
use s9e\TextFormatter\Configurator\Items\AttributeFilter;
class FloatFilter extends AttributeFilter
{
public function __construct()
{
parent::__construct('s9e\\TextFormatter\\Parser\\AttributeFilters\\NumericFilter::filterFloat');
$this->setJS('NumericFilter.filterFloat');
$this->markAsSafeAsURL();
$this->markAsSafeInCSS();
$this->markAsSafeInJS();
}
}

View File

@ -0,0 +1,21 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
class FontfamilyFilter extends RegexpFilter
{
public function __construct()
{
$namechars = '[- \\w]+';
$double = '"' . $namechars . '"';
$single = "'" . $namechars . "'";
$name = '(?:' . $single . '|' . $double . '|' . $namechars . ')';
$regexp = '/^' . $name . '(?:, *' . $name . ')*$/';
parent::__construct($regexp);
$this->markAsSafeInCSS();
}
}

View File

@ -0,0 +1,74 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
use InvalidArgumentException;
use RuntimeException;
use s9e\TextFormatter\Configurator\Helpers\ContextSafeness;
use s9e\TextFormatter\Configurator\Items\AttributeFilter;
use s9e\TextFormatter\Configurator\JavaScript\Dictionary;
class HashmapFilter extends AttributeFilter
{
public function __construct(array $map = \null, $strict = \false)
{
parent::__construct('s9e\\TextFormatter\\Parser\\AttributeFilters\\HashmapFilter::filter');
$this->resetParameters();
$this->addParameterByName('attrValue');
$this->addParameterByName('map');
$this->addParameterByName('strict');
$this->setJS('HashmapFilter.filter');
if (isset($map))
$this->setMap($map, $strict);
}
public function asConfig()
{
if (!isset($this->vars['map']))
throw new RuntimeException("Hashmap filter is missing a 'map' value");
return parent::asConfig();
}
public function setMap(array $map, $strict = \false)
{
if (!\is_bool($strict))
throw new InvalidArgumentException('Argument 2 passed to ' . __METHOD__ . ' must be a boolean');
if (!$strict)
$map = $this->optimizeLooseMap($map);
\ksort($map);
$this->vars['map'] = new Dictionary($map);
$this->vars['strict'] = $strict;
$this->resetSafeness();
if (!empty($this->vars['strict']))
{
$this->evaluateSafenessInCSS();
$this->evaluateSafenessInJS();
}
}
protected function evaluateSafenessInCSS()
{
$disallowedChars = ContextSafeness::getDisallowedCharactersInCSS();
foreach ($this->vars['map'] as $value)
foreach ($disallowedChars as $char)
if (\strpos($value, $char) !== \false)
return;
$this->markAsSafeInCSS();
}
protected function evaluateSafenessInJS()
{
$disallowedChars = ContextSafeness::getDisallowedCharactersInJS();
foreach ($this->vars['map'] as $value)
foreach ($disallowedChars as $char)
if (\strpos($value, $char) !== \false)
return;
$this->markAsSafeInJS();
}
protected function optimizeLooseMap(array $map)
{
foreach ($map as $k => $v)
if ($k === $v)
unset($map[$k]);
return $map;
}
}

View File

@ -0,0 +1,17 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
class IdentifierFilter extends RegexpFilter
{
public function __construct()
{
parent::__construct('/^[-0-9A-Za-z_]+$/D');
$this->markAsSafeAsURL();
$this->markAsSafeInCSS();
}
}

View File

@ -0,0 +1,20 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
use s9e\TextFormatter\Configurator\Items\AttributeFilter;
class IntFilter extends AttributeFilter
{
public function __construct()
{
parent::__construct('s9e\\TextFormatter\\Parser\\AttributeFilters\\NumericFilter::filterInt');
$this->setJS('NumericFilter.filterInt');
$this->markAsSafeAsURL();
$this->markAsSafeInCSS();
$this->markAsSafeInJS();
}
}

View File

@ -0,0 +1,17 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
use s9e\TextFormatter\Configurator\Items\AttributeFilter;
class IpFilter extends AttributeFilter
{
public function __construct()
{
parent::__construct('s9e\\TextFormatter\\Parser\\AttributeFilters\\NetworkFilter::filterIp');
$this->setJS('NetworkFilter.filterIp');
}
}

View File

@ -0,0 +1,17 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
use s9e\TextFormatter\Configurator\Items\AttributeFilter;
class IpportFilter extends AttributeFilter
{
public function __construct()
{
parent::__construct('s9e\\TextFormatter\\Parser\\AttributeFilters\\NetworkFilter::filterIpport');
$this->setJS('NetworkFilter.filterIpport');
}
}

View File

@ -0,0 +1,17 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
use s9e\TextFormatter\Configurator\Items\AttributeFilter;
class Ipv4Filter extends AttributeFilter
{
public function __construct()
{
parent::__construct('s9e\\TextFormatter\\Parser\\AttributeFilters\\NetworkFilter::filterIpv4');
$this->setJS('NetworkFilter.filterIpv4');
}
}

View File

@ -0,0 +1,17 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
use s9e\TextFormatter\Configurator\Items\AttributeFilter;
class Ipv6Filter extends AttributeFilter
{
public function __construct()
{
parent::__construct('s9e\\TextFormatter\\Parser\\AttributeFilters\\NetworkFilter::filterIpv6');
$this->setJS('NetworkFilter.filterIpv6');
}
}

View File

@ -0,0 +1,88 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
use InvalidArgumentException;
use RuntimeException;
use s9e\TextFormatter\Configurator\Helpers\ContextSafeness;
use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
use s9e\TextFormatter\Configurator\Items\AttributeFilter;
use s9e\TextFormatter\Configurator\Items\Regexp;
class MapFilter extends AttributeFilter
{
public function __construct(array $map = \null, $caseSensitive = \false, $strict = \false)
{
parent::__construct('s9e\\TextFormatter\\Parser\\AttributeFilters\\MapFilter::filter');
$this->resetParameters();
$this->addParameterByName('attrValue');
$this->addParameterByName('map');
$this->setJS('MapFilter.filter');
if (isset($map))
$this->setMap($map, $caseSensitive, $strict);
}
public function asConfig()
{
if (!isset($this->vars['map']))
throw new RuntimeException("Map filter is missing a 'map' value");
return parent::asConfig();
}
public function setMap(array $map, $caseSensitive = \false, $strict = \false)
{
if (!\is_bool($caseSensitive))
throw new InvalidArgumentException('Argument 2 passed to ' . __METHOD__ . ' must be a boolean');
if (!\is_bool($strict))
throw new InvalidArgumentException('Argument 3 passed to ' . __METHOD__ . ' must be a boolean');
$this->resetSafeness();
if ($strict)
$this->assessSafeness($map);
$valueKeys = [];
foreach ($map as $key => $value)
$valueKeys[$value][] = $key;
$map = [];
foreach ($valueKeys as $value => $keys)
{
$regexp = RegexpBuilder::fromList(
$keys,
[
'delimiter' => '/',
'caseInsensitive' => !$caseSensitive
]
);
$regexp = '/^' . $regexp . '$/D';
if (!$caseSensitive)
$regexp .= 'i';
if (!\preg_match('#^[[:ascii:]]*$#D', $regexp))
$regexp .= 'u';
$map[] = [new Regexp($regexp), $value];
}
if ($strict)
$map[] = [new Regexp('//'), \false];
$this->vars['map'] = $map;
}
protected function assessSafeness(array $map)
{
$values = \implode('', $map);
$isSafeInCSS = \true;
foreach (ContextSafeness::getDisallowedCharactersInCSS() as $char)
if (\strpos($values, $char) !== \false)
{
$isSafeInCSS = \false;
break;
}
if ($isSafeInCSS)
$this->markAsSafeInCSS();
$isSafeInJS = \true;
foreach (ContextSafeness::getDisallowedCharactersInJS() as $char)
if (\strpos($values, $char) !== \false)
{
$isSafeInJS = \false;
break;
}
if ($isSafeInJS)
$this->markAsSafeInJS();
}
}

View File

@ -0,0 +1,18 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
class NumberFilter extends RegexpFilter
{
public function __construct()
{
parent::__construct('/^[0-9]+$/D');
$this->markAsSafeAsURL();
$this->markAsSafeInCSS();
$this->markAsSafeInJS();
}
}

View File

@ -0,0 +1,50 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
use InvalidArgumentException;
use RuntimeException;
use s9e\TextFormatter\Configurator\Items\AttributeFilter;
class RangeFilter extends AttributeFilter
{
public function __construct($min = \null, $max = \null)
{
parent::__construct('s9e\\TextFormatter\\Parser\\AttributeFilters\\NumericFilter::filterRange');
$this->resetParameters();
$this->addParameterByName('attrValue');
$this->addParameterByName('min');
$this->addParameterByName('max');
$this->addParameterByName('logger');
$this->setJS('NumericFilter.filterRange');
$this->markAsSafeAsURL();
$this->markAsSafeInCSS();
$this->markAsSafeInJS();
if (isset($min))
$this->setRange($min, $max);
}
public function asConfig()
{
if (!isset($this->vars['min']))
throw new RuntimeException("Range filter is missing a 'min' value");
if (!isset($this->vars['max']))
throw new RuntimeException("Range filter is missing a 'max' value");
return parent::asConfig();
}
public function setRange($min, $max)
{
$min = \filter_var($min, \FILTER_VALIDATE_INT);
$max = \filter_var($max, \FILTER_VALIDATE_INT);
if ($min === \false)
throw new InvalidArgumentException('Argument 1 passed to ' . __METHOD__ . ' must be an integer');
if ($max === \false)
throw new InvalidArgumentException('Argument 2 passed to ' . __METHOD__ . ' must be an integer');
if ($min > $max)
throw new InvalidArgumentException('Invalid range: min (' . $min . ') > max (' . $max . ')');
$this->vars['min'] = $min;
$this->vars['max'] = $max;
}
}

View File

@ -0,0 +1,92 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
use Exception;
use RuntimeException;
use s9e\TextFormatter\Configurator\Helpers\ContextSafeness;
use s9e\TextFormatter\Configurator\Helpers\RegexpParser;
use s9e\TextFormatter\Configurator\Items\AttributeFilter;
use s9e\TextFormatter\Configurator\Items\Regexp;
class RegexpFilter extends AttributeFilter
{
public function __construct($regexp = \null)
{
parent::__construct('s9e\\TextFormatter\\Parser\\AttributeFilters\\RegexpFilter::filter');
$this->resetParameters();
$this->addParameterByName('attrValue');
$this->addParameterByName('regexp');
$this->setJS('RegexpFilter.filter');
if (isset($regexp))
$this->setRegexp($regexp);
}
public function asConfig()
{
if (!isset($this->vars['regexp']))
throw new RuntimeException("Regexp filter is missing a 'regexp' value");
return parent::asConfig();
}
public function getRegexp()
{
return (string) $this->vars['regexp'];
}
public function setRegexp($regexp)
{
if (\is_string($regexp))
$regexp = new Regexp($regexp);
$this->vars['regexp'] = $regexp;
$this->resetSafeness();
$this->evaluateSafeness();
}
protected function evaluateSafeness()
{
try
{
$this->evaluateSafenessAsURL();
$this->evaluateSafenessInCSS();
$this->evaluateSafenessInJS();
}
catch (Exception $e)
{
}
}
protected function evaluateSafenessAsURL()
{
$regexpInfo = RegexpParser::parse($this->vars['regexp']);
$captureStart = '(?>\\((?:\\?:)?)*';
$regexp = '#^\\^' . $captureStart . '(?!data|\\w*script)[a-z0-9]+\\??:#i';
if (\preg_match($regexp, $regexpInfo['regexp'])
&& \strpos($regexpInfo['modifiers'], 'm') === \false)
{
$this->markAsSafeAsURL();
return;
}
$regexp = RegexpParser::getAllowedCharacterRegexp($this->vars['regexp']);
foreach (ContextSafeness::getDisallowedCharactersAsURL() as $char)
if (\preg_match($regexp, $char))
return;
$this->markAsSafeAsURL();
}
protected function evaluateSafenessInCSS()
{
$regexp = RegexpParser::getAllowedCharacterRegexp($this->vars['regexp']);
foreach (ContextSafeness::getDisallowedCharactersInCSS() as $char)
if (\preg_match($regexp, $char))
return;
$this->markAsSafeInCSS();
}
protected function evaluateSafenessInJS()
{
$safeExpressions = [
'\\d+',
'[0-9]+'
];
$regexp = '(^(?<delim>.)\\^(?:(?<expr>' . \implode('|', \array_map('preg_quote', $safeExpressions)) . ')|\\((?:\\?[:>])?(?&expr)\\))\\$(?&delim)(?=.*D)[Dis]*$)D';
if (\preg_match($regexp, $this->vars['regexp']))
$this->markAsSafeInJS();
}
}

View File

@ -0,0 +1,16 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
class SimpletextFilter extends RegexpFilter
{
public function __construct()
{
parent::__construct('/^[- +,.0-9A-Za-z_]+$/D');
$this->markAsSafeInCSS();
}
}

View File

@ -0,0 +1,20 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
use s9e\TextFormatter\Configurator\Items\AttributeFilter;
class TimestampFilter extends AttributeFilter
{
public function __construct()
{
parent::__construct('s9e\\TextFormatter\\Parser\\AttributeFilters\\TimestampFilter::filter');
$this->setJS('TimestampFilter.filter');
$this->markAsSafeAsURL();
$this->markAsSafeInCSS();
$this->markAsSafeInJS();
}
}

View File

@ -0,0 +1,29 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items\AttributeFilters;
use s9e\TextFormatter\Configurator\Items\AttributeFilter;
class UintFilter extends AttributeFilter
{
public function __construct()
{
parent::__construct('s9e\\TextFormatter\\Parser\\AttributeFilters\\NumericFilter::filterUint');
$this->setJS('NumericFilter.filterUint');
}
public function isSafeInCSS()
{
return \true;
}
public function isSafeInJS()
{
return \true;
}
public function isSafeAsURL()
{
return \true;
}
}

View File

@ -0,0 +1,22 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items;
use InvalidArgumentException;
use s9e\TextFormatter\Configurator\Items\Regexp;
class AttributePreprocessor extends Regexp
{
public function getAttributes()
{
return $this->getNamedCaptures();
}
public function getRegexp()
{
return $this->regexp;
}
}

View File

@ -0,0 +1,22 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 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\TemplateHelper;
class TemplateDocument extends DOMDocument
{
protected $template;
public function __construct(Template $template)
{
$this->template = $template;
}
public function saveChanges()
{
$this->template->setContent(TemplateHelper::saveTemplate($this));
}
}

View File

@ -0,0 +1,11 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Items;
class UnsafeTemplate extends Template
{
}

View File

@ -0,0 +1,229 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator;
use ReflectionClass;
use s9e\TextFormatter\Configurator;
use s9e\TextFormatter\Configurator\Helpers\ConfigHelper;
use s9e\TextFormatter\Configurator\JavaScript\CallbackGenerator;
use s9e\TextFormatter\Configurator\JavaScript\Code;
use s9e\TextFormatter\Configurator\JavaScript\ConfigOptimizer;
use s9e\TextFormatter\Configurator\JavaScript\Dictionary;
use s9e\TextFormatter\Configurator\JavaScript\Encoder;
use s9e\TextFormatter\Configurator\JavaScript\HintGenerator;
use s9e\TextFormatter\Configurator\JavaScript\Minifier;
use s9e\TextFormatter\Configurator\JavaScript\Minifiers\Noop;
use s9e\TextFormatter\Configurator\JavaScript\RegexpConvertor;
use s9e\TextFormatter\Configurator\JavaScript\StylesheetCompressor;
use s9e\TextFormatter\Configurator\RendererGenerators\XSLT;
class JavaScript
{
protected $callbackGenerator;
protected $config;
protected $configOptimizer;
protected $configurator;
public $encoder;
public $exportMethods = [
'disablePlugin',
'disableTag',
'enablePlugin',
'enableTag',
'getLogger',
'parse',
'preview',
'setNestingLimit',
'setParameter',
'setTagLimit'
];
protected $hintGenerator;
protected $minifier;
protected $stylesheetCompressor;
protected $xsl;
public function __construct(Configurator $configurator)
{
$this->encoder = new Encoder;
$this->callbackGenerator = new CallbackGenerator;
$this->configOptimizer = new ConfigOptimizer($this->encoder);
$this->configurator = $configurator;
$this->hintGenerator = new HintGenerator;
$this->stylesheetCompressor = new StylesheetCompressor;
}
public function getMinifier()
{
if (!isset($this->minifier))
$this->minifier = new Noop;
return $this->minifier;
}
public function getParser(array $config = \null)
{
$this->configOptimizer->reset();
$xslt = new XSLT;
$xslt->optimizer->normalizer->remove('RemoveLivePreviewAttributes');
$this->xsl = $xslt->getXSL($this->configurator->rendering);
$this->config = (isset($config)) ? $config : $this->configurator->asConfig();
$this->config = ConfigHelper::filterConfig($this->config, 'JS');
$this->config = $this->callbackGenerator->replaceCallbacks($this->config);
$src = $this->getHints() . $this->injectConfig($this->getSource());
$src .= "if (!window['s9e']) window['s9e'] = {};\n" . $this->getExports();
$src = $this->getMinifier()->get($src);
$src = '(function(){' . $src . '})()';
return $src;
}
public function setMinifier($minifier)
{
if (\is_string($minifier))
{
$className = __NAMESPACE__ . '\\JavaScript\\Minifiers\\' . $minifier;
$args = \array_slice(\func_get_args(), 1);
if (!empty($args))
{
$reflection = new ReflectionClass($className);
$minifier = $reflection->newInstanceArgs($args);
}
else
$minifier = new $className;
}
$this->minifier = $minifier;
return $minifier;
}
protected function encode($value)
{
return $this->encoder->encode($value);
}
protected function getExports()
{
if (empty($this->exportMethods))
return '';
$methods = [];
foreach ($this->exportMethods as $method)
$methods[] = "'" . $method . "':" . $method;
return "window['s9e']['TextFormatter'] = {" . \implode(',', $methods) . '}';
}
protected function getHints()
{
$this->hintGenerator->setConfig($this->config);
$this->hintGenerator->setPlugins($this->configurator->plugins);
$this->hintGenerator->setXSL($this->xsl);
return $this->hintGenerator->getHints();
}
protected function getPluginsConfig()
{
$plugins = new Dictionary;
foreach ($this->config['plugins'] as $pluginName => $pluginConfig)
{
if (!isset($pluginConfig['js']))
continue;
$js = $pluginConfig['js'];
unset($pluginConfig['js']);
unset($pluginConfig['className']);
if (isset($pluginConfig['quickMatch']))
{
$valid = [
'[[:ascii:]]',
'[\\xC0-\\xDF][\\x80-\\xBF]',
'[\\xE0-\\xEF][\\x80-\\xBF]{2}',
'[\\xF0-\\xF7][\\x80-\\xBF]{3}'
];
$regexp = '#(?>' . \implode('|', $valid) . ')+#';
if (\preg_match($regexp, $pluginConfig['quickMatch'], $m))
$pluginConfig['quickMatch'] = $m[0];
else
unset($pluginConfig['quickMatch']);
}
$globalKeys = [
'quickMatch' => 1,
'regexp' => 1,
'regexpLimit' => 1
];
$globalConfig = \array_intersect_key($pluginConfig, $globalKeys);
$localConfig = \array_diff_key($pluginConfig, $globalKeys);
if (isset($globalConfig['regexp']) && !($globalConfig['regexp'] instanceof Code))
$globalConfig['regexp'] = new Code(RegexpConvertor::toJS($globalConfig['regexp'], \true));
$globalConfig['parser'] = new Code(
'/**
* @param {!string} text
* @param {!Array.<Array>} matches
*/
function(text, matches)
{
/** @const */
var config=' . $this->encode($localConfig) . ';
' . $js . '
}'
);
$plugins[$pluginName] = $globalConfig;
}
return $plugins;
}
protected function getRegisteredVarsConfig()
{
$registeredVars = $this->config['registeredVars'];
unset($registeredVars['cacheDir']);
return new Dictionary($registeredVars);
}
protected function getRootContext()
{
return $this->config['rootContext'];
}
protected function getSource()
{
$rootDir = __DIR__ . '/..';
$src = '';
$logger = (\in_array('getLogger', $this->exportMethods)) ? 'Logger.js' : 'NullLogger.js';
$files = \glob($rootDir . '/Parser/AttributeFilters/*.js');
$files[] = $rootDir . '/Parser/utils.js';
$files[] = $rootDir . '/Parser/' . $logger;
$files[] = $rootDir . '/Parser/Tag.js';
$files[] = $rootDir . '/Parser.js';
if (\in_array('preview', $this->exportMethods, \true))
{
$files[] = $rootDir . '/render.js';
$src .= '/** @const */ var xsl=' . $this->getStylesheet() . ";\n";
}
$src .= \implode("\n", \array_map('file_get_contents', $files));
return $src;
}
protected function getStylesheet()
{
return $this->stylesheetCompressor->encode($this->xsl);
}
protected function getTagsConfig()
{
$tags = new Dictionary;
foreach ($this->config['tags'] as $tagName => $tagConfig)
{
if (isset($tagConfig['attributes']))
$tagConfig['attributes'] = new Dictionary($tagConfig['attributes']);
$tags[$tagName] = $tagConfig;
}
return $tags;
}
protected function injectConfig($src)
{
$config = \array_map(
[$this, 'encode'],
$this->configOptimizer->optimize(
[
'plugins' => $this->getPluginsConfig(),
'registeredVars' => $this->getRegisteredVarsConfig(),
'rootContext' => $this->getRootContext(),
'tagsConfig' => $this->getTagsConfig()
]
)
);
$src = \preg_replace_callback(
'/(\\nvar (' . \implode('|', \array_keys($config)) . '))(;)/',
function ($m) use ($config)
{
return $m[1] . '=' . $config[$m[2]] . $m[3];
},
$src
);
$src = $this->configOptimizer->getVarDeclarations() . $src;
return $src;
}
}

View File

@ -0,0 +1,84 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\JavaScript;
class CallbackGenerator
{
public $callbacks = [
'tags.*.attributes.*.filterChain.*' => [
'attrValue' => '*',
'attrName' => '!string'
],
'tags.*.attributes.*.generator' => [
'attrName' => '!string'
],
'tags.*.filterChain.*' => [
'tag' => '!Tag',
'tagConfig' => '!Object'
]
];
protected $encoder;
public function __construct()
{
$this->encoder = new Encoder;
}
public function replaceCallbacks(array $config)
{
foreach ($this->callbacks as $path => $params)
$config = $this->mapArray($config, \explode('.', $path), $params);
return $config;
}
protected function buildCallbackArguments(array $params, array $localVars)
{
unset($params['parser']);
$localVars += ['logger' => 1, 'openTags' => 1, 'registeredVars' => 1, 'text' => 1];
$args = [];
foreach ($params as $k => $v)
if (isset($v))
$args[] = $this->encoder->encode($v);
elseif (isset($localVars[$k]))
$args[] = $k;
else
$args[] = 'registeredVars[' . \json_encode($k) . ']';
return \implode(',', $args);
}
protected function generateFunction(array $config, array $params)
{
if ($config['js'] == 'returnFalse' || $config['js'] == 'returnTrue')
return new Code((string) $config['js']);
$config += ['params' => []];
$src = $this->getHeader($params);
$src .= 'function(' . \implode(',', \array_keys($params)) . '){';
$src .= 'return ' . $this->parenthesizeCallback($config['js']);
$src .= '(' . $this->buildCallbackArguments($config['params'], $params) . ');}';
return new Code($src);
}
protected function getHeader(array $params)
{
$header = "/**\n";
foreach ($params as $paramName => $paramType)
$header .= '* @param {' . $paramType . '} ' . $paramName . "\n";
$header .= "*/\n";
return $header;
}
protected function mapArray(array $array, array $path, array $params)
{
$key = \array_shift($path);
$keys = ($key === '*') ? \array_keys($array) : [$key];
foreach ($keys as $key)
{
if (!isset($array[$key]))
continue;
$array[$key] = (empty($path)) ? $this->generateFunction($array[$key], $params) : $this->mapArray($array[$key], $path, $params);
}
return $array;
}
protected function parenthesizeCallback($callback)
{
return (\preg_match('(^[.\\w]+$)D', $callback)) ? $callback : '(' . $callback . ')';
}
}

View File

@ -0,0 +1,95 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\JavaScript;
class ConfigOptimizer
{
protected $configValues;
protected $encoder;
protected $jsLengths;
public function __construct(Encoder $encoder)
{
$this->encoder = $encoder;
$this->reset();
}
public function getVarDeclarations()
{
\asort($this->jsLengths);
$src = '';
foreach (\array_keys($this->jsLengths) as $varName)
{
$configValue = $this->configValues[$varName];
if ($configValue->isDeduplicated())
$src .= '/** @const */ var ' . $varName . '=' . $this->encoder->encode($configValue->getValue()) . ";\n";
}
return $src;
}
public function optimize($object)
{
return \current($this->optimizeObjectContent([$object]))->getValue();
}
public function reset()
{
$this->configValues = [];
$this->jsLengths = [];
}
protected function canDeduplicate($value)
{
if (\is_array($value) || $value instanceof Dictionary)
return (bool) \count($value);
return ($value instanceof Code);
}
protected function deduplicateConfigValues()
{
\arsort($this->jsLengths);
foreach (\array_keys($this->jsLengths) as $varName)
{
$configValue = $this->configValues[$varName];
if ($configValue->getUseCount() > 1)
$configValue->deduplicate();
}
}
protected function getVarName($js)
{
return \sprintf('o%08X', \crc32($js));
}
protected function isIterable($value)
{
return (\is_array($value) || $value instanceof Dictionary);
}
protected function optimizeObjectContent($object)
{
$object = $this->recordObject($object);
$this->deduplicateConfigValues();
return $object->getValue();
}
protected function recordObject($object)
{
$js = $this->encoder->encode($object);
$varName = $this->getVarName($js);
if ($this->isIterable($object))
$object = $this->recordObjectContent($object);
if (!isset($this->configValues[$varName]))
{
$this->configValues[$varName] = new ConfigValue($object, $varName);
$this->jsLengths[$varName] = \strlen($js);
}
$this->configValues[$varName]->incrementUseCount();
return $this->configValues[$varName];
}
protected function recordObjectContent($object)
{
foreach ($object as $k => $v)
if ($this->canDeduplicate($v) && !$this->shouldPreserve($v))
$object[$k] = $this->recordObject($v);
return $object;
}
protected function shouldPreserve($value)
{
return ($value instanceof Code && \preg_match('(^\\w+$)', $value));
}
}

View File

@ -0,0 +1,56 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\JavaScript;
class ConfigValue
{
protected $isDeduplicated = \false;
protected $name;
protected $useCount = 0;
protected $value;
protected $varName;
public function __construct($value, $varName)
{
$this->value = $value;
$this->varName = $varName;
}
public function deduplicate()
{
if ($this->useCount > 1)
{
$this->isDeduplicated = \true;
$this->decrementUseCount($this->useCount - 1);
}
}
public function getUseCount()
{
return $this->useCount;
}
public function getValue()
{
return $this->value;
}
public function getVarName()
{
return $this->varName;
}
public function incrementUseCount()
{
++$this->useCount;
}
public function isDeduplicated()
{
return $this->isDeduplicated;
}
protected function decrementUseCount($step = 1)
{
$this->useCount -= $step;
foreach ($this->value as $value)
if ($value instanceof ConfigValue)
$value->decrementUseCount($step);
}
}

View File

@ -0,0 +1,21 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\JavaScript;
use ArrayObject;
use s9e\TextFormatter\Configurator\FilterableConfigValue;
use s9e\TextFormatter\Configurator\Helpers\ConfigHelper;
class Dictionary extends ArrayObject implements FilterableConfigValue
{
public function filterConfig($target)
{
$value = $this->getArrayCopy();
if ($target === 'JS')
$value = new Dictionary(ConfigHelper::filterConfig($value, $target));
return $value;
}
}

View File

@ -0,0 +1,114 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\JavaScript;
use RuntimeException;
use s9e\TextFormatter\Configurator\Items\Regexp;
use s9e\TextFormatter\Configurator\JavaScript\Code;
use s9e\TextFormatter\Configurator\JavaScript\Dictionary;
use s9e\TextFormatter\Configurator\JavaScript\Encoder;
class Encoder
{
public $objectEncoders;
public $typeEncoders;
public function __construct()
{
$ns = 's9e\\TextFormatter\\Configurator\\';
$this->objectEncoders = [
$ns . 'Items\\Regexp' => [$this, 'encodeRegexp'],
$ns . 'JavaScript\\Code' => [$this, 'encodeCode'],
$ns . 'JavaScript\\ConfigValue' => [$this, 'encodeConfigValue'],
$ns . 'JavaScript\\Dictionary' => [$this, 'encodeDictionary']
];
$this->typeEncoders = [
'array' => [$this, 'encodeArray'],
'boolean' => [$this, 'encodeBoolean'],
'double' => [$this, 'encodeScalar'],
'integer' => [$this, 'encodeScalar'],
'object' => [$this, 'encodeObject'],
'string' => [$this, 'encodeScalar']
];
}
public function encode($value)
{
$type = \gettype($value);
if (!isset($this->typeEncoders[$type]))
throw new RuntimeException('Cannot encode ' . $type . ' value');
return $this->typeEncoders[$type]($value);
}
protected function encodeArray(array $array)
{
return ($this->isIndexedArray($array)) ? $this->encodeIndexedArray($array) : $this->encodeAssociativeArray($array);
}
protected function encodeAssociativeArray(array $array, $preserveNames = \false)
{
\ksort($array);
$src = '{';
$sep = '';
foreach ($array as $k => $v)
{
$src .= $sep . $this->encodePropertyName("$k", $preserveNames) . ':' . $this->encode($v);
$sep = ',';
}
$src .= '}';
return $src;
}
protected function encodeBoolean($value)
{
return ($value) ? '!0' : '!1';
}
protected function encodeCode(Code $code)
{
return (string) $code;
}
protected function encodeConfigValue(ConfigValue $configValue)
{
return ($configValue->isDeduplicated()) ? $configValue->getVarName() : $this->encode($configValue->getValue());
}
protected function encodeDictionary(Dictionary $dict)
{
return $this->encodeAssociativeArray($dict->getArrayCopy(), \true);
}
protected function encodeIndexedArray(array $array)
{
return '[' . \implode(',', \array_map([$this, 'encode'], $array)) . ']';
}
protected function encodeObject($object)
{
foreach ($this->objectEncoders as $className => $callback)
if ($object instanceof $className)
return $callback($object);
throw new RuntimeException('Cannot encode instance of ' . \get_class($object));
}
protected function encodePropertyName($name, $preserveNames)
{
return ($preserveNames || !$this->isLegalProp($name)) ? \json_encode($name) : $name;
}
protected function encodeRegexp(Regexp $regexp)
{
return $regexp->getJS();
}
protected function encodeScalar($value)
{
return \json_encode($value);
}
protected function isIndexedArray(array $array)
{
if (empty($array))
return \true;
if (isset($array[0]) && \array_keys($array) === \range(0, \count($array) - 1))
return \true;
return \false;
}
protected function isLegalProp($name)
{
$reserved = ['abstract', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'double', 'else', 'enum', 'export', 'extends', 'false', 'final', 'finally', 'float', 'for', 'function', 'goto', 'if', 'implements', 'import', 'in', 'instanceof', 'int', 'interface', 'let', 'long', 'native', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'short', 'static', 'super', 'switch', 'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 'try', 'typeof', 'var', 'void', 'volatile', 'while', 'with'];
if (\in_array($name, $reserved, \true))
return \false;
return (bool) \preg_match('#^(?![0-9])[$_\\pL][$_\\pL\\pNl]+$#Du', $name);
}
}

View File

@ -0,0 +1,93 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\JavaScript;
use ReflectionClass;
use s9e\TextFormatter\Configurator\Collections\PluginCollection;
class HintGenerator
{
protected $config;
protected $hints;
protected $plugins;
protected $xsl;
public function getHints()
{
$this->hints = [];
$this->setPluginsHints();
$this->setRenderingHints();
$this->setRulesHints();
$this->setTagsHints();
$js = "/** @const */ var HINT={};\n";
\ksort($this->hints);
foreach ($this->hints as $hintName => $hintValue)
$js .= '/** @const */ HINT.' . $hintName . '=' . \json_encode($hintValue) . ";\n";
return $js;
}
public function setConfig(array $config)
{
$this->config = $config;
}
public function setPlugins(PluginCollection $plugins)
{
$this->plugins = $plugins;
}
public function setXSL($xsl)
{
$this->xsl = $xsl;
}
protected function setPluginsHints()
{
foreach ($this->plugins as $plugins)
$this->hints += $plugins->getJSHints();
}
protected function setRenderingHints()
{
$this->hints['postProcessing'] = (int) (\strpos($this->xsl, 'data-s9e-livepreview-postprocess') !== \false);
$this->hints['ignoreAttrs'] = (int) (\strpos($this->xsl, 'data-s9e-livepreview-ignore-attrs') !== \false);
}
protected function setRulesHints()
{
$this->hints['closeAncestor'] = 0;
$this->hints['closeParent'] = 0;
$this->hints['createChild'] = 0;
$this->hints['fosterParent'] = 0;
$this->hints['requireAncestor'] = 0;
$flags = 0;
foreach ($this->config['tags'] as $tagConfig)
{
foreach (\array_intersect_key($tagConfig['rules'], $this->hints) as $k => $v)
$this->hints[$k] = 1;
$flags |= $tagConfig['rules']['flags'];
}
$flags |= $this->config['rootContext']['flags'];
$parser = new ReflectionClass('s9e\\TextFormatter\\Parser');
foreach ($parser->getConstants() as $constName => $constValue)
if (\substr($constName, 0, 5) === 'RULE_')
$this->hints[$constName] = ($flags & $constValue) ? 1 : 0;
}
protected function setTagAttributesHints(array $tagConfig)
{
if (empty($tagConfig['attributes']))
return;
foreach ($tagConfig['attributes'] as $attrConfig)
{
$this->hints['attributeGenerator'] |= isset($attrConfig['generator']);
$this->hints['attributeDefaultValue'] |= isset($attrConfig['defaultValue']);
}
}
protected function setTagsHints()
{
$this->hints['attributeGenerator'] = 0;
$this->hints['attributeDefaultValue'] = 0;
$this->hints['namespaces'] = 0;
foreach ($this->config['tags'] as $tagName => $tagConfig)
{
$this->hints['namespaces'] |= (\strpos($tagName, ':') !== \false);
$this->setTagAttributesHints($tagConfig);
}
}
}

View File

@ -0,0 +1,41 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\JavaScript;
use Exception;
abstract class Minifier
{
public $cacheDir;
public $keepGoing = \false;
abstract public function minify($src);
public function get($src)
{
try
{
return (isset($this->cacheDir)) ? $this->getFromCache($src) : $this->minify($src);
}
catch (Exception $e)
{
if (!$this->keepGoing)
throw $e;
}
return $src;
}
public function getCacheDifferentiator()
{
return '';
}
protected function getFromCache($src)
{
$differentiator = $this->getCacheDifferentiator();
$key = \sha1(\serialize([\get_class($this), $differentiator, $src]));
$cacheFile = $this->cacheDir . '/minifier.' . $key . '.js';
if (!\file_exists($cacheFile))
\file_put_contents($cacheFile, $this->minify($src));
return \file_get_contents($cacheFile);
}
}

View File

@ -0,0 +1,79 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\JavaScript\Minifiers;
use RuntimeException;
use s9e\TextFormatter\Configurator\JavaScript\Minifier;
class ClosureCompilerApplication extends Minifier
{
public $closureCompilerBin;
public $compilationLevel = 'ADVANCED_OPTIMIZATIONS';
public $excludeDefaultExterns = \true;
public $javaBin = 'java';
public $options = '--use_types_for_optimization';
public function __construct($filepath = \null)
{
if (isset($filepath))
{
$this->closureCompilerBin = $filepath;
$this->testFilepaths();
}
}
public function getCacheDifferentiator()
{
$key = [
$this->compilationLevel,
$this->excludeDefaultExterns,
$this->options,
$this->getClosureCompilerBinHash()
];
if ($this->excludeDefaultExterns)
$key[] = \file_get_contents(__DIR__ . '/../externs.application.js');
return $key;
}
public function minify($src)
{
$this->testFilepaths();
$options = ($this->options) ? ' ' . $this->options : '';
if ($this->excludeDefaultExterns && $this->compilationLevel === 'ADVANCED_OPTIMIZATIONS')
$options .= ' --externs ' . __DIR__ . '/../externs.application.js --env=CUSTOM';
$crc = \crc32($src);
$inFile = \sys_get_temp_dir() . '/' . $crc . '.js';
$outFile = \sys_get_temp_dir() . '/' . $crc . '.min.js';
\file_put_contents($inFile, $src);
$cmd = \escapeshellcmd($this->javaBin)
. ' -jar ' . \escapeshellarg($this->closureCompilerBin)
. ' --compilation_level ' . \escapeshellarg($this->compilationLevel)
. $options
. ' --js ' . \escapeshellarg($inFile)
. ' --js_output_file ' . \escapeshellarg($outFile);
\exec($cmd . ' 2>&1', $output, $return);
\unlink($inFile);
if (\file_exists($outFile))
{
$src = \trim(\file_get_contents($outFile));
\unlink($outFile);
}
if (!empty($return))
throw new RuntimeException('An error occured during minification: ' . \implode("\n", $output));
return $src;
}
protected function getClosureCompilerBinHash()
{
static $cache = [];
if (!isset($cache[$this->closureCompilerBin]))
$cache[$this->closureCompilerBin] = \md5_file($this->closureCompilerBin);
return $cache[$this->closureCompilerBin];
}
protected function testFilepaths()
{
if (!isset($this->closureCompilerBin))
throw new RuntimeException('No path set for Closure Compiler');
if (!\file_exists($this->closureCompilerBin))
throw new RuntimeException('Cannot find Closure Compiler at ' . $this->closureCompilerBin);
}
}

View File

@ -0,0 +1,88 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\JavaScript\Minifiers;
use RuntimeException;
use s9e\TextFormatter\Configurator\JavaScript\OnlineMinifier;
class ClosureCompilerService extends OnlineMinifier
{
public $compilationLevel = 'ADVANCED_OPTIMIZATIONS';
public $excludeDefaultExterns = \true;
public $externs;
public $url = 'https://closure-compiler.appspot.com/compile';
public function __construct()
{
parent::__construct();
$this->externs = \file_get_contents(__DIR__ . '/../externs.service.js');
}
public function getCacheDifferentiator()
{
$key = [$this->compilationLevel, $this->excludeDefaultExterns];
if ($this->excludeDefaultExterns)
$key[] = $this->externs;
return $key;
}
public function minify($src)
{
$body = $this->generateRequestBody($src);
$response = $this->query($body);
if ($response === \false)
throw new RuntimeException('Could not contact the Closure Compiler service');
return $this->decodeResponse($response);
}
protected function decodeResponse($response)
{
$response = \json_decode($response, \true);
if (\is_null($response))
{
$msgs = array(
\JSON_ERROR_NONE => 'No error',
\JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
\JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)',
\JSON_ERROR_CTRL_CHAR => 'Control character error, possibly incorrectly encoded',
\JSON_ERROR_SYNTAX => 'Syntax error',
\JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded'
);
throw new RuntimeException('Closure Compiler service returned invalid JSON: ' . (isset($msgs[\json_last_error()]) ? $msgs[\json_last_error()] : 'Unknown error'));
}
if (isset($response['serverErrors'][0]))
{
$error = $response['serverErrors'][0];
throw new RuntimeException('Server error ' . $error['code'] . ': ' . $error['error']);
}
if (isset($response['errors'][0]))
{
$error = $response['errors'][0];
throw new RuntimeException('Compilation error: ' . $error['error']);
}
return $response['compiledCode'];
}
protected function generateRequestBody($src)
{
$params = [
'compilation_level' => $this->compilationLevel,
'js_code' => $src,
'output_format' => 'json',
'output_info' => 'compiled_code'
];
if ($this->excludeDefaultExterns && $this->compilationLevel === 'ADVANCED_OPTIMIZATIONS')
{
$params['exclude_default_externs'] = 'true';
$params['js_externs'] = $this->externs;
}
$body = \http_build_query($params) . '&output_info=errors';
return $body;
}
protected function query($body)
{
return $this->httpClient->post(
$this->url,
['Content-Type: application/x-www-form-urlencoded'],
$body
);
}
}

View File

@ -0,0 +1,38 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\JavaScript\Minifiers;
use ArrayAccess;
use Exception;
use RuntimeException;
use s9e\TextFormatter\Configurator\Collections\MinifierList;
use s9e\TextFormatter\Configurator\JavaScript\Minifier;
use s9e\TextFormatter\Configurator\Traits\CollectionProxy;
class FirstAvailable extends Minifier implements ArrayAccess
{
use CollectionProxy;
protected $collection;
public function __construct()
{
$this->collection = new MinifierList;
foreach (\func_get_args() as $minifier)
$this->collection->add($minifier);
}
public function minify($src)
{
foreach ($this->collection as $minifier)
try
{
return $minifier->minify($src);
}
catch (Exception $e)
{
}
throw new RuntimeException('No minifier available');
}
}

View File

@ -0,0 +1,18 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\JavaScript\Minifiers;
use MatthiasMullie\Minify;
use s9e\TextFormatter\Configurator\JavaScript\Minifier;
class MatthiasMullieMinify extends Minifier
{
public function minify($src)
{
$minifier = new Minify\JS($src);
return $minifier->minify();
}
}

View File

@ -0,0 +1,16 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\JavaScript\Minifiers;
use s9e\TextFormatter\Configurator\JavaScript\Minifier;
class Noop extends Minifier
{
public function minify($src)
{
return $src;
}
}

View File

@ -0,0 +1,17 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\JavaScript;
use s9e\TextFormatter\Utils\Http;
abstract class OnlineMinifier extends Minifier
{
public $httpClient;
public function __construct()
{
$this->httpClient = Http::getClient();
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,111 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\JavaScript;
use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
use s9e\TextFormatter\Configurator\JavaScript\Code;
class StylesheetCompressor
{
protected $deduplicateTargets = [
'<xsl:template match="',
'</xsl:template>',
'<xsl:apply-templates/>',
'<param name="allowfullscreen" value="true"/>',
'<xsl:value-of select="',
'<xsl:copy-of select="@',
'<iframe allowfullscreen="" scrolling="no"',
'display:block;overflow:hidden;position:relative;padding-bottom:',
'display:inline-block;width:100%;max-width:',
' [-:\\w]++="',
'\\{[^}]++\\}',
'@[-\\w]{4,}+',
'(?<=<)[-:\\w]{4,}+',
'(?<==")[^"]{4,}+"'
];
protected $dictionary;
protected $keyPrefix = '$';
public $minSaving = 10;
protected $savings;
protected $xsl;
public function encode($xsl)
{
$this->xsl = $xsl;
$this->estimateSavings();
$this->filterSavings();
$this->buildDictionary();
$js = \json_encode($this->getCompressedStylesheet());
if (!empty($this->dictionary))
$js .= '.replace(' . $this->getReplacementRegexp() . ',function(k){return' . \json_encode($this->dictionary) . '[k]})';
return $js;
}
protected function buildDictionary()
{
$keys = $this->getAvailableKeys();
\rsort($keys);
$this->dictionary = [];
\arsort($this->savings);
foreach (\array_keys($this->savings) as $str)
{
$key = \array_pop($keys);
if (!$key)
break;
$this->dictionary[$key] = $str;
}
}
protected function estimateSavings()
{
$this->savings = [];
foreach ($this->getStringsFrequency() as $str => $cnt)
{
$len = \strlen($str);
$originalCost = $cnt * $len;
$replacementCost = $cnt * 2;
$overhead = $len + 6;
$this->savings[$str] = $originalCost - ($replacementCost + $overhead);
}
}
protected function filterSavings()
{
$this->savings = \array_filter(
$this->savings,
function ($saving)
{
return ($saving >= $this->minSaving);
}
);
}
protected function getAvailableKeys()
{
return \array_diff($this->getPossibleKeys(), $this->getUnavailableKeys());
}
protected function getCompressedStylesheet()
{
return \strtr($this->xsl, \array_flip($this->dictionary));
}
protected function getPossibleKeys()
{
$keys = [];
foreach (\range('a', 'z') as $char)
$keys[] = $this->keyPrefix . $char;
return $keys;
}
protected function getReplacementRegexp()
{
return '/' . RegexpBuilder::fromList(\array_keys($this->dictionary)) . '/g';
}
protected function getStringsFrequency()
{
$regexp = '(' . \implode('|', $this->deduplicateTargets) . ')S';
\preg_match_all($regexp, $this->xsl, $matches);
return \array_count_values($matches[0]);
}
protected function getUnavailableKeys()
{
\preg_match_all('(' . \preg_quote($this->keyPrefix) . '.)', $this->xsl, $matches);
return \array_unique($matches[0]);
}
}

View File

@ -0,0 +1,227 @@
/*
* Copyright 2008 The Closure Compiler Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// This file was auto-generated.
// See https://github.com/google/closure-compiler for the original source.
// See https://github.com/s9e/TextFormatter/blob/master/scripts/generateExterns.php for details.
/**
* @const
*/
var punycode = {};
/**
* @param {string} domain
* @return {string}
*/
punycode.toASCII;
/** @constructor */
function XSLTProcessor() {}
/**
* @type {string}
* @implicitCast
*/
Element.prototype.innerHTML;
/**
* @constructor
*/
function DOMParser() {}
/**
* @param {string} src The UTF16 string to be parsed.
* @param {string} type The content type of the string.
* @return {Document}
*/
DOMParser.prototype.parseFromString = function(src, type) {};
/**
* @type {!Window}
*/
var window;
/**
* @constructor
* @extends {Node}
*/
function Document() {}
/**
* @return {!DocumentFragment}
* @nosideeffects
*/
Document.prototype.createDocumentFragment = function() {};
/**
* @param {string} tagName
* @param {string=} opt_typeExtension
* @return {!Element}
* @nosideeffects
*/
Document.prototype.createElement = function(tagName, opt_typeExtension) {};
/**
* @constructor
* @extends {Node}
*/
function DocumentFragment() {}
/**
* @constructor
* @implements {IObject<(string|number), T>}
* @implements {IArrayLike<T>}
* @implements {Iterable<T>}
* @template T
*/
function NamedNodeMap() {}
/**
* @param {number} index
* @return {Node}
* @nosideeffects
*/
NamedNodeMap.prototype.item = function(index) {};
/**
* @type {number}
*/
NamedNodeMap.prototype.length;
/**
* @constructor
*/
function Node() {}
/**
* @param {Node} newChild
* @return {!Node}
*/
Node.prototype.appendChild = function(newChild) {};
/**
* @type {!NodeList<!Node>}
*/
Node.prototype.childNodes;
/**
* @param {boolean} deep
* @return {!Node}
* @nosideeffects
*/
Node.prototype.cloneNode = function(deep) {};
/**
* @type {Node}
*/
Node.prototype.firstChild;
/**
* @param {Node} newChild
* @param {Node} refChild
* @return {!Node}
*/
Node.prototype.insertBefore = function(newChild, refChild) {};
/**
* @type {string}
*/
Node.prototype.nodeName;
/**
* @type {number}
*/
Node.prototype.nodeType;
/**
* @type {string}
*/
Node.prototype.nodeValue;
/**
* @type {Document}
*/
Node.prototype.ownerDocument;
/**
* @type {Node}
*/
Node.prototype.parentNode;
/**
* @param {Node} oldChild
* @return {!Node}
*/
Node.prototype.removeChild = function(oldChild) {};
/**
* @constructor
* @implements {IArrayLike<T>}
* @implements {Iterable<T>}
* @template T
*/
function NodeList() {}
/**
* @type {number}
*/
NodeList.prototype.length;
/**
* @constructor
* @extends {Node}
*/
function Element() {}
/**
* @constructor
*/
function Window() {}
/**
* @param {Node} externalNode
* @param {boolean} deep
* @return {Node}
*/
Document.prototype.importNode = function(externalNode, deep) {};
/**
* @constructor
* @extends {Document}
*/
function HTMLDocument() {}
/**
* @constructor
* @extends {Element}
*/
function HTMLElement() {}
/**
* @param {?string} namespaceURI
* @param {string} localName
* @return {string}
* @nosideeffects
*/
Element.prototype.getAttributeNS = function(namespaceURI, localName) {};
/**
* @param {?string} namespaceURI
* @param {string} localName
* @return {boolean}
* @nosideeffects
*/
Element.prototype.hasAttributeNS = function(namespaceURI, localName) {};
/**
* @param {?string} namespaceURI
* @param {string} localName
* @return {undefined}
*/
Element.prototype.removeAttributeNS = function(namespaceURI, localName) {};
/**
* @param {?string} namespaceURI
* @param {string} qualifiedName
* @param {string|number|boolean} value Values are converted to strings with
* @return {undefined}
*/
Element.prototype.setAttributeNS = function(namespaceURI, qualifiedName, value) {};
/**
* @param {Node} arg
* @return {boolean}
* @nosideeffects
*/
Node.prototype.isEqualNode = function(arg) {};
/**
* @type {string}
*/
Node.prototype.namespaceURI;
/**
* @type {string}
* @implicitCast
*/
Node.prototype.textContent;
/**
* @type {!HTMLDocument}
* @const
*/
var document;

View File

@ -0,0 +1,553 @@
/*
* Copyright 2008 The Closure Compiler Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// This file was auto-generated.
// See https://github.com/google/closure-compiler for the original source.
// See https://github.com/s9e/TextFormatter/blob/master/scripts/generateExterns.php for details.
/**
* @const
*/
var punycode = {};
/**
* @param {string} domain
* @return {string}
*/
punycode.toASCII;
/** @constructor */
function XSLTProcessor() {}
/**
* @type {number}
* @const
*/
var Infinity;
/**
* @type {undefined}
* @const
*/
var undefined;
/** @typedef {?} */
var symbol;
/**
* @param {string=} opt_description
* @return {symbol}
*/
function Symbol(opt_description) {}
/**
* @param {string} uri
* @return {string}
* @throws {URIError} when used wrongly.
*/
function decodeURIComponent(uri) {}
/**
* @param {string} uri
* @return {string}
* @throws {URIError} if one attempts to encode a surrogate which is not part of
*/
function encodeURIComponent(uri) {}
/**
* @param {string} str
* @return {string}
* @nosideeffects
*/
function escape(str) {}
/**
* @param {*} num
* @return {boolean}
* @nosideeffects
*/
function isNaN(num) {}
/**
* @param {*} num
* @param {number|undefined} base
* @return {number}
* @nosideeffects
*/
function parseInt(num, base) {}
/**
* @constructor
* @implements {IArrayLike<T>}
* @implements {Iterable<T>}
* @param {...*} var_args
* @return {!Array<?>}
* @nosideeffects
* @template T
*/
function Array(var_args) {}
/**
* @param {?function(this:S, T, number, !Array<T>): ?} callback
* @param {S=} opt_thisobj
* @this {IArrayLike<T>|string}
* @template T,S
* @return {undefined}
*/
Array.prototype.forEach = function(callback, opt_thisobj) {};
/**
* @param {T} obj
* @param {number=} opt_fromIndex
* @return {number}
* @this {IArrayLike<T>|string}
* @nosideeffects
* @template T
*/
Array.prototype.indexOf = function(obj, opt_fromIndex) {};
/**
* @param {*=} opt_separator Specifies a string to separate each element of the
* @return {string}
* @this {IArrayLike<?>|string}
* @nosideeffects
*/
Array.prototype.join = function(opt_separator) {};
/**
* @type {number}
*/
Array.prototype.length;
/**
* @return {T}
* @this {IArrayLike<T>}
* @modifies {this}
* @template T
*/
Array.prototype.pop = function() {};
/**
* @param {...T} var_args
* @return {number} The new length of the array.
* @this {IArrayLike<T>}
* @template T
* @modifies {this}
*/
Array.prototype.push = function(var_args) {};
/**
* @return {THIS} A reference to the original modified array.
* @this {THIS}
* @template THIS
* @modifies {this}
*/
Array.prototype.reverse = function() {};
/**
* @this {IArrayLike<T>}
* @modifies {this}
* @return {T}
* @template T
*/
Array.prototype.shift = function() {};
/**
* @param {*=} opt_begin Zero-based index at which to begin extraction. A
* @param {*=} opt_end Zero-based index at which to end extraction. slice
* @return {!Array<T>}
* @this {IArrayLike<T>|string}
* @template T
* @nosideeffects
*/
Array.prototype.slice = function(opt_begin, opt_end) {};
/**
* @param {function(T,T):number=} opt_compareFn Specifies a function that
* @this {IArrayLike<T>}
* @template T
* @modifies {this}
* @return {!Array<T>}
*/
Array.prototype.sort = function(opt_compareFn) {};
/**
* @param {*=} opt_index Index at which to start changing the array. If negative, * will begin that many elements from the end. A non-number type will be
* @param {*=} opt_howMany An integer indicating the number of old array elements
* @param {...T} var_args
* @return {!Array<T>}
* @this {IArrayLike<T>}
* @modifies {this}
* @template T
*/
Array.prototype.splice = function(opt_index, opt_howMany, var_args) {};
/**
* @param {...*} var_args
* @return {number} The new length of the array
* @this {IArrayLike<?>}
* @modifies {this}
*/
Array.prototype.unshift = function(var_args) {};
/**
* @param {?=} opt_yr_num
* @param {?=} opt_mo_num
* @param {?=} opt_day_num
* @param {?=} opt_hr_num
* @param {?=} opt_min_num
* @param {?=} opt_sec_num
* @param {?=} opt_ms_num
* @constructor
* @return {string}
* @nosideeffects
*/
function Date(opt_yr_num, opt_mo_num, opt_day_num, opt_hr_num, opt_min_num, opt_sec_num, opt_ms_num) {}
/**
* @param {*} date
* @return {number}
* @nosideeffects
*/
Date.parse = function(date) {};
/**
* @constructor
* @param {...*} var_args
* @throws {Error}
*/
function Function(var_args) {}
/**
* @const
*/
var Math = {};
/**
* @param {?} x
* @return {number}
* @nosideeffects
*/
Math.floor = function(x) {};
/**
* @param {...?} var_args
* @return {number}
* @nosideeffects
*/
Math.max = function(var_args) {};
/**
* @param {...?} var_args
* @return {number}
* @nosideeffects
*/
Math.min = function(var_args) {};
/**
* @return {number}
* @nosideeffects
*/
Math.random = function() {};
/**
* @constructor
* @param {*=} opt_value
* @return {number}
* @nosideeffects
*/
function Number(opt_value) {}
/**
* @this {Number|number}
* @param {(number|Number)=} opt_radix An optional radix.
* @return {string}
* @nosideeffects
* @override
*/
Number.prototype.toString = function(opt_radix) {};
/**
* @constructor
* @param {*=} opt_value
* @return {!Object}
* @nosideeffects
*/
function Object(opt_value) {}
/**
* @this {*}
* @return {string}
* @nosideeffects
*/
Object.prototype.toString = function() {};
/**
* @constructor
* @param {*=} opt_pattern
* @param {*=} opt_flags
* @return {!RegExp}
* @throws {SyntaxError} if opt_pattern is an invalid pattern.
*/
function RegExp(opt_pattern, opt_flags) {}
/**
* @param {*} str The string to search.
* @return {Array<string>} This should really return an Array with a few
*/
RegExp.prototype.exec = function(str) {};
/**
* @type {number}
*/
RegExp.prototype.lastIndex;
/**
* @param {*} str The string to search.
* @return {boolean} Whether the string was matched.
*/
RegExp.prototype.test = function(str) {};
/**
* @constructor
* @implements {Iterable<string>}
* @param {*=} opt_str
* @return {string}
* @nosideeffects
*/
function String(opt_str) {}
/**
* @param {...number} var_args
* @return {string}
* @nosideeffects
*/
String.fromCharCode = function(var_args) {};
/**
* @this {String|string}
* @param {number} index
* @return {string}
* @nosideeffects
*/
String.prototype.charAt = function(index) {};
/**
* @this {String|string}
* @param {number=} opt_index
* @return {number}
* @nosideeffects
*/
String.prototype.charCodeAt = function(opt_index) {};
/**
* @this {String|string}
* @param {string|null} searchValue
* @param {(number|null)=} opt_fromIndex
* @return {number}
* @nosideeffects
*/
String.prototype.indexOf = function(searchValue, opt_fromIndex) {};
/**
* @type {number}
*/
String.prototype.length;
/**
* @this {String|string}
* @param {RegExp|string} pattern
* @param {string|Function} replacement
* @return {string}
*/
String.prototype.replace = function(pattern, replacement) {};
/**
* @this {String|string}
* @param {*=} opt_separator
* @param {number=} opt_limit
* @return {!Array<string>}
* @nosideeffects
*/
String.prototype.split = function(opt_separator, opt_limit) {};
/**
* @this {String|string}
* @param {number} start
* @param {number=} opt_length
* @return {string} The specified substring.
* @nosideeffects
*/
String.prototype.substr = function(start, opt_length) {};
/**
* @this {String|string}
* @return {string}
* @nosideeffects
*/
String.prototype.toLowerCase = function() {};
/**
* @this {String|string}
* @return {string}
* @nosideeffects
*/
String.prototype.toUpperCase = function() {};
/**
* @type {string}
* @implicitCast
*/
Element.prototype.innerHTML;
/**
* @constructor
*/
function DOMParser() {}
/**
* @param {string} src The UTF16 string to be parsed.
* @param {string} type The content type of the string.
* @return {Document}
*/
DOMParser.prototype.parseFromString = function(src, type) {};
/**
* @type {!Window}
*/
var window;
/**
* @constructor
* @extends {Node}
*/
function Document() {}
/**
* @return {!DocumentFragment}
* @nosideeffects
*/
Document.prototype.createDocumentFragment = function() {};
/**
* @param {string} tagName
* @param {string=} opt_typeExtension
* @return {!Element}
* @nosideeffects
*/
Document.prototype.createElement = function(tagName, opt_typeExtension) {};
/**
* @constructor
* @extends {Node}
*/
function DocumentFragment() {}
/**
* @constructor
* @implements {IObject<(string|number), T>}
* @implements {IArrayLike<T>}
* @implements {Iterable<T>}
* @template T
*/
function NamedNodeMap() {}
/**
* @param {number} index
* @return {Node}
* @nosideeffects
*/
NamedNodeMap.prototype.item = function(index) {};
/**
* @type {number}
*/
NamedNodeMap.prototype.length;
/**
* @constructor
*/
function Node() {}
/**
* @param {Node} newChild
* @return {!Node}
*/
Node.prototype.appendChild = function(newChild) {};
/**
* @type {!NodeList<!Node>}
*/
Node.prototype.childNodes;
/**
* @param {boolean} deep
* @return {!Node}
* @nosideeffects
*/
Node.prototype.cloneNode = function(deep) {};
/**
* @type {Node}
*/
Node.prototype.firstChild;
/**
* @param {Node} newChild
* @param {Node} refChild
* @return {!Node}
*/
Node.prototype.insertBefore = function(newChild, refChild) {};
/**
* @type {string}
*/
Node.prototype.nodeName;
/**
* @type {number}
*/
Node.prototype.nodeType;
/**
* @type {string}
*/
Node.prototype.nodeValue;
/**
* @type {Document}
*/
Node.prototype.ownerDocument;
/**
* @type {Node}
*/
Node.prototype.parentNode;
/**
* @param {Node} oldChild
* @return {!Node}
*/
Node.prototype.removeChild = function(oldChild) {};
/**
* @constructor
* @implements {IArrayLike<T>}
* @implements {Iterable<T>}
* @template T
*/
function NodeList() {}
/**
* @type {number}
*/
NodeList.prototype.length;
/**
* @constructor
* @extends {Node}
*/
function Element() {}
/**
* @constructor
*/
function Window() {}
/**
* @param {Node} externalNode
* @param {boolean} deep
* @return {Node}
*/
Document.prototype.importNode = function(externalNode, deep) {};
/**
* @constructor
* @extends {Document}
*/
function HTMLDocument() {}
/**
* @constructor
* @extends {Element}
*/
function HTMLElement() {}
/**
* @param {?string} namespaceURI
* @param {string} localName
* @return {string}
* @nosideeffects
*/
Element.prototype.getAttributeNS = function(namespaceURI, localName) {};
/**
* @param {?string} namespaceURI
* @param {string} localName
* @return {boolean}
* @nosideeffects
*/
Element.prototype.hasAttributeNS = function(namespaceURI, localName) {};
/**
* @param {?string} namespaceURI
* @param {string} localName
* @return {undefined}
*/
Element.prototype.removeAttributeNS = function(namespaceURI, localName) {};
/**
* @param {?string} namespaceURI
* @param {string} qualifiedName
* @param {string|number|boolean} value Values are converted to strings with
* @return {undefined}
*/
Element.prototype.setAttributeNS = function(namespaceURI, qualifiedName, value) {};
/**
* @param {Node} arg
* @return {boolean}
* @nosideeffects
*/
Node.prototype.isEqualNode = function(arg) {};
/**
* @type {string}
*/
Node.prototype.namespaceURI;
/**
* @type {string}
* @implicitCast
*/
Node.prototype.textContent;
/**
* @type {!HTMLDocument}
* @const
*/
var document;

View File

@ -0,0 +1,18 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\RendererGenerators;
use s9e\TextFormatter\Configurator\RendererGenerator;
use s9e\TextFormatter\Configurator\Rendering;
use s9e\TextFormatter\Renderers\Unformatted as UnformattedRenderer;
class Unformatted implements RendererGenerator
{
public function getRenderer(Rendering $rendering)
{
return new UnformattedRenderer;
}
}

View File

@ -0,0 +1,67 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\RendererGenerators;
use s9e\TextFormatter\Configurator\Helpers\TemplateHelper;
use s9e\TextFormatter\Configurator\RendererGenerator;
use s9e\TextFormatter\Configurator\RendererGenerators\XSLT\Optimizer;
use s9e\TextFormatter\Configurator\Rendering;
use s9e\TextFormatter\Renderers\XSLT as XSLTRenderer;
class XSLT implements RendererGenerator
{
public $optimizer;
public function __construct()
{
$this->optimizer = new Optimizer;
}
public function getRenderer(Rendering $rendering)
{
return new XSLTRenderer($this->getXSL($rendering));
}
public function getXSL(Rendering $rendering)
{
$groupedTemplates = [];
$prefixes = [];
$templates = $rendering->getTemplates();
TemplateHelper::replaceHomogeneousTemplates($templates, 3);
foreach ($templates as $tagName => $template)
{
$template = $this->optimizer->optimizeTemplate($template);
$groupedTemplates[$template][] = $tagName;
$pos = \strpos($tagName, ':');
if ($pos !== \false)
$prefixes[\substr($tagName, 0, $pos)] = 1;
}
$xsl = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"';
$prefixes = \array_keys($prefixes);
\sort($prefixes);
foreach ($prefixes as $prefix)
$xsl .= ' xmlns:' . $prefix . '="urn:s9e:TextFormatter:' . $prefix . '"';
if (!empty($prefixes))
$xsl .= ' exclude-result-prefixes="' . \implode(' ', $prefixes) . '"';
$xsl .= '><xsl:output method="html" encoding="utf-8" indent="no"';
$xsl .= '/>';
foreach ($rendering->getAllParameters() as $paramName => $paramValue)
{
$xsl .= '<xsl:param name="' . \htmlspecialchars($paramName) . '"';
if ($paramValue === '')
$xsl .= '/>';
else
$xsl .= '>' . \htmlspecialchars($paramValue) . '</xsl:param>';
}
foreach ($groupedTemplates as $template => $tagNames)
{
$xsl .= '<xsl:template match="' . \implode('|', $tagNames) . '"';
if ($template === '')
$xsl .= '/>';
else
$xsl .= '>' . $template . '</xsl:template>';
}
$xsl .= '</xsl:stylesheet>';
return $xsl;
}
}

View File

@ -0,0 +1,26 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\RendererGenerators\XSLT;
use s9e\TextFormatter\Configurator\TemplateNormalizer;
class Optimizer
{
public $normalizer;
public function __construct()
{
$this->normalizer = new TemplateNormalizer;
$this->normalizer->clear();
$this->normalizer->append('MergeConsecutiveCopyOf');
$this->normalizer->append('MergeIdenticalConditionalBranches');
$this->normalizer->append('OptimizeNestedConditionals');
$this->normalizer->append('RemoveLivePreviewAttributes');
}
public function optimizeTemplate($template)
{
return $this->normalizer->normalizeTemplate($template);
}
}

View File

@ -0,0 +1,17 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\RulesGenerators;
use s9e\TextFormatter\Configurator\Helpers\TemplateInspector;
use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\TargetedRulesGenerator;
class AllowAll implements TargetedRulesGenerator
{
public function generateTargetedRules(TemplateInspector $src, TemplateInspector $trg)
{
return ['allowChild', 'allowDescendant'];
}
}

View File

@ -0,0 +1,27 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\RulesGenerators;
use s9e\TextFormatter\Configurator\Helpers\TemplateInspector;
use s9e\TextFormatter\Configurator\RulesGenerators\Interfaces\BooleanRulesGenerator;
class ManageParagraphs implements BooleanRulesGenerator
{
protected $p;
public function __construct()
{
$this->p = new TemplateInspector('<p><xsl:apply-templates/></p>');
}
public function generateBooleanRules(TemplateInspector $src)
{
$rules = [];
if ($src->allowsChild($this->p) && $src->isBlock() && !$this->p->closesParent($src))
$rules['createParagraphs'] = \true;
if ($src->closesParent($this->p))
$rules['breakParagraph'] = \true;
return $rules;
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\TemplateChecks;
use DOMElement;
use DOMXPath;
use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException;
use s9e\TextFormatter\Configurator\Items\Tag;
use s9e\TextFormatter\Configurator\TemplateCheck;
class DisallowElement extends TemplateCheck
{
public $elName;
public function __construct($elName)
{
$this->elName = \strtolower($elName);
}
public function check(DOMElement $template, Tag $tag)
{
$xpath = new DOMXPath($template->ownerDocument);
$query
= '//*[translate(local-name(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz") = "' . $this->elName . '"]|//xsl:element[translate(@name,"ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz") = "' . $this->elName . '"]';
$node = $xpath->query($query)->item(0);
if ($node)
throw new UnsafeTemplateException("Element '" . $this->elName . "' is disallowed", $node);
}
}

View File

@ -0,0 +1,21 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\TemplateChecks;
class DisallowFlashFullScreen extends AbstractFlashRestriction
{
public $defaultSetting = 'false';
protected $settingName = 'allowFullScreen';
protected $settings = [
'true' => 1,
'false' => 0
];
public function __construct($onlyIfDynamic = \false)
{
parent::__construct('false', $onlyIfDynamic);
}
}

View File

@ -0,0 +1,27 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\TemplateChecks;
use DOMElement;
use DOMXPath;
use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException;
use s9e\TextFormatter\Configurator\Items\Tag;
use s9e\TextFormatter\Configurator\TemplateCheck;
class DisallowNodeByXPath extends TemplateCheck
{
public $query;
public function __construct($query)
{
$this->query = $query;
}
public function check(DOMElement $template, Tag $tag)
{
$xpath = new DOMXPath($template->ownerDocument);
foreach ($xpath->query($this->query) as $node)
throw new UnsafeTemplateException("Node '" . $node->nodeName . "' is disallowed because it matches '" . $this->query . "'", $node);
}
}

View File

@ -0,0 +1,18 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\TemplateChecks;
class RestrictFlashNetworking extends AbstractFlashRestriction
{
public $defaultSetting = 'all';
protected $settingName = 'allowNetworking';
protected $settings = [
'all' => 3,
'internal' => 2,
'none' => 1
];
}

View File

@ -0,0 +1,44 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
use DOMNode;
class ConvertCurlyExpressionsInText extends AbstractNormalization
{
protected $queries = ['//*[namespace-uri() != $XSL]/text()[contains(., "{@") or contains(., "{$")]'];
protected function insertTextBefore($text, $node)
{
$node->parentNode->insertBefore($this->createTextNode($text), $node);
}
protected function normalizeNode(DOMNode $node)
{
$parentNode = $node->parentNode;
\preg_match_all(
'#\\{([$@][-\\w]+)\\}#',
$node->textContent,
$matches,
\PREG_SET_ORDER | \PREG_OFFSET_CAPTURE
);
$lastPos = 0;
foreach ($matches as $m)
{
$pos = $m[0][1];
if ($pos > $lastPos)
{
$text = \substr($node->textContent, $lastPos, $pos - $lastPos);
$this->insertTextBefore($text, $node);
}
$lastPos = $pos + \strlen($m[0][0]);
$parentNode
->insertBefore($this->createElement('xsl:value-of'), $node)
->setAttribute('select', $m[1][0]);
}
$text = \substr($node->textContent, $lastPos);
$this->insertTextBefore($text, $node);
$parentNode->removeChild($node);
}
}

View File

@ -0,0 +1,21 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
use DOMElement;
class Custom extends AbstractNormalization
{
protected $callback;
public function __construct(callable $callback)
{
$this->callback = $callback;
}
public function normalize(DOMElement $template)
{
\call_user_func($this->callback, $template);
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
use DOMElement;
class MergeConsecutiveCopyOf extends AbstractNormalization
{
protected $queries = ['//xsl:copy-of'];
protected function normalizeElement(DOMElement $element)
{
while ($this->nextSiblingIsCopyOf($element))
{
$element->setAttribute('select', $element->getAttribute('select') . '|' . $element->nextSibling->getAttribute('select'));
$element->parentNode->removeChild($element->nextSibling);
}
}
protected function nextSiblingIsCopyOf(DOMElement $element)
{
return ($element->nextSibling && $this->isXsl($element->nextSibling, 'copy-of'));
}
}

View File

@ -0,0 +1,89 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
use DOMElement;
use DOMNode;
use s9e\TextFormatter\Configurator\Helpers\TemplateParser;
class MergeIdenticalConditionalBranches extends AbstractNormalization
{
protected $queries = ['//xsl:choose'];
protected function collectCompatibleBranches(DOMNode $node)
{
$nodes = [];
$key = \null;
$values = [];
while ($node && $this->isXsl($node, 'when'))
{
$branch = TemplateParser::parseEqualityExpr($node->getAttribute('test'));
if ($branch === \false || \count($branch) !== 1)
break;
if (isset($key) && \key($branch) !== $key)
break;
if (\array_intersect($values, \end($branch)))
break;
$key = \key($branch);
$values = \array_merge($values, \end($branch));
$nodes[] = $node;
$node = $node->nextSibling;
}
return $nodes;
}
protected function mergeBranches(array $nodes)
{
$sortedNodes = [];
foreach ($nodes as $node)
{
$outerXML = $node->ownerDocument->saveXML($node);
$innerXML = \preg_replace('([^>]+>(.*)<[^<]+)s', '$1', $outerXML);
$sortedNodes[$innerXML][] = $node;
}
foreach ($sortedNodes as $identicalNodes)
{
if (\count($identicalNodes) < 2)
continue;
$expr = [];
foreach ($identicalNodes as $i => $node)
{
$expr[] = $node->getAttribute('test');
if ($i > 0)
$node->parentNode->removeChild($node);
}
$identicalNodes[0]->setAttribute('test', \implode(' or ', $expr));
}
}
protected function mergeCompatibleBranches(DOMElement $choose)
{
$node = $choose->firstChild;
while ($node)
{
$nodes = $this->collectCompatibleBranches($node);
if (\count($nodes) > 1)
{
$node = \end($nodes)->nextSibling;
$this->mergeBranches($nodes);
}
else
$node = $node->nextSibling;
}
}
protected function mergeConsecutiveBranches(DOMElement $choose)
{
$nodes = [];
foreach ($choose->childNodes as $node)
if ($this->isXsl($node, 'when'))
$nodes[] = $node;
$i = \count($nodes);
while (--$i > 0)
$this->mergeBranches([$nodes[$i - 1], $nodes[$i]]);
}
protected function normalizeElement(DOMElement $element)
{
$this->mergeCompatibleBranches($element);
$this->mergeConsecutiveBranches($element);
}
}

View File

@ -0,0 +1,21 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
use DOMElement;
class OptimizeNestedConditionals extends AbstractNormalization
{
protected $queries = ['//xsl:choose/xsl:otherwise[count(node()) = 1]/xsl:choose'];
protected function normalizeElement(DOMElement $element)
{
$otherwise = $element->parentNode;
$outerChoose = $otherwise->parentNode;
while ($element->firstChild)
$outerChoose->appendChild($element->removeChild($element->firstChild));
$outerChoose->removeChild($otherwise);
}
}

View File

@ -0,0 +1,22 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
use DOMElement;
class SortAttributesByName extends AbstractNormalization
{
protected $queries = ['//*[@*]'];
protected function normalizeElement(DOMElement $element)
{
$attributes = [];
foreach ($element->attributes as $name => $attribute)
$attributes[$name] = $element->removeAttributeNode($attribute);
\ksort($attributes);
foreach ($attributes as $attribute)
$element->setAttributeNode($attribute);
}
}

View File

@ -0,0 +1,18 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
use DOMNode;
class TransposeComments extends AbstractNormalization
{
protected $queries = ['//comment()'];
protected function normalizeNode(DOMNode $node)
{
$xslComment = $this->createElement('xsl:comment', $node->nodeValue);
$node->parentNode->replaceChild($xslComment, $node);
}
}

View File

@ -0,0 +1,23 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\Validators;
use InvalidArgumentException;
abstract class TemplateParameterName
{
public static function isValid($name)
{
return (bool) \preg_match('#^[a-z_][-a-z_0-9]*$#Di', $name);
}
public static function normalize($name)
{
$name = (string) $name;
if (!static::isValid($name))
throw new InvalidArgumentException("Invalid parameter name '" . $name . "'");
return $name;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
/** @const */
var EmailFilter =
{
/**
* @param {*} attrValue
* @return {*}
*/
filter: function(attrValue)
{
return /^[-\w.+]+@[-\w.]+$/.test(attrValue) ? attrValue : false;
}
};

View File

@ -0,0 +1,15 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Parser\AttributeFilters;
class EmailFilter
{
public static function filter($attrValue)
{
return \filter_var($attrValue, \FILTER_VALIDATE_EMAIL);
}
}

View File

@ -0,0 +1,12 @@
/** @const */
var FalseFilter =
{
/**
* @param {*} attrValue
* @return {!boolean}
*/
filter: function(attrValue)
{
return false;
}
};

View File

@ -0,0 +1,15 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Parser\AttributeFilters;
class FalseFilter
{
public static function filter($attrValue)
{
return \false;
}
}

View File

@ -0,0 +1,19 @@
/** @const */
var HashmapFilter =
{
/**
* @param {*} attrValue Original value
* @param {!Object} map Hash map
* @param {!boolean} strict Whether this map is strict (values with no match are invalid)
* @return {*} Filtered value, or FALSE if invalid
*/
filter: function(attrValue, map, strict)
{
if (attrValue in map)
{
return map[attrValue];
}
return (strict) ? false : attrValue;
}
};

View File

@ -0,0 +1,17 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Parser\AttributeFilters;
class HashmapFilter
{
public static function filter($attrValue, array $map, $strict)
{
if (isset($map[$attrValue]))
return $map[$attrValue];
return ($strict) ? \false : $attrValue;
}
}

View File

@ -0,0 +1,22 @@
/** @const */
var MapFilter =
{
/**
* @param {*} attrValue
* @param {!Array.<!Array>} map
* @return {*}
*/
filter: function(attrValue, map)
{
var i = -1, cnt = map.length;
while (++i < cnt)
{
if (map[i][0].test(attrValue))
{
return map[i][1];
}
}
return attrValue;
}
};

View File

@ -0,0 +1,18 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Parser\AttributeFilters;
class MapFilter
{
public static function filter($attrValue, array $map)
{
foreach ($map as $pair)
if (\preg_match($pair[0], $attrValue))
return $pair[1];
return $attrValue;
}
}

View File

@ -0,0 +1,90 @@
/** @const */
var NetworkFilter =
{
/**
* @param {*} attrValue
* @return {*}
*/
filterIp: function(attrValue)
{
if (/^[\d.]+$/.test(attrValue))
{
return NetworkFilter.filterIpv4(attrValue);
}
if (/^[\da-f:]+$/i.test(attrValue))
{
return NetworkFilter.filterIpv6(attrValue);
}
return false;
},
/**
* @param {*} attrValue
* @return {*}
*/
filterIpport: function(attrValue)
{
var m, ip;
if (m = /^\[([\da-f:]+)(\]:[1-9]\d*)$/i.exec(attrValue))
{
ip = NetworkFilter.filterIpv6(m[1]);
if (ip === false)
{
return false;
}
return '[' + ip + m[2];
}
if (m = /^([\d.]+)(:[1-9]\d*)$/.exec(attrValue))
{
ip = NetworkFilter.filterIpv4(m[1]);
if (ip === false)
{
return false;
}
return ip + m[2];
}
return false;
},
/**
* @param {*} attrValue
* @return {*}
*/
filterIpv4: function(attrValue)
{
if (!/^\d+\.\d+\.\d+\.\d+$/.test(attrValue))
{
return false;
}
var i = 4, p = attrValue.split('.');
while (--i >= 0)
{
// NOTE: ext/filter doesn't support octal notation
if (p[i][0] === '0' || p[i] > 255)
{
return false;
}
}
return attrValue;
},
/**
* @param {*} attrValue
* @return {*}
*/
filterIpv6: function(attrValue)
{
return /^([\da-f]{0,4}:){2,7}(?:[\da-f]{0,4}|\d+\.\d+\.\d+\.\d+)$/.test(attrValue) ? attrValue : false;
}
};

View File

@ -0,0 +1,41 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Parser\AttributeFilters;
class NetworkFilter
{
public static function filterIp($attrValue)
{
return \filter_var($attrValue, \FILTER_VALIDATE_IP);
}
public static function filterIpport($attrValue)
{
if (\preg_match('/^\\[([^\\]]+)(\\]:[1-9][0-9]*)$/D', $attrValue, $m))
{
$ip = self::filterIpv6($m[1]);
if ($ip === \false)
return \false;
return '[' . $ip . $m[2];
}
if (\preg_match('/^([^:]+)(:[1-9][0-9]*)$/D', $attrValue, $m))
{
$ip = self::filterIpv4($m[1]);
if ($ip === \false)
return \false;
return $ip . $m[2];
}
return \false;
}
public static function filterIpv4($attrValue)
{
return \filter_var($attrValue, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4);
}
public static function filterIpv6($attrValue)
{
return \filter_var($attrValue, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6);
}
}

View File

@ -0,0 +1,83 @@
/** @const */
var NumericFilter =
{
/**
* @param {*} attrValue
* @return {*}
*/
filterFloat: function(attrValue)
{
return /^(?:0|-?[1-9]\d*)(?:\.\d+)?(?:e[1-9]\d*)?$/i.test(attrValue) ? attrValue : false;
},
/**
* @param {*} attrValue
* @return {*}
*/
filterInt: function(attrValue)
{
return /^(?:0|-?[1-9]\d*)$/.test(attrValue) ? attrValue : false;
},
/**
* @param {*} attrValue
* @param {!number} min
* @param {!number} max
* @param {Logger} logger
* @return {!number|boolean}
*/
filterRange: function(attrValue, min, max, logger)
{
if (!/^(?:0|-?[1-9]\d*)$/.test(attrValue))
{
return false;
}
attrValue = parseInt(attrValue, 10);
if (attrValue < min)
{
if (logger)
{
logger.warn(
'Value outside of range, adjusted up to min value',
{
'attrValue' : attrValue,
'min' : min,
'max' : max
}
);
}
return min;
}
if (attrValue > max)
{
if (logger)
{
logger.warn(
'Value outside of range, adjusted down to max value',
{
'attrValue' : attrValue,
'min' : min,
'max' : max
}
);
}
return max;
}
return attrValue;
},
/**
* @param {*} attrValue
* @return {*}
*/
filterUint: function(attrValue)
{
return /^(?:0|[1-9]\d*)$/.test(attrValue) ? attrValue : false;
}
};

View File

@ -0,0 +1,59 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Parser\AttributeFilters;
use s9e\TextFormatter\Parser\Logger;
class NumericFilter
{
public static function filterFloat($attrValue)
{
return \filter_var($attrValue, \FILTER_VALIDATE_FLOAT);
}
public static function filterInt($attrValue)
{
return \filter_var($attrValue, \FILTER_VALIDATE_INT);
}
public static function filterRange($attrValue, $min, $max, Logger $logger = \null)
{
$attrValue = \filter_var($attrValue, \FILTER_VALIDATE_INT);
if ($attrValue === \false)
return \false;
if ($attrValue < $min)
{
if (isset($logger))
$logger->warn(
'Value outside of range, adjusted up to min value',
[
'attrValue' => $attrValue,
'min' => $min,
'max' => $max
]
);
return $min;
}
if ($attrValue > $max)
{
if (isset($logger))
$logger->warn(
'Value outside of range, adjusted down to max value',
[
'attrValue' => $attrValue,
'min' => $min,
'max' => $max
]
);
return $max;
}
return $attrValue;
}
public static function filterUint($attrValue)
{
return \filter_var($attrValue, \FILTER_VALIDATE_INT, [
'options' => ['min_range' => 0]
]);
}
}

View File

@ -0,0 +1,13 @@
/** @const */
var RegexpFilter =
{
/**
* @param {*} attrValue
* @param {!RegExp} regexp
* @return {*}
*/
filter: function(attrValue, regexp)
{
return regexp.test(attrValue) ? attrValue : false;
}
};

View File

@ -0,0 +1,17 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Parser\AttributeFilters;
class RegexpFilter
{
public static function filter($attrValue, $regexp)
{
return \filter_var($attrValue, \FILTER_VALIDATE_REGEXP, [
'options' => ['regexp' => $regexp]
]);
}
}

View File

@ -0,0 +1,18 @@
/** @const */
var TimestampFilter =
{
/**
* @param {*} attrValue
* @return {*}
*/
filter: function(attrValue)
{
var m = /^(?=\d)(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/.exec(attrValue);
if (m)
{
return 3600 * (m[1] || 0) + 60 * (m[2] || 0) + (+m[3] || 0);
}
return NumericFilter.filterUint(attrValue);
}
};

View File

@ -0,0 +1,20 @@
<?php
/*
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2017 The s9e Authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Parser\AttributeFilters;
class TimestampFilter
{
public static function filter($attrValue)
{
if (\preg_match('/^(?=\\d)(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?$/D', $attrValue, $m))
{
$m += [0, 0, 0, 0];
return \intval($m[1]) * 3600 + \intval($m[2]) * 60 + \intval($m[3]);
}
return NumericFilter::filterUint($attrValue);
}
}

View File

@ -0,0 +1,239 @@
/** @const */
var UrlFilter =
{
/**
* @param {*} attrValue
* @param {!Object} urlConfig
* @param {Logger} logger
* @return {*}
*/
filter: function(attrValue, urlConfig, logger)
{
/**
* Trim the URL to conform with HTML5 then parse it
* @link http://dev.w3.org/html5/spec/links.html#attr-hyperlink-href
*/
var p = UrlFilter.parseUrl(attrValue.replace(/^\s+/, '').replace(/\s+$/, ''));
var error = UrlFilter.validateUrl(urlConfig, p);
if (error)
{
if (logger)
{
p['attrValue'] = attrValue;
logger.err(error, p);
}
return false;
}
return UrlFilter.rebuildUrl(urlConfig, p);
},
/**
* Parse a URL and return its components
*
* Similar to PHP's own parse_url() except that all parts are always returned
*
* @param {!string} url Original URL
* @return {!Object}
*/
parseUrl: function(url)
{
var regexp = /^(?:([a-z][-+.\w]*):)?(?:\/\/(?:([^:\/?#]*)(?::([^\/?#]*)?)?@)?(?:(\[[a-f\d:]+\]|[^:\/?#]+)(?::(\d*))?)?(?![^\/?#]))?([^?#]*)(\?[^#]*)?(#.*)?$/i;
// NOTE: this regexp always matches because of the last three captures
var m = regexp['exec'](url),
parts = {},
tokens = ['scheme', 'user', 'pass', 'host', 'port', 'path', 'query', 'fragment'];
tokens.forEach(
function(name, i)
{
parts[name] = (m[i + 1] > '') ? m[i + 1] : '';
}
);
/**
* @link http://tools.ietf.org/html/rfc3986#section-3.1
*
* 'An implementation should accept uppercase letters as equivalent to lowercase in
* scheme names (e.g., allow "HTTP" as well as "http") for the sake of robustness but
* should only produce lowercase scheme names for consistency.'
*/
parts['scheme'] = parts['scheme'].toLowerCase();
/**
* Normalize the domain label separators and remove trailing dots
* @link http://url.spec.whatwg.org/#domain-label-separators
*/
parts['host'] = parts['host'].replace(/[\u3002\uff0e\uff61]/g, '.').replace(/\.+$/g, '');
// Test whether host has non-ASCII characters and punycode it if possible
if (/[^\x00-\x7F]/.test(parts['host']) && typeof punycode !== 'undefined')
{
parts['host'] = punycode.toASCII(parts['host']);
}
return parts;
},
/**
* Rebuild a parsed URL
*
* @param {!Object} urlConfig
* @param {!Object} p
* @return {!string}
*/
rebuildUrl: function(urlConfig, p)
{
var url = '';
if (p['scheme'] !== '')
{
url += p['scheme'] + ':';
}
if (p['host'] === '')
{
// Allow the file: scheme to not have a host and ensure it starts with slashes
if (p['scheme'] === 'file')
{
url += '//';
}
}
else
{
url += '//';
// Add the credentials if applicable
if (p['user'] !== '')
{
// Reencode the credentials in case there are invalid chars in them, or suspicious
// characters such as : or @ that could confuse a browser into connecting to the
// wrong host (or at least, to a host that is different than the one we thought)
url += rawurlencode(decodeURIComponent(p['user']));
if (p['pass'] !== '')
{
url += ':' + rawurlencode(decodeURIComponent(p['pass']));
}
url += '@';
}
url += p['host'];
// Append the port number (note that as per the regexp it can only contain digits)
if (p['port'] !== '')
{
url += ':' + p['port'];
}
}
// Build the path, including the query and fragment parts
var path = p['path'] + p['query'] + p['fragment'];
/**
* "For consistency, URI producers and normalizers should use uppercase hexadecimal digits
* for all percent- encodings."
*
* @link http://tools.ietf.org/html/rfc3986#section-2.1
*/
path = path.replace(
/%.?[a-f]/g,
function (str)
{
return str.toUpperCase();
},
path
);
// Append the sanitized path to the URL
url += UrlFilter.sanitizeUrl(path);
// Replace the first colon if there's no scheme and it could potentially be interpreted as
// the scheme separator
if (!p['scheme'])
{
url = url.replace(/^([^\/]*):/, '$1%3A', url);
}
return url;
},
/**
* Sanitize a URL for safe use regardless of context
*
* This method URL-encodes some sensitive characters in case someone would want to use the URL in
* some JavaScript thingy, or in CSS. We also encode characters that are not allowed in the path
* of a URL as defined in RFC 3986 appendix A, including percent signs that are not immediately
* followed by two hex digits.
*
* " and ' to prevent breaking out of quotes (JavaScript or otherwise)
* ( and ) to prevent the use of functions in JavaScript (eval()) or CSS (expression())
* < and > to prevent breaking out of <script>
* \r and \n because they're illegal in JavaScript
* [ and ] because the W3 validator rejects them and they "should" be escaped as per RFC 3986
* Non-ASCII characters as per RFC 3986
* Control codes and spaces, as per RFC 3986
*
* @link http://sla.ckers.org/forum/read.php?2,51478
* @link http://timelessrepo.com/json-isnt-a-javascript-subset
* @link http://www.ietf.org/rfc/rfc3986.txt
* @link http://stackoverflow.com/a/1547922
* @link http://tools.ietf.org/html/rfc3986#appendix-A
*
* @param {!string} url Original URL
* @return {!string} Sanitized URL
*/
sanitizeUrl: function(url)
{
return url.replace(/[^\u0020-\u007E]+/g, encodeURIComponent).replace(/%(?![0-9A-Fa-f]{2})|[^!#-&*-;=?-Z_a-z~]/g, escape);
},
/**
* Validate a parsed URL
*
* @param {!Object} urlConfig
* @param {!Object} p
* @return {string|undefined}
*/
validateUrl: function(urlConfig, p)
{
if (p['scheme'] !== '' && !urlConfig.allowedSchemes.test(p['scheme']))
{
return 'URL scheme is not allowed';
}
if (p['host'] === '')
{
// Reject malformed URLs such as http:///example.org but allow schemeless paths
if (p['scheme'] !== 'file' && p['scheme'] !== '')
{
return 'Missing host';
}
}
else
{
/**
* Test whether the host is valid
* @link http://tools.ietf.org/html/rfc1035#section-2.3.1
* @link http://tools.ietf.org/html/rfc1123#section-2
*/
var regexp = /^(?!-)[-a-z0-9]{0,62}[a-z0-9](?:\.(?!-)[-a-z0-9]{0,62}[a-z0-9])*$/i;
if (!regexp.test(p['host']))
{
// If the host invalid, retest as an IPv4 and IPv6 address (IPv6 in brackets)
if (!NetworkFilter.filterIpv4(p['host'])
&& !NetworkFilter.filterIpv6(p['host'].replace(/^\[(.*)\]$/, '$1', p['host'])))
{
return 'URL host is invalid';
}
}
if ((urlConfig.disallowedHosts && urlConfig.disallowedHosts.test(p['host']))
|| (urlConfig.restrictedHosts && !urlConfig.restrictedHosts.test(p['host'])))
{
return 'URL host is not allowed';
}
}
}
};

Some files were not shown because too many files have changed in this diff Show More