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,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\Plugins\Autoemail;
use s9e\TextFormatter\Plugins\ConfiguratorBase;
class Configurator extends ConfiguratorBase
{
protected $attrName = 'email';
protected $quickMatch = '@';
protected $regexp = '/\\b[-a-z0-9_+.]+@[-a-z0-9.]*[a-z0-9]/Si';
protected $tagName = 'EMAIL';
protected function setUp()
{
if (isset($this->configurator->tags[$this->tagName]))
return;
$tag = $this->configurator->tags->add($this->tagName);
$filter = $this->configurator->attributeFilters->get('#email');
$tag->attributes->add($this->attrName)->filterChain->append($filter);
$tag->template = '<a href="mailto:{@' . $this->attrName . '}"><xsl:apply-templates/></a>';
}
}

View File

@ -0,0 +1,15 @@
var tagName = config.tagName,
attrName = config.attrName;
matches.forEach(function(m)
{
// Create a zero-width start tag right before the address
var startTag = addStartTag(tagName, m[0][1], 0);
startTag.setAttribute(attrName, m[0][0]);
// Create a zero-width end tag right after the address
var endTag = addEndTag(tagName, m[0][1] + m[0][0].length, 0);
// Pair the tags together
startTag.pairWith(endTag);
});

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\Plugins\Autoemail;
use s9e\TextFormatter\Plugins\ParserBase;
class Parser extends ParserBase
{
public function parse($text, array $matches)
{
$tagName = $this->config['tagName'];
$attrName = $this->config['attrName'];
foreach ($matches as $m)
{
$startTag = $this->parser->addStartTag($tagName, $m[0][1], 0);
$startTag->setAttribute($attrName, $m[0][0]);
$endTag = $this->parser->addEndTag($tagName, $m[0][1] + \strlen($m[0][0]), 0);
$startTag->pairWith($endTag);
}
}
}

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\Plugins\Autoimage;
use s9e\TextFormatter\Plugins\ConfiguratorBase;
class Configurator extends ConfiguratorBase
{
protected $attrName = 'src';
protected $quickMatch = '://';
protected $regexp = '#\\bhttps?://[-.\\w]+/(?:[-+.:/\\w]|%[0-9a-f]{2}|\\(\\w+\\))+\\.(?:gif|jpe?g|png)(?!\\S)#i';
protected $tagName = 'IMG';
protected function setUp()
{
if (isset($this->configurator->tags[$this->tagName]))
return;
$tag = $this->configurator->tags->add($this->tagName);
$filter = $this->configurator->attributeFilters->get('#url');
$tag->attributes->add($this->attrName)->filterChain->append($filter);
$tag->template = '<img src="{@' . $this->attrName . '}"/>';
}
}

View File

@ -0,0 +1,7 @@
var tagName = config.tagName,
attrName = config.attrName;
matches.forEach(function(m)
{
addTagPair(tagName, m[0][1], 0, m[0][1] + m[0][0].length, 0, 2).setAttribute(attrName, m[0][0]);
});

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\Plugins\Autoimage;
use s9e\TextFormatter\Plugins\ParserBase;
class Parser extends ParserBase
{
public function parse($text, array $matches)
{
$tagName = $this->config['tagName'];
$attrName = $this->config['attrName'];
foreach ($matches as $m)
$this->parser->addTagPair($tagName, $m[0][1], 0, $m[0][1] + \strlen($m[0][0]), 0, 2)
->setAttribute($attrName, $m[0][0]);
}
}

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\Plugins\Autolink;
use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
use s9e\TextFormatter\Plugins\ConfiguratorBase;
class Configurator extends ConfiguratorBase
{
protected $attrName = 'url';
public $matchWww = \false;
protected $tagName = 'URL';
protected function setUp()
{
if (isset($this->configurator->tags[$this->tagName]))
return;
$tag = $this->configurator->tags->add($this->tagName);
$filter = $this->configurator->attributeFilters->get('#url');
$tag->attributes->add($this->attrName)->filterChain->append($filter);
$tag->template = '<a href="{@' . $this->attrName . '}"><xsl:apply-templates/></a>';
}
public function asConfig()
{
$config = [
'attrName' => $this->attrName,
'regexp' => $this->getRegexp(),
'tagName' => $this->tagName
];
if (!$this->matchWww)
$config['quickMatch'] = '://';
return $config;
}
protected function getRegexp()
{
$anchor = RegexpBuilder::fromList($this->configurator->urlConfig->getAllowedSchemes()) . '://';
if ($this->matchWww)
$anchor = '(?:' . $anchor . '|www\\.)';
$regexp = '#\\b' . $anchor . '\\S(?>[^\\s()\\[\\]\\x{FF01}-\\x{FF0F}\\x{FF1A}-\\x{FF20}\\x{FF3B}-\\x{FF40}\\x{FF5B}-\\x{FF65}]|\\([^\\s()]*\\)|\\[\\w*\\])++#Siu';
return $regexp;
}
}

View File

@ -0,0 +1,55 @@
matches.forEach(function(m)
{
// Linkify the trimmed URL
linkifyUrl(m[0][1], trimUrl(m[0][0]));
});
/**
* Linkify given URL at given position
*
* @param {!number} tagPos URL's position in the text
* @param {!string} url URL
*/
function linkifyUrl(tagPos, url)
{
// Ensure that the anchor (scheme/www) is still there
if (!/^www\.|^[^:]+:/i.test(url))
{
return;
}
// Create a zero-width end tag right after the URL
var endTag = addEndTag(config.tagName, tagPos + url.length, 0);
// If the URL starts with "www." we prepend "http://"
if (url[3] === '.')
{
url = 'http://' + url;
}
// Create a zero-width start tag right before the URL, with a slightly worse priority to
// allow specialized plugins to use the URL instead
var startTag = addStartTag(config.tagName, tagPos, 0, 1);
startTag.setAttribute(config.attrName, url);
// Pair the tags together
startTag.pairWith(endTag);
};
/**
* Remove trailing punctuation from given URL
*
* We remove most ASCII non-letters from the end of the string.
* Exceptions:
* - dashes (some YouTube URLs end with a dash due to the video ID)
* - equal signs (because of "foo?bar="),
* - trailing slashes,
* - closing parentheses are balanced separately.
*
* @param {!string} url Original URL
* @return {!string} Trimmed URL
*/
function trimUrl(url)
{
return url.replace(/(?![-=\/)])[\s!-.:-@[-`{-~]+$/, '');
}

View File

@ -0,0 +1,32 @@
<?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\Plugins\Autolink;
use s9e\TextFormatter\Plugins\ParserBase;
class Parser extends ParserBase
{
public function parse($text, array $matches)
{
foreach ($matches as $m)
$this->linkifyUrl($m[0][1], $this->trimUrl($m[0][0]));
}
protected function linkifyUrl($tagPos, $url)
{
if (!\preg_match('/^[^:]+:|^www\\./i', $url))
return;
$endTag = $this->parser->addEndTag($this->config['tagName'], $tagPos + \strlen($url), 0);
if ($url[3] === '.')
$url = 'http://' . $url;
$startTag = $this->parser->addStartTag($this->config['tagName'], $tagPos, 0, 1);
$startTag->setAttribute($this->config['attrName'], $url);
$startTag->pairWith($endTag);
}
protected function trimUrl($url)
{
return \preg_replace('#(?![-=/)])[\\s!-.:-@[-`{-~\\pP]+$#Du', '', $url);
}
}

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\Plugins\Autovideo;
use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
use s9e\TextFormatter\Plugins\ConfiguratorBase;
class Configurator extends ConfiguratorBase
{
protected $attrName = 'src';
protected $quickMatch = '://';
protected $regexp = '#\\bhttps?://[-.\\w]+/(?:[-+.:/\\w]|%[0-9a-f]{2}|\\(\\w+\\))+\\.(?:mp4|ogg|webm)(?!\\S)#i';
protected $tagName = 'VIDEO';
protected function setUp()
{
if (isset($this->configurator->tags[$this->tagName]))
return;
$tag = $this->configurator->tags->add($this->tagName);
$filter = $this->configurator->attributeFilters['#url'];
$tag->attributes->add($this->attrName)->filterChain->append($filter);
$tag->template = '<video src="{@' . $this->attrName . '}"/>';
$tag->rules->allowChild('URL');
}
}

View File

@ -0,0 +1,7 @@
var tagName = config.tagName,
attrName = config.attrName;
matches.forEach(function(m)
{
addTagPair(tagName, m[0][1], 0, m[0][1] + m[0][0].length, 0, -1).setAttribute(attrName, m[0][0]);
});

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\Plugins\Autovideo;
use s9e\TextFormatter\Plugins\ParserBase;
class Parser extends ParserBase
{
public function parse($text, array $matches)
{
$tagName = $this->config['tagName'];
$attrName = $this->config['attrName'];
foreach ($matches as $m)
$this->parser->addTagPair($tagName, $m[0][1], 0, $m[0][1] + \strlen($m[0][0]), 0, -1)
->setAttribute($attrName, $m[0][0]);
}
}

View File

@ -0,0 +1,81 @@
<?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\Plugins\BBCodes;
use ArrayAccess;
use Countable;
use InvalidArgumentException;
use Iterator;
use RuntimeException;
use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
use s9e\TextFormatter\Configurator\Helpers\RegexpParser;
use s9e\TextFormatter\Configurator\JavaScript\Dictionary;
use s9e\TextFormatter\Configurator\Traits\CollectionProxy;
use s9e\TextFormatter\Plugins\BBCodes\Configurator\BBCode;
use s9e\TextFormatter\Plugins\BBCodes\Configurator\BBCodeCollection;
use s9e\TextFormatter\Plugins\BBCodes\Configurator\BBCodeMonkey;
use s9e\TextFormatter\Plugins\BBCodes\Configurator\Repository;
use s9e\TextFormatter\Plugins\BBCodes\Configurator\RepositoryCollection;
use s9e\TextFormatter\Plugins\ConfiguratorBase;
class Configurator extends ConfiguratorBase implements ArrayAccess, Countable, Iterator
{
use CollectionProxy;
public $bbcodeMonkey;
public $collection;
protected $quickMatch = '[';
protected $regexp = '#\\[/?(\\*|[-\\w]+)(?=[\\]\\s=:/])#';
public $repositories;
protected function setUp()
{
$this->bbcodeMonkey = new BBCodeMonkey($this->configurator);
$this->collection = new BBCodeCollection;
$this->repositories = new RepositoryCollection($this->bbcodeMonkey);
$this->repositories->add('default', __DIR__ . '/Configurator/repository.xml');
}
public function addCustom($usage, $template, array $options = [])
{
$config = $this->bbcodeMonkey->create($usage, $template);
if (isset($options['tagName']))
$config['bbcode']->tagName = $options['tagName'];
if (isset($options['rules']))
$config['tag']->rules->merge($options['rules']);
return $this->addFromConfig($config);
}
public function addFromRepository($name, $repository = 'default', array $vars = [])
{
if (!($repository instanceof Repository))
{
if (!$this->repositories->exists($repository))
throw new InvalidArgumentException("Repository '" . $repository . "' does not exist");
$repository = $this->repositories->get($repository);
}
return $this->addFromConfig($repository->get($name, $vars));
}
protected function addFromConfig(array $config)
{
$bbcodeName = $config['bbcodeName'];
$bbcode = $config['bbcode'];
$tag = $config['tag'];
if (!isset($bbcode->tagName))
$bbcode->tagName = $bbcodeName;
$this->configurator->templateNormalizer->normalizeTag($tag);
$this->configurator->templateChecker->checkTag($tag);
$this->collection->add($bbcodeName, $bbcode);
$this->configurator->tags->add($bbcode->tagName, $tag);
return $bbcode;
}
public function asConfig()
{
if (!\count($this->collection))
return;
return [
'bbcodes' => $this->collection->asConfig(),
'quickMatch' => $this->quickMatch,
'regexp' => $this->regexp
];
}
}

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\Plugins\BBCodes\Configurator;
use s9e\TextFormatter\Configurator\Collections\NormalizedCollection;
use s9e\TextFormatter\Configurator\Validators\AttributeName;
class AttributeValueCollection extends NormalizedCollection
{
public function normalizeKey($key)
{
return AttributeName::normalize($key);
}
}

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\Plugins\BBCodes\Configurator;
use InvalidArgumentException;
use s9e\TextFormatter\Configurator\Collections\AttributeList;
use s9e\TextFormatter\Configurator\ConfigProvider;
use s9e\TextFormatter\Configurator\Helpers\ConfigHelper;
use s9e\TextFormatter\Configurator\JavaScript\Dictionary;
use s9e\TextFormatter\Configurator\Traits\Configurable;
use s9e\TextFormatter\Configurator\Validators\AttributeName;
use s9e\TextFormatter\Configurator\Validators\TagName;
class BBCode implements ConfigProvider
{
use Configurable;
protected $contentAttributes;
protected $defaultAttribute;
protected $forceLookahead = \false;
protected $predefinedAttributes;
protected $tagName;
public function __construct(array $options = \null)
{
$this->contentAttributes = new AttributeList;
$this->predefinedAttributes = new AttributeValueCollection;
if (isset($options))
foreach ($options as $optionName => $optionValue)
$this->__set($optionName, $optionValue);
}
public function asConfig()
{
$config = ConfigHelper::toArray(\get_object_vars($this));
if (!$this->forceLookahead)
unset($config['forceLookahead']);
if (isset($config['predefinedAttributes']))
$config['predefinedAttributes'] = new Dictionary($config['predefinedAttributes']);
return $config;
}
public static function normalizeName($bbcodeName)
{
if ($bbcodeName === '*')
return '*';
if (!TagName::isValid($bbcodeName))
throw new InvalidArgumentException("Invalid BBCode name '" . $bbcodeName . "'");
return TagName::normalize($bbcodeName);
}
public function setDefaultAttribute($attrName)
{
$this->defaultAttribute = AttributeName::normalize($attrName);
}
public function setTagName($tagName)
{
$this->tagName = TagName::normalize($tagName);
}
}

View File

@ -0,0 +1,52 @@
<?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\Plugins\BBCodes\Configurator;
use RuntimeException;
use s9e\TextFormatter\Configurator\Collections\NormalizedCollection;
use s9e\TextFormatter\Configurator\JavaScript\Dictionary;
use s9e\TextFormatter\Configurator\Validators\AttributeName;
use s9e\TextFormatter\Configurator\Validators\TagName;
class BBCodeCollection extends NormalizedCollection
{
protected $onDuplicateAction = 'replace';
protected function getAlreadyExistsException($key)
{
return new RuntimeException("BBCode '" . $key . "' already exists");
}
protected function getNotExistException($key)
{
return new RuntimeException("BBCode '" . $key . "' does not exist");
}
public function normalizeKey($key)
{
return BBCode::normalizeName($key);
}
public function normalizeValue($value)
{
return ($value instanceof BBCode)
? $value
: new BBCode($value);
}
public function asConfig()
{
$bbcodes = parent::asConfig();
foreach ($bbcodes as $bbcodeName => &$bbcode)
{
if (isset($bbcode['tagName'])
&& TagName::isValid($bbcodeName)
&& TagName::normalize($bbcodeName) === $bbcode['tagName'])
unset($bbcode['tagName']);
if (isset($bbcode['defaultAttribute'])
&& AttributeName::isValid($bbcodeName)
&& AttributeName::normalize($bbcodeName) === $bbcode['defaultAttribute'])
unset($bbcode['defaultAttribute']);
}
unset($bbcode);
return new Dictionary($bbcodes);
}
}

View File

@ -0,0 +1,471 @@
<?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\Plugins\BBCodes\Configurator;
use Exception;
use InvalidArgumentException;
use RuntimeException;
use s9e\TextFormatter\Configurator;
use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
use s9e\TextFormatter\Configurator\Items\Attribute;
use s9e\TextFormatter\Configurator\Items\ProgrammableCallback;
use s9e\TextFormatter\Configurator\Items\Tag;
use s9e\TextFormatter\Configurator\Items\Template;
class BBCodeMonkey
{
const REGEXP = '(.).*?(?<!\\\\)(?>\\\\\\\\)*+\\g{-1}[DSUisu]*';
public $allowedFilters = [
'addslashes',
'dechex',
'intval',
'json_encode',
'ltrim',
'mb_strtolower',
'mb_strtoupper',
'rawurlencode',
'rtrim',
'str_rot13',
'stripslashes',
'strrev',
'strtolower',
'strtotime',
'strtoupper',
'trim',
'ucfirst',
'ucwords',
'urlencode'
];
protected $configurator;
public $tokenRegexp = [
'ANYTHING' => '[\\s\\S]*?',
'COLOR' => '[a-zA-Z]+|#[0-9a-fA-F]+',
'EMAIL' => '[^@]+@.+?',
'FLOAT' => '(?>0|-?[1-9]\\d*)(?>\\.\\d+)?(?>e[1-9]\\d*)?',
'ID' => '[-a-zA-Z0-9_]+',
'IDENTIFIER' => '[-a-zA-Z0-9_]+',
'INT' => '0|-?[1-9]\\d*',
'INTEGER' => '0|-?[1-9]\\d*',
'NUMBER' => '\\d+',
'RANGE' => '\\d+',
'SIMPLETEXT' => '[-a-zA-Z0-9+.,_ ]+',
'TEXT' => '[\\s\\S]*?',
'UINT' => '0|[1-9]\\d*'
];
public $unfilteredTokens = [
'ANYTHING',
'TEXT'
];
public function __construct(Configurator $configurator)
{
$this->configurator = $configurator;
}
public function create($usage, $template)
{
$config = $this->parse($usage);
if (!($template instanceof Template))
$template = new Template($template);
$template->replaceTokens(
'#\\{(?:[A-Z]+[A-Z_0-9]*|@[-\\w]+)\\}#',
function ($m) use ($config)
{
$tokenId = \substr($m[0], 1, -1);
if ($tokenId[0] === '@')
return ['expression', $tokenId];
if (isset($config['tokens'][$tokenId]))
return ['expression', '@' . $config['tokens'][$tokenId]];
if ($tokenId === $config['passthroughToken'])
return ['passthrough'];
if ($this->isFilter($tokenId))
throw new RuntimeException('Token {' . $tokenId . '} is ambiguous or undefined');
return ['expression', '$' . $tokenId];
}
);
$return = [
'bbcode' => $config['bbcode'],
'bbcodeName' => $config['bbcodeName'],
'tag' => $config['tag']
];
$return['tag']->template = $template;
return $return;
}
protected function parse($usage)
{
$tag = new Tag;
$bbcode = new BBCode;
$config = [
'tag' => $tag,
'bbcode' => $bbcode,
'passthroughToken' => \null
];
$usage = \preg_replace_callback(
'#(\\{(?>HASH)?MAP=)([^:]+:[^,;}]+(?>,[^:]+:[^,;}]+)*)(?=[;}])#',
function ($m)
{
return $m[1] . \base64_encode($m[2]);
},
$usage
);
$usage = \preg_replace_callback(
'#(\\{(?:PARSE|REGEXP)=)(' . self::REGEXP . '(?:,' . self::REGEXP . ')*)#',
function ($m)
{
return $m[1] . \base64_encode($m[2]);
},
$usage
);
$regexp = '(^'
. '\\[(?<bbcodeName>\\S+?)'
. '(?<defaultAttribute>=.+?)?'
. '(?<attributes>(?:\\s+[^=]+=\\S+?)*?)?'
. '\\s*(?:/?\\]|\\]\\s*(?<content>.*?)\\s*(?<endTag>\\[/\\1]))$)i';
if (!\preg_match($regexp, \trim($usage), $m))
throw new InvalidArgumentException('Cannot interpret the BBCode definition');
$config['bbcodeName'] = BBCode::normalizeName($m['bbcodeName']);
$definitions = \preg_split('#\\s+#', \trim($m['attributes']), -1, \PREG_SPLIT_NO_EMPTY);
if (!empty($m['defaultAttribute']))
\array_unshift($definitions, $m['bbcodeName'] . $m['defaultAttribute']);
if (!empty($m['content']))
{
$regexp = '#^\\{' . RegexpBuilder::fromList($this->unfilteredTokens) . '[0-9]*\\}$#D';
if (\preg_match($regexp, $m['content']))
$config['passthroughToken'] = \substr($m['content'], 1, -1);
else
{
$definitions[] = 'content=' . $m['content'];
$bbcode->contentAttributes[] = 'content';
}
}
$attributeDefinitions = [];
foreach ($definitions as $definition)
{
$pos = \strpos($definition, '=');
$name = \substr($definition, 0, $pos);
$value = \preg_replace('(^"(.*?)")s', '$1', \substr($definition, 1 + $pos));
$value = \preg_replace_callback(
'#(\\{(?>HASHMAP|MAP|PARSE|REGEXP)=)([A-Za-z0-9+/]+=*)#',
function ($m)
{
return $m[1] . \base64_decode($m[2]);
},
$value
);
if ($name[0] === '$')
{
$optionName = \substr($name, 1);
$object = ($optionName === 'nestingLimit' || $optionName === 'tagLimit') ? $tag : $bbcode;
$object->$optionName = $this->convertValue($value);
}
elseif ($name[0] === '#')
{
$ruleName = \substr($name, 1);
foreach (\explode(',', $value) as $value)
$tag->rules->$ruleName($this->convertValue($value));
}
else
{
$attrName = \strtolower(\trim($name));
$attributeDefinitions[] = [$attrName, $value];
}
}
$tokens = $this->addAttributes($attributeDefinitions, $bbcode, $tag);
if (isset($tokens[$config['passthroughToken']]))
$config['passthroughToken'] = \null;
$config['tokens'] = \array_filter($tokens);
return $config;
}
protected function addAttributes(array $definitions, BBCode $bbcode, Tag $tag)
{
$composites = [];
$table = [];
foreach ($definitions as $_e874cdc7)
{
list($attrName, $definition) = $_e874cdc7;
if (!isset($bbcode->defaultAttribute))
$bbcode->defaultAttribute = $attrName;
$tokens = $this->parseTokens($definition);
if (empty($tokens))
throw new RuntimeException('No valid tokens found in ' . $attrName . "'s definition " . $definition);
if ($tokens[0]['content'] === $definition)
{
$token = $tokens[0];
if ($token['type'] === 'PARSE')
foreach ($token['regexps'] as $regexp)
$tag->attributePreprocessors->add($attrName, $regexp);
elseif (isset($tag->attributes[$attrName]))
throw new RuntimeException("Attribute '" . $attrName . "' is declared twice");
else
{
if (!empty($token['options']['useContent']))
$bbcode->contentAttributes[] = $attrName;
unset($token['options']['useContent']);
$tag->attributes[$attrName] = $this->generateAttribute($token);
$tokenId = $token['id'];
$table[$tokenId] = (isset($table[$tokenId]))
? \false
: $attrName;
}
}
else
$composites[] = [$attrName, $definition, $tokens];
}
foreach ($composites as $_2d84f0a0)
{
list($attrName, $definition, $tokens) = $_2d84f0a0;
$regexp = '/^';
$lastPos = 0;
$usedTokens = [];
foreach ($tokens as $token)
{
$tokenId = $token['id'];
$tokenType = $token['type'];
if ($tokenType === 'PARSE')
throw new RuntimeException('{PARSE} tokens can only be used has the sole content of an attribute');
if (isset($usedTokens[$tokenId]))
throw new RuntimeException('Token {' . $tokenId . '} used multiple times in attribute ' . $attrName . "'s definition");
$usedTokens[$tokenId] = 1;
if (isset($table[$tokenId]))
{
$matchName = $table[$tokenId];
if ($matchName === \false)
throw new RuntimeException('Token {' . $tokenId . "} used in attribute '" . $attrName . "' is ambiguous");
}
else
{
$i = 0;
do
{
$matchName = $attrName . $i;
++$i;
}
while (isset($tag->attributes[$matchName]));
$attribute = $tag->attributes->add($matchName);
if (!\in_array($tokenType, $this->unfilteredTokens, \true))
{
$filter = $this->configurator->attributeFilters->get('#' . \strtolower($tokenType));
$attribute->filterChain->append($filter);
}
$table[$tokenId] = $matchName;
}
$literal = \preg_quote(\substr($definition, $lastPos, $token['pos'] - $lastPos), '/');
$literal = \preg_replace('(\\s+)', '\\s+', $literal);
$regexp .= $literal;
$expr = (isset($this->tokenRegexp[$tokenType]))
? $this->tokenRegexp[$tokenType]
: '.+?';
$regexp .= '(?<' . $matchName . '>' . $expr . ')';
$lastPos = $token['pos'] + \strlen($token['content']);
}
$regexp .= \preg_quote(\substr($definition, $lastPos), '/') . '$/D';
$tag->attributePreprocessors->add($attrName, $regexp);
}
$newAttributes = [];
foreach ($tag->attributePreprocessors as $attributePreprocessor)
foreach ($attributePreprocessor->getAttributes() as $attrName => $regexp)
{
if (isset($tag->attributes[$attrName]))
continue;
if (isset($newAttributes[$attrName])
&& $newAttributes[$attrName] !== $regexp)
throw new RuntimeException("Ambiguous attribute '" . $attrName . "' created using different regexps needs to be explicitly defined");
$newAttributes[$attrName] = $regexp;
}
foreach ($newAttributes as $attrName => $regexp)
{
$filter = $this->configurator->attributeFilters->get('#regexp');
$tag->attributes->add($attrName)->filterChain->append($filter)->setRegexp($regexp);
}
return $table;
}
protected function convertValue($value)
{
if ($value === 'true')
return \true;
if ($value === 'false')
return \false;
return $value;
}
protected function parseTokens($definition)
{
$tokenTypes = [
'choice' => 'CHOICE[0-9]*=(?<choices>.+?)',
'map' => '(?:HASH)?MAP[0-9]*=(?<map>.+?)',
'parse' => 'PARSE=(?<regexps>' . self::REGEXP . '(?:,' . self::REGEXP . ')*)',
'range' => 'RAN(?:DOM|GE)[0-9]*=(?<min>-?[0-9]+),(?<max>-?[0-9]+)',
'regexp' => 'REGEXP[0-9]*=(?<regexp>' . self::REGEXP . ')',
'other' => '(?<other>[A-Z_]+[0-9]*)'
];
\preg_match_all(
'#\\{(' . \implode('|', $tokenTypes) . ')(?<options>\\??(?:;[^;]*)*)\\}#',
$definition,
$matches,
\PREG_SET_ORDER | \PREG_OFFSET_CAPTURE
);
$tokens = [];
foreach ($matches as $m)
{
if (isset($m['other'][0])
&& \preg_match('#^(?:CHOICE|HASHMAP|MAP|REGEXP|PARSE|RANDOM|RANGE)#', $m['other'][0]))
throw new RuntimeException("Malformed token '" . $m['other'][0] . "'");
$token = [
'pos' => $m[0][1],
'content' => $m[0][0],
'options' => (isset($m['options'][0])) ? $this->parseOptionString($m['options'][0]) : []
];
$head = $m[1][0];
$pos = \strpos($head, '=');
if ($pos === \false)
$token['id'] = $head;
else
{
$token['id'] = \substr($head, 0, $pos);
foreach ($m as $k => $v)
if (!\is_numeric($k) && $k !== 'options' && $v[1] !== -1)
$token[$k] = $v[0];
}
$token['type'] = \rtrim($token['id'], '0123456789');
if ($token['type'] === 'PARSE')
{
\preg_match_all('#' . self::REGEXP . '(?:,|$)#', $token['regexps'], $m);
$regexps = [];
foreach ($m[0] as $regexp)
$regexps[] = \rtrim($regexp, ',');
$token['regexps'] = $regexps;
}
$tokens[] = $token;
}
return $tokens;
}
protected function generateAttribute(array $token)
{
$attribute = new Attribute;
if (isset($token['options']['preFilter']))
{
$this->appendFilters($attribute, $token['options']['preFilter']);
unset($token['options']['preFilter']);
}
if ($token['type'] === 'REGEXP')
{
$filter = $this->configurator->attributeFilters->get('#regexp');
$attribute->filterChain->append($filter)->setRegexp($token['regexp']);
}
elseif ($token['type'] === 'RANGE')
{
$filter = $this->configurator->attributeFilters->get('#range');
$attribute->filterChain->append($filter)->setRange($token['min'], $token['max']);
}
elseif ($token['type'] === 'RANDOM')
{
$attribute->generator = new ProgrammableCallback('mt_rand');
$attribute->generator->addParameterByValue((int) $token['min']);
$attribute->generator->addParameterByValue((int) $token['max']);
}
elseif ($token['type'] === 'CHOICE')
{
$filter = $this->configurator->attributeFilters->get('#choice');
$attribute->filterChain->append($filter)->setValues(
\explode(',', $token['choices']),
!empty($token['options']['caseSensitive'])
);
unset($token['options']['caseSensitive']);
}
elseif ($token['type'] === 'HASHMAP' || $token['type'] === 'MAP')
{
$map = [];
foreach (\explode(',', $token['map']) as $pair)
{
$pos = \strpos($pair, ':');
if ($pos === \false)
throw new RuntimeException("Invalid map assignment '" . $pair . "'");
$map[\substr($pair, 0, $pos)] = \substr($pair, 1 + $pos);
}
if ($token['type'] === 'HASHMAP')
{
$filter = $this->configurator->attributeFilters->get('#hashmap');
$attribute->filterChain->append($filter)->setMap(
$map,
!empty($token['options']['strict'])
);
}
else
{
$filter = $this->configurator->attributeFilters->get('#map');
$attribute->filterChain->append($filter)->setMap(
$map,
!empty($token['options']['caseSensitive']),
!empty($token['options']['strict'])
);
}
unset($token['options']['caseSensitive']);
unset($token['options']['strict']);
}
elseif (!\in_array($token['type'], $this->unfilteredTokens, \true))
{
$filter = $this->configurator->attributeFilters->get('#' . $token['type']);
$attribute->filterChain->append($filter);
}
if (isset($token['options']['postFilter']))
{
$this->appendFilters($attribute, $token['options']['postFilter']);
unset($token['options']['postFilter']);
}
if (isset($token['options']['required']))
$token['options']['required'] = (bool) $token['options']['required'];
elseif (isset($token['options']['optional']))
$token['options']['required'] = !$token['options']['optional'];
unset($token['options']['optional']);
foreach ($token['options'] as $k => $v)
$attribute->$k = $v;
return $attribute;
}
protected function appendFilters(Attribute $attribute, $filters)
{
foreach (\preg_split('#\\s*,\\s*#', $filters) as $filterName)
{
if (\substr($filterName, 0, 1) !== '#'
&& !\in_array($filterName, $this->allowedFilters, \true))
throw new RuntimeException("Filter '" . $filterName . "' is not allowed");
$filter = $this->configurator->attributeFilters->get($filterName);
$attribute->filterChain->append($filter);
}
}
protected function isFilter($tokenId)
{
$filterName = \rtrim($tokenId, '0123456789');
if (\in_array($filterName, $this->unfilteredTokens, \true))
return \true;
try
{
if ($this->configurator->attributeFilters->get('#' . $filterName))
return \true;
}
catch (Exception $e)
{
}
return \false;
}
protected function parseOptionString($string)
{
$string = \preg_replace('(^\\?)', ';optional', $string);
$options = [];
foreach (\preg_split('#;+#', $string, -1, \PREG_SPLIT_NO_EMPTY) as $pair)
{
$pos = \strpos($pair, '=');
if ($pos === \false)
{
$k = $pair;
$v = \true;
}
else
{
$k = \substr($pair, 0, $pos);
$v = \substr($pair, 1 + $pos);
}
$options[$k] = $v;
}
return $options;
}
}

View File

@ -0,0 +1,85 @@
<?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\Plugins\BBCodes\Configurator;
use DOMDocument;
use DOMElement;
use DOMXPath;
use InvalidArgumentException;
use RuntimeException;
class Repository
{
protected $bbcodeMonkey;
protected $dom;
public function __construct($value, BBCodeMonkey $bbcodeMonkey)
{
if (!($value instanceof DOMDocument))
{
if (!\file_exists($value))
throw new InvalidArgumentException('Not a DOMDocument or the path to a repository file');
$dom = new DOMDocument;
$dom->preserveWhiteSpace = \false;
$useErrors = \libxml_use_internal_errors(\true);
$success = $dom->load($value);
\libxml_use_internal_errors($useErrors);
if (!$success)
throw new InvalidArgumentException('Invalid repository file');
$value = $dom;
}
$this->bbcodeMonkey = $bbcodeMonkey;
$this->dom = $value;
}
public function get($name, array $vars = [])
{
$name = \preg_replace_callback(
'/^[^#]+/',
function ($m)
{
return BBCode::normalizeName($m[0]);
},
$name
);
$xpath = new DOMXPath($this->dom);
$node = $xpath->query('//bbcode[@name="' . \htmlspecialchars($name) . '"]')->item(0);
if (!($node instanceof DOMElement))
throw new RuntimeException("Could not find '" . $name . "' in repository");
$clonedNode = $node->cloneNode(\true);
foreach ($xpath->query('.//var', $clonedNode) as $varNode)
{
$varName = $varNode->getAttribute('name');
if (isset($vars[$varName]))
$varNode->parentNode->replaceChild(
$this->dom->createTextNode($vars[$varName]),
$varNode
);
}
$usage = $xpath->evaluate('string(usage)', $clonedNode);
$template = $xpath->evaluate('string(template)', $clonedNode);
$config = $this->bbcodeMonkey->create($usage, $template);
$bbcode = $config['bbcode'];
$bbcodeName = $config['bbcodeName'];
$tag = $config['tag'];
if ($node->hasAttribute('tagName'))
$bbcode->tagName = $node->getAttribute('tagName');
foreach ($xpath->query('rules/*', $node) as $ruleNode)
{
$methodName = $ruleNode->nodeName;
$args = [];
if ($ruleNode->textContent)
$args[] = $ruleNode->textContent;
\call_user_func_array([$tag->rules, $methodName], $args);
}
foreach ($node->getElementsByTagName('predefinedAttributes') as $predefinedAttributes)
foreach ($predefinedAttributes->attributes as $attribute)
$bbcode->predefinedAttributes->set($attribute->name, $attribute->value);
return [
'bbcode' => $bbcode,
'bbcodeName' => $bbcodeName,
'tag' => $tag
];
}
}

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\Plugins\BBCodes\Configurator;
use s9e\TextFormatter\Configurator;
use s9e\TextFormatter\Configurator\Collections\NormalizedCollection;
class RepositoryCollection extends NormalizedCollection
{
protected $bbcodeMonkey;
public function __construct(BBCodeMonkey $bbcodeMonkey)
{
$this->bbcodeMonkey = $bbcodeMonkey;
}
public function normalizeValue($value)
{
return ($value instanceof Repository)
? $value
: new Repository($value, $this->bbcodeMonkey);
}
}

View File

@ -0,0 +1,498 @@
<?xml version="1.0" encoding="utf-8" ?>
<repository>
<bbcode name="ACRONYM">
<usage>[ACRONYM title={TEXT1?}]{TEXT2}[/ACRONYM]</usage>
<template><![CDATA[
<acronym title="{TEXT1}">{TEXT2}</acronym>
]]></template>
</bbcode>
<bbcode name="ALIGN">
<usage>[ALIGN={CHOICE=left,right,center,justify}]{TEXT}[/ALIGN]</usage>
<template><![CDATA[
<div style="text-align:{CHOICE}">{TEXT}</div>
]]></template>
</bbcode>
<bbcode name="B">
<usage>[B]{TEXT}[/B]</usage>
<template><![CDATA[
<b><xsl:apply-templates /></b>
]]></template>
</bbcode>
<bbcode name="BACKGROUND">
<usage>[BACKGROUND={COLOR}]{TEXT}[/BACKGROUND]</usage>
<template><![CDATA[
<span style="background-color:{COLOR}">{TEXT}</span>
]]></template>
</bbcode>
<bbcode name="C">
<usage>[C]{TEXT}[/C]</usage>
<template><![CDATA[
<code class="inline"><xsl:apply-templates /></code>
]]></template>
</bbcode>
<bbcode name="CENTER">
<usage>[CENTER]{TEXT}[/CENTER]</usage>
<template><![CDATA[
<div style="text-align:center">{TEXT}</div>
]]></template>
</bbcode>
<!-- [CODE] BBCode, uses Hightlight.js for highlighting: https://highlightjs.org/ -->
<bbcode name="CODE">
<usage>[CODE lang={IDENTIFIER?}]{TEXT}[/CODE]</usage>
<template><![CDATA[
<pre data-hljs="" data-s9e-livepreview-postprocess="if('undefined'!==typeof hljs)hljs._hb(this)"><code>
<xsl:if test="@lang">
<xsl:attribute name="class">language-<xsl:value-of select="@lang"/></xsl:attribute>
</xsl:if>
<xsl:apply-templates />
</code></pre>
<script>if("undefined"!==typeof hljs)hljs._ha();else if("undefined"===typeof hljsLoading){hljsLoading=1;var a=document.getElementsByTagName("head")[0],e=document.createElement("link");e.type="text/css";e.rel="stylesheet";e.href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/styles/default.min.css";a.appendChild(e);e=document.createElement("script");e.type="text/javascript";e.onload=function(){var d={},f=0;hljs._hb=function(b){b.removeAttribute("data-hljs");var c=b.innerHTML;c in d?b.innerHTML=d[c]:(7&lt;++f&amp;&amp;(d={},f=0),hljs.highlightBlock(b.firstChild),d[c]=b.innerHTML)};hljs._ha=function(){for(var b=document.querySelectorAll("pre[data-hljs]"),c=b.length;0&lt;c;)hljs._hb(b.item(--c))};hljs._ha()};e.async=!0;e.src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/highlight.min.js";a.appendChild(e)}</script>
]]></template>
<ignore><![CDATA[
if (typeof hljs !== 'undefined')
{
hljs['_ha']();
}
else if (typeof hljsLoading === 'undefined')
{
hljsLoading = 1;
var head = document.getElementsByTagName('head')[0],
el = document.createElement('link');
el.type = 'text/css';
el.rel = 'stylesheet';
el.href = '//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/styles/default.min.css';
head.appendChild(el);
el = document.createElement('script');
el.type = 'text/javascript';
el.onload = function()
{
var cache = {}, cnt = 0;
// Highlight block
hljs['_hb'] = function (block)
{
block.removeAttribute('data-hljs');
var html = block.innerHTML;
if (html in cache)
{
block.innerHTML = cache[html];
}
else
{
if (++cnt > 7)
{
cache = {};
cnt = 0;
}
hljs['highlightBlock'](block.firstChild);
cache[html] = block.innerHTML;
}
};
// Highlight all
hljs['_ha'] = function ()
{
var blocks = document.querySelectorAll('pre[data-hljs]'),
i = blocks.length;
while (i > 0)
{
hljs['_hb'](blocks.item(--i));
}
};
hljs['_ha']();
};
el.async = true;
el.src = '//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/highlight.min.js';
head.appendChild(el);
}
]]></ignore>
</bbcode>
<bbcode name="COLOR">
<usage>[COLOR={COLOR}]{TEXT}[/COLOR]</usage>
<template><![CDATA[
<span style="color:{COLOR}">{TEXT}</span>
]]></template>
</bbcode>
<bbcode name="DD">
<usage>[DD]{TEXT}[/DD]</usage>
<template><![CDATA[
<dd>{TEXT}</dd>
]]></template>
</bbcode>
<bbcode name="DEL">
<usage>[DEL]{TEXT}[/DEL]</usage>
<template><![CDATA[
<del>{TEXT}</del>
]]></template>
</bbcode>
<bbcode name="DL">
<usage>[DL]{TEXT}[/DL]</usage>
<template><![CDATA[
<dl>{TEXT}</dl>
]]></template>
</bbcode>
<bbcode name="DT">
<usage>[DT]{TEXT}[/DT]</usage>
<template><![CDATA[
<dt>{TEXT}</dt>
]]></template>
</bbcode>
<bbcode name="EM">
<usage>[EM]{TEXT}[/EM]</usage>
<template><![CDATA[
<em>{TEXT}</em>
]]></template>
</bbcode>
<bbcode name="EMAIL">
<usage>[EMAIL={EMAIL;useContent}]{TEXT}[/EMAIL]</usage>
<template><![CDATA[
<a href="mailto:{EMAIL}">{TEXT}</a>
]]></template>
</bbcode>
<bbcode name="FLASH">
<!-- The size of the object is set to range from 0x0 to 1920x1080 and defaults to 80x60 -->
<usage><![CDATA[[FLASH={PARSE=/^(?<width>\d+),(?<height>\d+)/} width={RANGE=]]><var name="minWidth" description="Minimum width for the Flash object">0</var>,<var name="maxWidth" description="Maximum width for the Flash object">1920</var><![CDATA[;defaultValue=80} height={RANGE=]]><var name="minHeight" description="Minimum height for the Flash object">0</var>,<var name="maxHeight" description="Maximum height for the Flash object">1080</var><![CDATA[;defaultValue=60} url={URL;useContent}]
]]></usage>
<template><![CDATA[
<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://fpdownload.macromedia.com/get/shockwave/cabs/flash/swflash.cab#version=7,0,0,0" width="{@width}" height="{@height}">
<param name="movie" value="{@url}" />
<param name="quality" value="high" />
<param name="wmode" value="opaque" />
<param name="play" value="false" />
<param name="loop" value="false" />
<param name="allowScriptAccess" value="never" />
<param name="allowNetworking" value="internal" />
<embed src="{@url}" quality="high" width="{@width}" height="{@height}" wmode="opaque" type="application/x-shockwave-flash" pluginspage="http://www.adobe.com/go/getflashplayer" play="false" loop="false" allowscriptaccess="never" allownetworking="internal"></embed>
</object>
]]></template>
</bbcode>
<bbcode name="FLOAT">
<usage>[float={CHOICE=left,right,none}]{TEXT}[/float]</usage>
<template><![CDATA[
<div style="float:{CHOICE}">{TEXT}</div>
]]></template>
</bbcode>
<bbcode name="FONT">
<usage>[font={FONTFAMILY}]{TEXT}[/font]</usage>
<template><![CDATA[
<span style="font-family:{FONTFAMILY}">{TEXT}</span>
]]></template>
</bbcode>
<bbcode name="H1">
<usage>[H1]{TEXT}[/H1]</usage>
<template><![CDATA[
<h1>{TEXT}</h1>
]]></template>
</bbcode>
<bbcode name="H2">
<usage>[H2]{TEXT}[/H2]</usage>
<template><![CDATA[
<h2>{TEXT}</h2>
]]></template>
</bbcode>
<bbcode name="H3">
<usage>[H3]{TEXT}[/H3]</usage>
<template><![CDATA[
<h3>{TEXT}</h3>
]]></template>
</bbcode>
<bbcode name="H4">
<usage>[H4]{TEXT}[/H4]</usage>
<template><![CDATA[
<h4>{TEXT}</h4>
]]></template>
</bbcode>
<bbcode name="H5">
<usage>[H5]{TEXT}[/H5]</usage>
<template><![CDATA[
<h5>{TEXT}</h5>
]]></template>
</bbcode>
<bbcode name="H6">
<usage>[H6]{TEXT}[/H6]</usage>
<template><![CDATA[
<h6>{TEXT}</h6>
]]></template>
</bbcode>
<bbcode name="HR">
<usage>[HR]</usage>
<template><![CDATA[<hr/>]]></template>
</bbcode>
<bbcode name="I">
<usage>[I]{TEXT}[/I]</usage>
<template><![CDATA[
<i>{TEXT}</i>
]]></template>
</bbcode>
<bbcode name="IMG">
<usage>[IMG src={URL;useContent} title={TEXT?} alt={TEXT?} height={UINT?} width={UINT?} ]</usage>
<template><![CDATA[
<img src="{@src}" title="{@title}" alt="{@alt}">
<xsl:copy-of select="@height"/>
<xsl:copy-of select="@width"/>
</img>
]]></template>
</bbcode>
<bbcode name="INS">
<usage>[INS]{TEXT}[/INS]</usage>
<template><![CDATA[
<ins>{TEXT}</ins>
]]></template>
</bbcode>
<bbcode name="JUSTIFY">
<usage>[JUSTIFY]{TEXT}[/JUSTIFY]</usage>
<template><![CDATA[
<div style="text-align:justify">{TEXT}</div>
]]></template>
</bbcode>
<bbcode name="LEFT">
<usage>[LEFT]{TEXT}[/LEFT]</usage>
<template><![CDATA[
<div style="text-align:left">{TEXT}</div>
]]></template>
</bbcode>
<bbcode name="LIST">
<usage>[LIST type={HASHMAP=1:decimal,a:lower-alpha,A:upper-alpha,i:lower-roman,I:upper-roman;optional;postFilter=#simpletext} start={UINT;optional} #createChild=LI]{TEXT}[/LIST]</usage>
<template><![CDATA[
<xsl:choose>
<xsl:when test="not(@type)">
<ul><xsl:apply-templates /></ul>
</xsl:when>
<xsl:when test="starts-with(@type,'decimal') or starts-with(@type,'lower') or starts-with(@type,'upper')">
<ol style="list-style-type:{@type}"><xsl:copy-of select="@start"/><xsl:apply-templates /></ol>
</xsl:when>
<xsl:otherwise>
<ul style="list-style-type:{@type}"><xsl:apply-templates /></ul>
</xsl:otherwise>
</xsl:choose>
]]></template>
</bbcode>
<bbcode name="*" tagName="LI">
<usage>[*]{TEXT}[/*]</usage>
<template><![CDATA[
<li><xsl:apply-templates /></li>
]]></template>
</bbcode>
<bbcode name="MAGNET">
<usage>[MAGNET={REGEXP=/^magnet:/;useContent}]{TEXT}[/MAGNET]</usage>
<!-- Includes a public domain image from http://commons.wikimedia.org/wiki/File:TPB_Magnet_Icon.gif -->
<template><![CDATA[
<a href="{REGEXP}"><img alt="" src="data:image/gif;base64,R0lGODlhDAAMALMPAOXl5ewvErW1tebm5oocDkVFRePj47a2ts0WAOTk5MwVAIkcDesuEs0VAEZGRv///yH5BAEAAA8ALAAAAAAMAAwAAARB8MnnqpuzroZYzQvSNMroUeFIjornbK1mVkRzUgQSyPfbFi/dBRdzCAyJoTFhcBQOiYHyAABUDsiCxAFNWj6UbwQAOw==" style="vertical-align:middle;border:0;margin:0 5px 0 0"/>{TEXT}</a>
]]></template>
</bbcode>
<bbcode name="NOPARSE">
<usage>[NOPARSE #ignoreTags=true]{TEXT}[/NOPARSE]</usage>
<template>{TEXT}</template>
</bbcode>
<bbcode name="OL">
<usage>[OL]{TEXT}[/OL]</usage>
<template><![CDATA[
<ol>{TEXT}</ol>
]]></template>
</bbcode>
<bbcode name="QUOTE">
<usage>[QUOTE author={TEXT?}]{TEXT}[/QUOTE]</usage>
<template><![CDATA[
<blockquote>
<xsl:if test="not(@author)">
<xsl:attribute name="class">uncited</xsl:attribute>
</xsl:if>
<div>
<xsl:if test="@author">
<cite>]]>
<var name="authorStr"><![CDATA[<xsl:value-of select="@author" /> wrote:]]></var>
<![CDATA[</cite>
</xsl:if>
<xsl:apply-templates />
</div>
</blockquote>
]]></template>
</bbcode>
<bbcode name="RIGHT">
<usage>[RIGHT]{TEXT}[/RIGHT]</usage>
<template><![CDATA[
<div style="text-align:right">{TEXT}</div>
]]></template>
</bbcode>
<bbcode name="S">
<usage>[S]{TEXT}[/S]</usage>
<template><![CDATA[
<s>{TEXT}</s>
]]></template>
</bbcode>
<bbcode name="SIZE">
<usage>[SIZE={RANGE=<var name="min">8</var>,<var name="max">36</var>}]{TEXT}[/SIZE]</usage>
<template><![CDATA[
<span style="font-size:{RANGE}px">{TEXT}</span>
]]></template>
</bbcode>
<bbcode name="SPOILER">
<usage>[SPOILER title={TEXT1?}]{TEXT2}[/SPOILER]</usage>
<!--
var nextSiblingStyle = parentNode.nextSibling.style,
firstChildStyle = firstChild.style,
lastChildStyle = lastChild.style;
firstChildStyle.display = nextSiblingStyle.display;
nextSiblingStyle.display = lastChildStyle.display = (firstChildStyle.display) ? '' : 'none';
-->
<template><![CDATA[
<div class="spoiler">
<div class="spoiler-header">
<button onclick="var a=parentNode.nextSibling.style,b=firstChild.style,c=lastChild.style;b.display=a.display;a.display=c.display=(b.display)?'':'none'"><span>]]><var name="showStr">Show</var><![CDATA[</span><span style="display:none">]]><var name="hideStr">Hide</var><![CDATA[</span></button>
<span class="spoiler-title">]]><var name="spoilerStr">Spoiler:</var><![CDATA[ {TEXT1}</span>
</div>
<div class="spoiler-content" style="display:none">{TEXT2}</div>
</div>
]]></template>
</bbcode>
<bbcode name="STRONG">
<usage>[STRONG]{TEXT}[/STRONG]</usage>
<template><![CDATA[
<strong>{TEXT}</strong>
]]></template>
</bbcode>
<bbcode name="SUB">
<usage>[SUB]{TEXT}[/SUB]</usage>
<template><![CDATA[
<sub>{TEXT}</sub>
]]></template>
</bbcode>
<bbcode name="SUP">
<usage>[SUP]{TEXT}[/SUP]</usage>
<template><![CDATA[
<sup>{TEXT}</sup>
]]></template>
</bbcode>
<bbcode name="TABLE">
<usage>[TABLE]{ANYTHING}[/TABLE]</usage>
<template><![CDATA[
<table>{ANYTHING}</table>
]]></template>
</bbcode>
<bbcode name="TBODY">
<usage>[TBODY]{ANYTHING}[/TBODY]</usage>
<template><![CDATA[
<tbody>{ANYTHING}</tbody>
]]></template>
</bbcode>
<bbcode name="TD">
<usage>[TD align={CHOICE=left,center,right,justify;caseSensitive;optional;preFilter=strtolower} #createParagraphs=false]{TEXT}[/TD]</usage>
<template><![CDATA[
<td>
<xsl:if test="@align">
<xsl:attribute name="style">text-align:{CHOICE}</xsl:attribute>
</xsl:if>
<xsl:apply-templates/>
</td>
]]></template>
</bbcode>
<bbcode name="TH">
<usage>[TH align={CHOICE=left,center,right,justify;caseSensitive;optional;preFilter=strtolower} #createParagraphs=false]{TEXT}[/TH]</usage>
<template><![CDATA[
<th>
<xsl:if test="@align">
<xsl:attribute name="style">text-align:{CHOICE}</xsl:attribute>
</xsl:if>
<xsl:apply-templates/>
</th>
]]></template>
</bbcode>
<bbcode name="THEAD">
<usage>[THEAD]{ANYTHING}[/THEAD]</usage>
<template><![CDATA[
<thead>{ANYTHING}</thead>
]]></template>
</bbcode>
<bbcode name="TR">
<usage>[TR]{ANYTHING}[/TR]</usage>
<template><![CDATA[
<tr>{ANYTHING}</tr>
]]></template>
</bbcode>
<bbcode name="U">
<usage>[U]{TEXT}[/U]</usage>
<template><![CDATA[
<u>{TEXT}</u>
]]></template>
</bbcode>
<bbcode name="UL">
<usage>[UL]{TEXT}[/UL]</usage>
<template><![CDATA[
<ul>{TEXT}</ul>
]]></template>
</bbcode>
<bbcode name="URL">
<usage>[URL={URL;useContent} title={TEXT?}]{TEXT}[/URL]</usage>
<template><![CDATA[
<a href="{@url}"><xsl:copy-of select="@title" /><xsl:apply-templates /></a>
]]></template>
</bbcode>
<bbcode name="VAR">
<usage>[VAR]{TEXT}[/VAR]</usage>
<template><![CDATA[
<var>{TEXT}</var>
]]></template>
</bbcode>
</repository>

View File

@ -0,0 +1,377 @@
/**
* @type {!Object} Attributes of the BBCode being parsed
*/
var attributes;
/**
* @type {!Object} Configuration for the BBCode being parsed
*/
var bbcodeConfig;
/**
* @type {!string} Name of the BBCode being parsed
*/
var bbcodeName;
/**
* @type {!string} Suffix of the BBCode being parsed, including its colon
*/
var bbcodeSuffix;
/**
* @type {!number} Position of the cursor in the original text
*/
var pos;
/**
* @type {!number} Position of the start of the BBCode being parsed
*/
var startPos;
/**
* @type {!number} Length of the text being parsed
*/
var textLen = text.length;
/**
* @type {!string} Text being parsed, normalized to uppercase
*/
var uppercaseText = '';
matches.forEach(function(m)
{
bbcodeName = m[1][0].toUpperCase();
if (!(bbcodeName in config.bbcodes))
{
return;
}
bbcodeConfig = config.bbcodes[bbcodeName];
startPos = m[0][1];
pos = startPos + m[0][0].length;
try
{
parseBBCode();
}
catch (e)
{
// Do nothing
}
});
/**
* Add the end tag that matches current BBCode
*
* @return {!Tag}
*/
function addBBCodeEndTag()
{
return addEndTag(getTagName(), startPos, pos - startPos);
}
/**
* Add the self-closing tag that matches current BBCode
*
* @return {!Tag}
*/
function addBBCodeSelfClosingTag()
{
var tag = addSelfClosingTag(getTagName(), startPos, pos - startPos);
tag.setAttributes(attributes);
return tag;
}
/**
* Add the start tag that matches current BBCode
*
* @return {!Tag}
*/
function addBBCodeStartTag()
{
var tag = addStartTag(getTagName(), startPos, pos - startPos);
tag.setAttributes(attributes);
return tag;
}
/**
* Parse the end tag that matches given BBCode name and suffix starting at current position
*
* @return {Tag}
*/
function captureEndTag()
{
if (!uppercaseText)
{
uppercaseText = text.toUpperCase();
}
var match = '[/' + bbcodeName + bbcodeSuffix + ']',
endTagPos = uppercaseText.indexOf(match, pos);
if (endTagPos < 0)
{
return null;
}
return addEndTag(getTagName(), endTagPos, match.length);
}
/**
* Get the tag name for current BBCode
*
* @return string
*/
function getTagName()
{
// Use the configured tagName if available, or reuse the BBCode's name otherwise
return bbcodeConfig.tagName || bbcodeName;
}
/**
* Parse attributes starting at current position
*
* @return array Associative array of [name => value]
*/
function parseAttributes()
{
var firstPos = pos, attrName;
attributes = {};
while (pos < textLen)
{
var c = text[pos];
if (" \n\t".indexOf(c) > -1)
{
++pos;
continue;
}
if ('/]'.indexOf(c) > -1)
{
return;
}
// Capture the attribute name
var spn = /^[-\w]*/.exec(text.substr(pos, 100))[0].length;
if (spn)
{
attrName = text.substr(pos, spn).toLowerCase();
pos += spn;
if (pos >= textLen)
{
// The attribute name extends to the end of the text
throw '';
}
if (text[pos] !== '=')
{
// It's an attribute name not followed by an equal sign, ignore it
continue;
}
}
else if (c === '=' && pos === firstPos)
{
// This is the default param, e.g. [quote=foo]
attrName = bbcodeConfig.defaultAttribute || bbcodeName.toLowerCase();
}
else
{
throw '';
}
// Move past the = and make sure we're not at the end of the text
if (++pos >= textLen)
{
throw '';
}
attributes[attrName] = parseAttributeValue();
}
}
/**
* Parse the attribute value starting at current position
*
* @return string
*/
function parseAttributeValue()
{
// Test whether the value is in quotes
if (text[pos] === '"' || text[pos] === "'")
{
return parseQuotedAttributeValue();
}
// Capture everything up to whichever comes first:
// - an endline
// - whitespace followed by a slash and a closing bracket
// - a closing bracket, optionally preceded by whitespace
// - whitespace followed by another attribute (name followed by equal sign)
//
// NOTE: this is for compatibility with some forums (such as vBulletin it seems)
// that do not put attribute values in quotes, e.g.
// [quote=John Smith;123456] (quoting "John Smith" from post #123456)
var match = /[^\]\n]*?(?=\s*(?:\s\/)?\]|\s+[-\w]+=)/.exec(text.substr(pos));
if (!match)
{
throw '';
}
var attrValue = match[0];
pos += attrValue.length;
return attrValue;
}
/**
* Parse current BBCode
*
* @return void
*/
function parseBBCode()
{
parseBBCodeSuffix();
// Test whether this is an end tag
if (text[startPos + 1] === '/')
{
// Test whether the tag is properly closed and whether this tag has an identifier.
// We skip end tags that carry an identifier because they're automatically added
// when their start tag is processed
if (text[pos] === ']' && bbcodeSuffix === '')
{
++pos;
addBBCodeEndTag();
}
return;
}
// Parse attributes and fill in the blanks with predefined attributes
parseAttributes();
if (bbcodeConfig.predefinedAttributes)
{
for (var attrName in bbcodeConfig.predefinedAttributes)
{
if (!(attrName in attributes))
{
attributes[attrName] = bbcodeConfig.predefinedAttributes[attrName];
}
}
}
// Test whether the tag is properly closed
if (text[pos] === ']')
{
++pos;
}
else
{
// Test whether this is a self-closing tag
if (text.substr(pos, 2) === '/]')
{
pos += 2;
addBBCodeSelfClosingTag();
}
return;
}
// Record the names of attributes that need the content of this tag
var contentAttributes = [];
if (bbcodeConfig.contentAttributes)
{
bbcodeConfig.contentAttributes.forEach(function(attrName)
{
if (!(attrName in attributes))
{
contentAttributes.push(attrName);
}
});
}
// Look ahead and parse the end tag that matches this tag, if applicable
var requireEndTag = (bbcodeSuffix || bbcodeConfig.forceLookahead),
endTag = (requireEndTag || contentAttributes.length) ? captureEndTag() : null;
if (endTag)
{
contentAttributes.forEach(function(attrName)
{
attributes[attrName] = text.substr(pos, endTag.getPos() - pos);
});
}
else if (requireEndTag)
{
return;
}
// Create this start tag
var tag = addBBCodeStartTag();
// If an end tag was created, pair it with this start tag
if (endTag)
{
tag.pairWith(endTag);
}
}
/**
* Parse the BBCode suffix starting at current position
*
* Used to explicitly pair specific tags together, e.g.
* [code:123][code]type your code here[/code][/code:123]
*
* @return void
*/
function parseBBCodeSuffix()
{
bbcodeSuffix = '';
if (text[pos] === ':')
{
// Capture the colon and the (0 or more) digits following it
bbcodeSuffix = /^:\d*/.exec(text.substr(pos))[0];
// Move past the suffix
pos += bbcodeSuffix.length;
}
}
/**
* Parse a quoted attribute value that starts at current offset
*
* @return {!string}
*/
function parseQuotedAttributeValue()
{
var quote = text[pos],
valuePos = pos + 1;
while (1)
{
// Look for the next quote
pos = text.indexOf(quote, pos + 1);
if (pos < 0)
{
// No matching quote. Apparently that string never ends...
throw '';
}
// Test for an odd number of backslashes before this character
var n = 0;
do
{
++n;
}
while (text[pos - n] === '\\');
if (n % 2)
{
// If n is odd, it means there's an even number of backslashes. We can exit this loop
break;
}
}
// Unescape special characters ' " and \
var attrValue = text.substr(valuePos, pos - valuePos).replace(/\\([\\'"])/g, '$1');
// Skip past the closing quote
++pos;
return attrValue;
}

View File

@ -0,0 +1,200 @@
<?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\Plugins\BBCodes;
use RuntimeException;
use s9e\TextFormatter\Parser\Tag;
use s9e\TextFormatter\Plugins\ParserBase;
class Parser extends ParserBase
{
protected $attributes;
protected $bbcodeConfig;
protected $bbcodeName;
protected $bbcodeSuffix;
protected $pos;
protected $startPos;
protected $text;
protected $textLen;
protected $uppercaseText;
public function parse($text, array $matches)
{
$this->text = $text;
$this->textLen = \strlen($text);
$this->uppercaseText = '';
foreach ($matches as $m)
{
$this->bbcodeName = \strtoupper($m[1][0]);
if (!isset($this->config['bbcodes'][$this->bbcodeName]))
continue;
$this->bbcodeConfig = $this->config['bbcodes'][$this->bbcodeName];
$this->startPos = $m[0][1];
$this->pos = $this->startPos + \strlen($m[0][0]);
try
{
$this->parseBBCode();
}
catch (RuntimeException $e)
{
}
}
}
protected function addBBCodeEndTag()
{
return $this->parser->addEndTag($this->getTagName(), $this->startPos, $this->pos - $this->startPos);
}
protected function addBBCodeSelfClosingTag()
{
$tag = $this->parser->addSelfClosingTag($this->getTagName(), $this->startPos, $this->pos - $this->startPos);
$tag->setAttributes($this->attributes);
return $tag;
}
protected function addBBCodeStartTag()
{
$tag = $this->parser->addStartTag($this->getTagName(), $this->startPos, $this->pos - $this->startPos);
$tag->setAttributes($this->attributes);
return $tag;
}
protected function captureEndTag()
{
if (empty($this->uppercaseText))
$this->uppercaseText = \strtoupper($this->text);
$match = '[/' . $this->bbcodeName . $this->bbcodeSuffix . ']';
$endTagPos = \strpos($this->uppercaseText, $match, $this->pos);
if ($endTagPos === \false)
return;
return $this->parser->addEndTag($this->getTagName(), $endTagPos, \strlen($match));
}
protected function getTagName()
{
return (isset($this->bbcodeConfig['tagName']))
? $this->bbcodeConfig['tagName']
: $this->bbcodeName;
}
protected function parseAttributes()
{
$firstPos = $this->pos;
$this->attributes = [];
while ($this->pos < $this->textLen)
{
$c = $this->text[$this->pos];
if (\strpos(" \n\t", $c) !== \false)
{
++$this->pos;
continue;
}
if (\strpos('/]', $c) !== \false)
return;
$spn = \strspn($this->text, 'abcdefghijklmnopqrstuvwxyz_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-', $this->pos);
if ($spn)
{
$attrName = \strtolower(\substr($this->text, $this->pos, $spn));
$this->pos += $spn;
if ($this->pos >= $this->textLen)
throw new RuntimeException;
if ($this->text[$this->pos] !== '=')
continue;
}
elseif ($c === '=' && $this->pos === $firstPos)
$attrName = (isset($this->bbcodeConfig['defaultAttribute']))
? $this->bbcodeConfig['defaultAttribute']
: \strtolower($this->bbcodeName);
else
throw new RuntimeException;
if (++$this->pos >= $this->textLen)
throw new RuntimeException;
$this->attributes[$attrName] = $this->parseAttributeValue();
}
}
protected function parseAttributeValue()
{
if ($this->text[$this->pos] === '"' || $this->text[$this->pos] === "'")
return $this->parseQuotedAttributeValue();
if (!\preg_match('#[^\\]\\n]*?(?=\\s*(?:\\s/)?\\]|\\s+[-\\w]+=)#', $this->text, $m, \null, $this->pos))
throw new RuntimeException;
$attrValue = $m[0];
$this->pos += \strlen($attrValue);
return $attrValue;
}
protected function parseBBCode()
{
$this->parseBBCodeSuffix();
if ($this->text[$this->startPos + 1] === '/')
{
if (\substr($this->text, $this->pos, 1) === ']' && $this->bbcodeSuffix === '')
{
++$this->pos;
$this->addBBCodeEndTag();
}
return;
}
$this->parseAttributes();
if (isset($this->bbcodeConfig['predefinedAttributes']))
$this->attributes += $this->bbcodeConfig['predefinedAttributes'];
if (\substr($this->text, $this->pos, 1) === ']')
++$this->pos;
else
{
if (\substr($this->text, $this->pos, 2) === '/]')
{
$this->pos += 2;
$this->addBBCodeSelfClosingTag();
}
return;
}
$contentAttributes = [];
if (isset($this->bbcodeConfig['contentAttributes']))
foreach ($this->bbcodeConfig['contentAttributes'] as $attrName)
if (!isset($this->attributes[$attrName]))
$contentAttributes[] = $attrName;
$requireEndTag = ($this->bbcodeSuffix || !empty($this->bbcodeConfig['forceLookahead']));
$endTag = ($requireEndTag || !empty($contentAttributes)) ? $this->captureEndTag() : \null;
if (isset($endTag))
foreach ($contentAttributes as $attrName)
$this->attributes[$attrName] = \substr($this->text, $this->pos, $endTag->getPos() - $this->pos);
elseif ($requireEndTag)
return;
$tag = $this->addBBCodeStartTag();
if (isset($endTag))
$tag->pairWith($endTag);
}
protected function parseBBCodeSuffix()
{
$this->bbcodeSuffix = '';
if ($this->text[$this->pos] === ':')
{
$spn = 1 + \strspn($this->text, '0123456789', 1 + $this->pos);
$this->bbcodeSuffix = \substr($this->text, $this->pos, $spn);
$this->pos += $spn;
}
}
protected function parseQuotedAttributeValue()
{
$quote = $this->text[$this->pos];
$valuePos = $this->pos + 1;
while (1)
{
$this->pos = \strpos($this->text, $quote, $this->pos + 1);
if ($this->pos === \false)
throw new RuntimeException;
$n = 0;
do
{
++$n;
}
while ($this->text[$this->pos - $n] === '\\');
if ($n % 2)
break;
}
$attrValue = \preg_replace(
'#\\\\([\\\\\'"])#',
'$1',
\substr($this->text, $valuePos, $this->pos - $valuePos)
);
++$this->pos;
return $attrValue;
}
}

View File

@ -0,0 +1,122 @@
<?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\Plugins\Censor;
use ArrayAccess;
use Countable;
use Iterator;
use s9e\TextFormatter\Configurator\Collections\NormalizedCollection;
use s9e\TextFormatter\Configurator\Helpers\ConfigHelper;
use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
use s9e\TextFormatter\Configurator\Items\Regexp;
use s9e\TextFormatter\Configurator\JavaScript\Code;
use s9e\TextFormatter\Configurator\JavaScript\RegexpConvertor;
use s9e\TextFormatter\Configurator\Traits\CollectionProxy;
use s9e\TextFormatter\Plugins\ConfiguratorBase;
class Configurator extends ConfiguratorBase implements ArrayAccess, Countable, Iterator
{
use CollectionProxy;
protected $allowed = [];
protected $attrName = 'with';
protected $collection;
protected $defaultReplacement = '****';
protected $regexpOptions = [
'caseInsensitive' => \true,
'specialChars' => [
'*' => '[\\pL\\pN]*',
'?' => '.',
' ' => '\\s*'
]
];
protected $tagName = 'CENSOR';
protected function setUp()
{
$this->collection = new NormalizedCollection;
$this->collection->onDuplicate('replace');
if (isset($this->configurator->tags[$this->tagName]))
return;
$tag = $this->configurator->tags->add($this->tagName);
$tag->attributes->add($this->attrName)->required = \false;
$tag->rules->ignoreTags();
$tag->template =
'<xsl:choose>
<xsl:when test="@' . $this->attrName . '">
<xsl:value-of select="@' . \htmlspecialchars($this->attrName) . '"/>
</xsl:when>
<xsl:otherwise>' . \htmlspecialchars($this->defaultReplacement) . '</xsl:otherwise>
</xsl:choose>';
}
public function allow($word)
{
$this->allowed[$word] = \true;
}
public function getHelper()
{
$config = $this->asConfig();
if (isset($config))
$config = ConfigHelper::filterConfig($config, 'PHP');
else
$config = [
'attrName' => $this->attrName,
'regexp' => '/(?!)/',
'tagName' => $this->tagName
];
return new Helper($config);
}
public function asConfig()
{
$words = $this->getWords();
if (empty($words))
return;
$config = [
'attrName' => $this->attrName,
'regexp' => $this->getWordsRegexp(\array_keys($words)),
'regexpHtml' => $this->getWordsRegexp(\array_map('htmlspecialchars', \array_keys($words))),
'tagName' => $this->tagName
];
$replacementWords = [];
foreach ($words as $word => $replacement)
if (isset($replacement) && $replacement !== $this->defaultReplacement)
$replacementWords[$replacement][] = $word;
foreach ($replacementWords as $replacement => $words)
{
$wordsRegexp = '/^' . RegexpBuilder::fromList($words, $this->regexpOptions) . '$/Diu';
$regexp = new Regexp($wordsRegexp);
$regexp->setJS(RegexpConvertor::toJS(\str_replace('[\\pL\\pN]', '[^\\s!-\\/:-?]', $wordsRegexp)));
$config['replacements'][] = [$regexp, $replacement];
}
if (!empty($this->allowed))
$config['allowed'] = $this->getWordsRegexp(\array_keys($this->allowed));
return $config;
}
public function getJSHints()
{
$hints = [
'CENSOR_HAS_ALLOWED' => !empty($this->allowed),
'CENSOR_HAS_REPLACEMENTS' => \false
];
foreach ($this->getWords() as $replacement)
if (isset($replacement) && $replacement !== $this->defaultReplacement)
{
$hints['CENSOR_HAS_REPLACEMENTS'] = \true;
break;
}
return $hints;
}
protected function getWords()
{
return \array_diff_key(\iterator_to_array($this->collection), $this->allowed);
}
protected function getWordsRegexp(array $words)
{
$expr = RegexpBuilder::fromList($words, $this->regexpOptions);
$expr = \preg_replace('/(?<!\\\\)((?>\\\\\\\\)*)\\(\\?:/', '$1(?>', $expr);
$regexp = new Regexp('/(?<![\\pL\\pN])' . $expr . '(?![\\pL\\pN])/Siu');
$regexp->setJS('/(?:^|\\W)' . \str_replace('[\\pL\\pN]', '[^\\s!-\\/:-?]', $expr) . '(?!\\w)/gi');
return $regexp;
}
}

View File

@ -0,0 +1,75 @@
<?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\Plugins\Censor;
class Helper
{
public $allowed;
public $attrName = 'with';
public $defaultReplacement = '****';
public $regexp = '/(?!)/';
public $regexpHtml = '/(?!)/';
public $replacements = [];
public $tagName = 'CENSOR';
public function __construct(array $config)
{
foreach ($config as $k => $v)
$this->$k = $v;
}
public function censorHtml($html, $censorAttributes = \false)
{
$attributesExpr = '';
if ($censorAttributes)
$attributesExpr = '|[^<">]*+(?=<|$|"(?> [-\\w]+="[^"]*+")*+\\/?>)';
$delim = $this->regexpHtml[0];
$pos = \strrpos($this->regexpHtml, $delim);
$regexp = $delim
. '(?<!&#)(?<!&)'
. \substr($this->regexpHtml, 1, $pos - 1)
. '(?=[^<>]*+(?=<|$)' . $attributesExpr . ')'
. \substr($this->regexpHtml, $pos);
return \preg_replace_callback(
$regexp,
function ($m)
{
return \htmlspecialchars($this->getReplacement(\html_entity_decode($m[0], \ENT_QUOTES, 'UTF-8')), \ENT_QUOTES);
},
$html
);
}
public function censorText($text)
{
return \preg_replace_callback(
$this->regexp,
function ($m)
{
return $this->getReplacement($m[0]);
},
$text
);
}
public function isCensored($word)
{
return (\preg_match($this->regexp, $word) && !$this->isAllowed($word));
}
protected function getReplacement($word)
{
if ($this->isAllowed($word))
return $word;
foreach ($this->replacements as $_23be09c)
{
list($regexp, $replacement) = $_23be09c;
if (\preg_match($regexp, $word))
return $replacement;
}
return $this->defaultReplacement;
}
protected function isAllowed($word)
{
return (isset($this->allowed) && \preg_match($this->allowed, $word));
}
}

View File

@ -0,0 +1,42 @@
var tagName = config.tagName,
attrName = config.attrName;
matches.forEach(function(m)
{
if (isAllowed(m[0][0]))
{
return;
}
// NOTE: unlike the PCRE regexp, the JavaScript regexp can consume an extra character at the
// start of the match, so we have to adjust the position and length accordingly
var offset = /^\W/.test(m[0][0]) ? 1 : 0,
word = m[0][0].substr(offset),
tag = addSelfClosingTag(tagName, m[0][1] + offset, word.length);
if (HINT.CENSOR_HAS_REPLACEMENTS && config.replacements)
{
for (var i = 0; i < config.replacements.length; ++i)
{
var regexp = config.replacements[i][0],
replacement = config.replacements[i][1];
if (regexp.test(word))
{
tag.setAttribute(attrName, replacement);
break;
}
}
}
});
/**
* Test whether given word is allowed
*
* @param {!string} word
* @return {!boolean}
*/
function isAllowed(word)
{
return (HINT.CENSOR_HAS_ALLOWED && config.allowed && config.allowed.test(word));
}

View File

@ -0,0 +1,37 @@
<?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\Plugins\Censor;
use s9e\TextFormatter\Plugins\ParserBase;
class Parser extends ParserBase
{
public function parse($text, array $matches)
{
$tagName = $this->config['tagName'];
$attrName = $this->config['attrName'];
$replacements = (isset($this->config['replacements'])) ? $this->config['replacements'] : [];
foreach ($matches as $m)
{
if ($this->isAllowed($m[0][0]))
continue;
$tag = $this->parser->addSelfClosingTag($tagName, $m[0][1], \strlen($m[0][0]));
foreach ($replacements as $_681051f1)
{
list($regexp, $replacement) = $_681051f1;
if (\preg_match($regexp, $m[0][0]))
{
$tag->setAttribute($attrName, $replacement);
break;
}
}
}
}
protected function isAllowed($word)
{
return (isset($this->config['allowed']) && \preg_match($this->config['allowed'], $word));
}
}

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\Plugins;
use InvalidArgumentException;
use RuntimeException;
use s9e\TextFormatter\Configurator;
use s9e\TextFormatter\Configurator\ConfigProvider;
use s9e\TextFormatter\Configurator\Helpers\ConfigHelper;
use s9e\TextFormatter\Configurator\JavaScript\Code;
use s9e\TextFormatter\Configurator\Validators\AttributeName;
use s9e\TextFormatter\Configurator\Validators\TagName;
abstract class ConfiguratorBase implements ConfigProvider
{
protected $configurator;
protected $quickMatch = \false;
protected $regexpLimit = 50000;
final public function __construct(Configurator $configurator, array $overrideProps = [])
{
$this->configurator = $configurator;
foreach ($overrideProps as $k => $v)
{
$methodName = 'set' . \ucfirst($k);
if (\method_exists($this, $methodName))
$this->$methodName($v);
elseif (\property_exists($this, $k))
$this->$k = $v;
else
throw new RuntimeException("Unknown property '" . $k . "'");
}
$this->setUp();
}
protected function setUp()
{
}
public function finalize()
{
}
public function asConfig()
{
$properties = \get_object_vars($this);
unset($properties['configurator']);
return ConfigHelper::toArray($properties);
}
final public function getBaseProperties()
{
$config = [
'className' => \preg_replace('/Configurator$/', 'Parser', \get_class($this)),
'quickMatch' => $this->quickMatch,
'regexpLimit' => $this->regexpLimit
];
$js = $this->getJSParser();
if (isset($js))
$config['js'] = new Code($js);
return $config;
}
public function getJSHints()
{
return [];
}
public function getJSParser()
{
$className = \get_class($this);
if (\strpos($className, 's9e\\TextFormatter\\Plugins\\') === 0)
{
$p = \explode('\\', $className);
$pluginName = $p[3];
$filepath = __DIR__ . '/' . $pluginName . '/Parser.js';
if (\file_exists($filepath))
return \file_get_contents($filepath);
}
return \null;
}
public function getTag()
{
if (!isset($this->tagName))
throw new RuntimeException('No tag associated with this plugin');
return $this->configurator->tags[$this->tagName];
}
public function disableQuickMatch()
{
$this->quickMatch = \false;
}
protected function setAttrName($attrName)
{
if (!\property_exists($this, 'attrName'))
throw new RuntimeException("Unknown property 'attrName'");
$this->attrName = AttributeName::normalize($attrName);
}
public function setQuickMatch($quickMatch)
{
if (!\is_string($quickMatch))
throw new InvalidArgumentException('quickMatch must be a string');
$this->quickMatch = $quickMatch;
}
public function setRegexpLimit($limit)
{
$limit = (int) $limit;
if ($limit < 1)
throw new InvalidArgumentException('regexpLimit must be a number greater than 0');
$this->regexpLimit = $limit;
}
protected function setTagName($tagName)
{
if (!\property_exists($this, 'tagName'))
throw new RuntimeException("Unknown property 'tagName'");
$this->tagName = TagName::normalize($tagName);
}
}

View File

@ -0,0 +1,66 @@
<?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\Plugins\Emoji;
use s9e\TextFormatter\Configurator\Helpers\ConfigHelper;
use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
use s9e\TextFormatter\Configurator\Items\Regexp;
use s9e\TextFormatter\Plugins\ConfiguratorBase;
class Configurator extends ConfiguratorBase
{
protected $attrName = 'seq';
protected $aliases = [];
protected $tagName = 'EMOJI';
protected function setUp()
{
if (isset($this->configurator->tags[$this->tagName]))
return;
$tag = $this->configurator->tags->add($this->tagName);
$tag->attributes->add($this->attrName)->filterChain->append(
$this->configurator->attributeFilters['#identifier']
);
$tag->template = '<img alt="{.}" class="emoji" draggable="false" src="//cdn.jsdelivr.net/emojione/assets/3.1/png/64/{@seq}.png"/>';
}
public function addAlias($alias, $emoji)
{
$this->aliases[$alias] = $emoji;
}
public function removeAlias($alias)
{
unset($this->aliases[$alias]);
}
public function getAliases()
{
return $this->aliases;
}
public function asConfig()
{
$config = [
'attrName' => $this->attrName,
'tagName' => $this->tagName
];
if (!empty($this->aliases))
{
$aliases = \array_keys($this->aliases);
$regexp = '/' . RegexpBuilder::fromList($aliases) . '/';
$config['aliases'] = $this->aliases;
$config['aliasesRegexp'] = new Regexp($regexp, \true);
$quickMatch = ConfigHelper::generateQuickMatchFromList($aliases);
if ($quickMatch !== \false)
$config['aliasesQuickMatch'] = $quickMatch;
}
return $config;
}
public function getJSHints()
{
$quickMatch = ConfigHelper::generateQuickMatchFromList(\array_keys($this->aliases));
return [
'EMOJI_HAS_ALIASES' => !empty($this->aliases),
'EMOJI_HAS_ALIAS_QUICKMATCH' => ($quickMatch !== \false)
];
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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\Plugins\Emoticons;
use ArrayAccess;
use Countable;
use Iterator;
use s9e\TextFormatter\Configurator\Helpers\ConfigHelper;
use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
use s9e\TextFormatter\Configurator\Items\Regexp;
use s9e\TextFormatter\Configurator\JavaScript\RegexpConvertor;
use s9e\TextFormatter\Configurator\Traits\CollectionProxy;
use s9e\TextFormatter\Plugins\ConfiguratorBase;
use s9e\TextFormatter\Plugins\Emoticons\Configurator\EmoticonCollection;
use s9e\TextFormatter\Utils\XPath;
class Configurator extends ConfiguratorBase implements ArrayAccess, Countable, Iterator
{
use CollectionProxy;
protected $collection;
public $notAfter = '';
public $notBefore = '';
public $notIfCondition;
protected $onDuplicateAction = 'replace';
protected $tagName = 'E';
protected function setUp()
{
$this->collection = new EmoticonCollection;
if (!$this->configurator->tags->exists($this->tagName))
$this->configurator->tags->add($this->tagName);
}
public function finalize()
{
$tag = $this->getTag();
if (!isset($tag->template))
$tag->template = $this->getTemplate();
}
public function asConfig()
{
if (!\count($this->collection))
return;
$codes = \array_keys(\iterator_to_array($this->collection));
$regexp = '/';
if ($this->notAfter !== '')
$regexp .= '(?<!' . $this->notAfter . ')';
$regexp .= RegexpBuilder::fromList($codes);
if ($this->notBefore !== '')
$regexp .= '(?!' . $this->notBefore . ')';
$regexp .= '/S';
if (\preg_match('/\\\\[pP](?>\\{\\^?\\w+\\}|\\w\\w?)/', $regexp))
$regexp .= 'u';
$regexp = \preg_replace('/(?<!\\\\)((?>\\\\\\\\)*)\\(\\?:/', '$1(?>', $regexp);
$config = [
'quickMatch' => $this->quickMatch,
'regexp' => $regexp,
'tagName' => $this->tagName
];
if ($this->notAfter !== '')
{
$lpos = 6 + \strlen($this->notAfter);
$rpos = \strrpos($regexp, '/');
$jsRegexp = RegexpConvertor::toJS('/' . \substr($regexp, $lpos, $rpos - $lpos) . '/', \true);
$config['regexp'] = new Regexp($regexp);
$config['regexp']->setJS($jsRegexp);
$config['notAfter'] = new Regexp('/' . $this->notAfter . '/');
}
if ($this->quickMatch === \false)
$config['quickMatch'] = ConfigHelper::generateQuickMatchFromList($codes);
return $config;
}
public function getJSHints()
{
return ['EMOTICONS_NOT_AFTER' => (int) !empty($this->notAfter)];
}
public function getTemplate()
{
$xsl = '<xsl:choose>';
if (!empty($this->notIfCondition))
$xsl .= '<xsl:when test="' . \htmlspecialchars($this->notIfCondition) . '"><xsl:value-of select="."/></xsl:when><xsl:otherwise><xsl:choose>';
foreach ($this->collection as $code => $template)
$xsl .= '<xsl:when test=".=' . \htmlspecialchars(XPath::export($code)) . '">'
. $template
. '</xsl:when>';
$xsl .= '<xsl:otherwise><xsl:value-of select="."/></xsl:otherwise>';
$xsl .= '</xsl:choose>';
if (!empty($this->notIfCondition))
$xsl .= '</xsl:otherwise></xsl:choose>';
return $xsl;
}
}

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\Plugins\Emoticons\Configurator;
use RuntimeException;
use s9e\TextFormatter\Configurator\Collections\NormalizedCollection;
use s9e\TextFormatter\Configurator\Helpers\TemplateHelper;
class EmoticonCollection extends NormalizedCollection
{
protected $onDuplicateAction = 'replace';
public function normalizeValue($value)
{
return TemplateHelper::saveTemplate(TemplateHelper::loadTemplate($value));
}
protected function getAlreadyExistsException($key)
{
return new RuntimeException("Emoticon '" . $key . "' already exists");
}
protected function getNotExistException($key)
{
return new RuntimeException("Emoticon '" . $key . "' does not exist");
}
}

View File

@ -0,0 +1,9 @@
matches.forEach(function(m)
{
if (HINT.EMOTICONS_NOT_AFTER && config.notAfter && m[0][1] && config.notAfter.test(text[m[0][1] - 1]))
{
return;
}
addSelfClosingTag(config.tagName, m[0][1], m[0][0].length);
});

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\Plugins\Emoticons;
use s9e\TextFormatter\Plugins\ParserBase;
class Parser extends ParserBase
{
public function parse($text, array $matches)
{
foreach ($matches as $m)
$this->parser->addSelfClosingTag($this->config['tagName'], $m[0][1], \strlen($m[0][0]));
}
}

View File

@ -0,0 +1,28 @@
<?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\Plugins\Escaper;
use s9e\TextFormatter\Plugins\ConfiguratorBase;
class Configurator extends ConfiguratorBase
{
protected $quickMatch = '\\';
protected $regexp;
protected $tagName = 'ESC';
public function escapeAll($bool = \true)
{
$this->regexp = ($bool) ? '/\\\\./su' : '/\\\\[-!#()*+.:<>@[\\\\\\]^_`{|}]/';
}
protected function setUp()
{
$this->escapeAll(\false);
$tag = $this->configurator->tags->add($this->tagName);
$tag->rules->disableAutoLineBreaks();
$tag->rules->ignoreTags();
$tag->rules->preventLineBreaks();
$tag->template = '<xsl:apply-templates/>';
}
}

View File

@ -0,0 +1,10 @@
matches.forEach(function(m)
{
addTagPair(
config.tagName,
m[0][1],
1,
m[0][1] + m[0][0].length,
0
);
});

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\Plugins\Escaper;
use s9e\TextFormatter\Plugins\ParserBase;
class Parser extends ParserBase
{
public function parse($text, array $matches)
{
foreach ($matches as $m)
$this->parser->addTagPair(
$this->config['tagName'],
$m[0][1],
1,
$m[0][1] + \strlen($m[0][0]),
0
);
}
}

View File

@ -0,0 +1,43 @@
<?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\Plugins\FancyPants;
use s9e\TextFormatter\Plugins\ConfiguratorBase;
class Configurator extends ConfiguratorBase
{
protected $attrName = 'char';
protected $disabledPasses = [];
protected $tagName = 'FP';
protected function setUp()
{
if (isset($this->configurator->tags[$this->tagName]))
return;
$tag = $this->configurator->tags->add($this->tagName);
$tag->attributes->add($this->attrName);
$tag->template
= '<xsl:value-of select="@' . \htmlspecialchars($this->attrName) . '"/>';
}
public function disablePass($passName)
{
$this->disabledPasses[] = $passName;
}
public function enablePass($passName)
{
foreach (\array_keys($this->disabledPasses, $passName, \true) as $k)
unset($this->disabledPasses[$k]);
}
public function asConfig()
{
$config = [
'attrName' => $this->attrName,
'tagName' => $this->tagName
];
foreach ($this->disabledPasses as $passName)
$config['disable' . $passName] = \true;
return $config;
}
}

View File

@ -0,0 +1,289 @@
var attrName = config.attrName,
hasSingleQuote = (text.indexOf("'") >= 0),
hasDoubleQuote = (text.indexOf('"') >= 0),
tagName = config.tagName;
if (!config.disableQuotes)
{
parseSingleQuotes();
parseSingleQuotePairs();
parseDoubleQuotePairs();
}
if (!config.disableGuillemets)
{
parseGuillemets();
}
if (!config.disableMathSymbols)
{
parseNotEqualSign();
parseSymbolsAfterDigits();
parseFractions();
}
if (!config.disablePunctuation)
{
parseDashesAndEllipses();
}
if (!config.disableSymbols)
{
parseSymbolsInParentheses();
}
/**
* Add a fancy replacement tag
*
* @param {!number} tagPos Position of the tag in the text
* @param {!number} tagLen Length of text consumed by the tag
* @param {!string} chr Replacement character
* @param {number} prio Tag's priority
* @return {!Tag}
*/
function addTag(tagPos, tagLen, chr, prio)
{
var tag = addSelfClosingTag(tagName, tagPos, tagLen, prio || 0);
tag.setAttribute(attrName, chr);
return tag;
}
/**
* Parse dashes and ellipses
*
* Does en dash , em dash — and ellipsis …
*/
function parseDashesAndEllipses()
{
if (text.indexOf('...') < 0 && text.indexOf('--') < 0)
{
return;
}
var chrs = {
'--' : "\u2013",
'---' : "\u2014",
'...' : "\u2026"
},
regexp = /---?|\.\.\./g,
m;
while (m = regexp.exec(text))
{
addTag(+m['index'], m[0].length, chrs[m[0]]);
}
}
/**
* Parse pairs of double quotes
*
* Does quote pairs “” -- must be done separately to handle nesting
*/
function parseDoubleQuotePairs()
{
if (hasDoubleQuote)
{
parseQuotePairs('"', /(?:^|\W)".+?"(?!\w)/g, "\u201c", "\u201d");
}
}
/**
* Parse vulgar fractions
*/
function parseFractions()
{
if (text.indexOf('/') < 0)
{
return;
}
/** @const */
var map = {
'0/3' : "\u2189",
'1/10' : "\u2152",
'1/2' : "\u00BD",
'1/3' : "\u2153",
'1/4' : "\u00BC",
'1/5' : "\u2155",
'1/6' : "\u2159",
'1/7' : "\u2150",
'1/8' : "\u215B",
'1/9' : "\u2151",
'2/3' : "\u2154",
'2/5' : "\u2156",
'3/4' : "\u00BE",
'3/5' : "\u2157",
'3/8' : "\u215C",
'4/5' : "\u2158",
'5/6' : "\u215A",
'5/8' : "\u215D",
'7/8' : "\u215E"
};
var m, regexp = /\b(?:0\/3|1\/(?:[2-9]|10)|2\/[35]|3\/[458]|4\/5|5\/[68]|7\/8)\b/g;
while (m = regexp.exec(text))
{
addTag(+m['index'], m[0].length, map[m[0]]);
}
}
/**
* Parse guillemets-style quotation marks
*/
function parseGuillemets()
{
if (text.indexOf('<<') < 0)
{
return;
}
var m, regexp = /<<( ?)(?! )[^\n<>]*?[^\n <>]\1>>(?!>)/g;
while (m = regexp.exec(text))
{
var left = addTag(+m['index'], 2, "\u00AB"),
right = addTag(+m['index'] + m[0].length - 2, 2, "\u00BB");
left.cascadeInvalidationTo(right);
}
}
/**
* Parse the not equal sign
*
* Supports != and =/=
*/
function parseNotEqualSign()
{
if (text.indexOf('!=') < 0 && text.indexOf('=/=') < 0)
{
return;
}
var m, regexp = /\b (?:!|=\/)=(?= \b)/g;
while (m = regexp.exec(text))
{
addTag(+m['index'] + 1, m[0].length - 1, "\u2260");
}
}
/**
* Parse pairs of quotes
*
* @param {!string} q ASCII quote character
* @param {!RegExp} regexp Regexp used to identify quote pairs
* @param {!string} leftQuote Fancy replacement for left quote
* @param {!string} rightQuote Fancy replacement for right quote
*/
function parseQuotePairs(q, regexp, leftQuote, rightQuote)
{
var m;
while (m = regexp.exec(text))
{
var left = addTag(+m['index'] + m[0].indexOf(q), 1, leftQuote),
right = addTag(+m['index'] + m[0].length - 1, 1, rightQuote);
// Cascade left tag's invalidation to the right so that if we skip the left quote,
// the right quote remains untouched
left.cascadeInvalidationTo(right);
}
}
/**
* Parse pairs of single quotes
*
* Does quote pairs must be done separately to handle nesting
*/
function parseSingleQuotePairs()
{
if (hasSingleQuote)
{
parseQuotePairs("'", /(?:^|\W)'.+?'(?!\w)/g, "\u2018", "\u2019");
}
}
/**
* Parse single quotes in general
*
* Does apostrophes after a letter or at the beginning of a word or a couple of digits
*/
function parseSingleQuotes()
{
if (!hasSingleQuote)
{
return;
}
var m, regexp = /[a-z]'|(?:^|\s)'(?=[a-z]|[0-9]{2})/gi;
while (m = regexp.exec(text))
{
// Give this tag a worse priority than default so that quote pairs take precedence
addTag(+m['index'] + m[0].indexOf("'"), 1, "\u2019", 10);
}
}
/**
* Parse symbols found after digits
*
* Does symbols found after a digit:
* - apostrophe if it's followed by an "s" as in 80's
* - prime and double prime ″
* - multiply sign × if it's followed by an optional space and another digit
*/
function parseSymbolsAfterDigits()
{
if (!hasSingleQuote && !hasDoubleQuote && text.indexOf('x') < 0)
{
return;
}
/** @const */
var map = {
// 80's -- use an apostrophe
"'s" : "\u2019",
// 12' or 12" -- use a prime
"'" : "\u2032",
"' " : "\u2032",
"'x" : "\u2032",
'"' : "\u2033",
'" ' : "\u2033",
'"x' : "\u2033"
};
var m, regexp = /[0-9](?:'s|["']? ?x(?= ?[0-9])|["'])/g;
while (m = regexp.exec(text))
{
// Test for a multiply sign at the end
if (m[0][m[0].length - 1] === 'x')
{
addTag(+m['index'] + m[0].length - 1, 1, "\u00d7");
}
// Test for an apostrophe/prime right after the digit
var str = m[0].substr(1, 2);
if (map[str])
{
addTag(+m['index'] + 1, 1, map[str]);
}
}
}
/**
* Parse symbols found in parentheses such as (c)
*
* Does symbols ©, ® and ™
*/
function parseSymbolsInParentheses()
{
if (text.indexOf('(') < 0)
{
return;
}
var chrs = {
'(c)' : "\u00A9",
'(r)' : "\u00AE",
'(tm)' : "\u2122"
},
regexp = /\((?:c|r|tm)\)/gi,
m;
while (m = regexp.exec(text))
{
addTag(+m['index'], m[0].length, chrs[m[0].toLowerCase()]);
}
}

View File

@ -0,0 +1,187 @@
<?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\Plugins\FancyPants;
use s9e\TextFormatter\Plugins\ParserBase;
class Parser extends ParserBase
{
protected $hasDoubleQuote;
protected $hasSingleQuote;
protected $text;
public function parse($text, array $matches)
{
$this->text = $text;
$this->hasSingleQuote = (\strpos($text, "'") !== \false);
$this->hasDoubleQuote = (\strpos($text, '"') !== \false);
if (empty($this->config['disableQuotes']))
{
$this->parseSingleQuotes();
$this->parseSingleQuotePairs();
$this->parseDoubleQuotePairs();
}
if (empty($this->config['disableGuillemets']))
$this->parseGuillemets();
if (empty($this->config['disableMathSymbols']))
{
$this->parseNotEqualSign();
$this->parseSymbolsAfterDigits();
$this->parseFractions();
}
if (empty($this->config['disablePunctuation']))
$this->parseDashesAndEllipses();
if (empty($this->config['disableSymbols']))
$this->parseSymbolsInParentheses();
unset($this->text);
}
protected function addTag($tagPos, $tagLen, $chr, $prio = 0)
{
$tag = $this->parser->addSelfClosingTag($this->config['tagName'], $tagPos, $tagLen, $prio);
$tag->setAttribute($this->config['attrName'], $chr);
return $tag;
}
protected function parseDashesAndEllipses()
{
if (\strpos($this->text, '...') === \false && \strpos($this->text, '--') === \false)
return;
$chrs = [
'--' => "\xE2\x80\x93",
'---' => "\xE2\x80\x94",
'...' => "\xE2\x80\xA6"
];
$regexp = '/---?|\\.\\.\\./S';
\preg_match_all($regexp, $this->text, $matches, \PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $m)
$this->addTag($m[1], \strlen($m[0]), $chrs[$m[0]]);
}
protected function parseDoubleQuotePairs()
{
if ($this->hasDoubleQuote)
$this->parseQuotePairs(
'/(?<![0-9\\pL])"[^"\\n]+"(?![0-9\\pL])/uS',
"\xE2\x80\x9C",
"\xE2\x80\x9D"
);
}
protected function parseFractions()
{
if (\strpos($this->text, '/') === \false)
return;
$map = [
'1/4' => "\xC2\xBC",
'1/2' => "\xC2\xBD",
'3/4' => "\xC2\xBE",
'1/7' => "\xE2\x85\x90",
'1/9' => "\xE2\x85\x91",
'1/10' => "\xE2\x85\x92",
'1/3' => "\xE2\x85\x93",
'2/3' => "\xE2\x85\x94",
'1/5' => "\xE2\x85\x95",
'2/5' => "\xE2\x85\x96",
'3/5' => "\xE2\x85\x97",
'4/5' => "\xE2\x85\x98",
'1/6' => "\xE2\x85\x99",
'5/6' => "\xE2\x85\x9A",
'1/8' => "\xE2\x85\x9B",
'3/8' => "\xE2\x85\x9C",
'5/8' => "\xE2\x85\x9D",
'7/8' => "\xE2\x85\x9E",
'0/3' => "\xE2\x86\x89"
];
$regexp = '/\\b(?:0\\/3|1\\/(?:[2-9]|10)|2\\/[35]|3\\/[458]|4\\/5|5\\/[68]|7\\/8)\\b/S';
\preg_match_all($regexp, $this->text, $matches, \PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $m)
$this->addTag($m[1], \strlen($m[0]), $map[$m[0]]);
}
protected function parseGuillemets()
{
if (\strpos($this->text, '<<') === \false)
return;
$regexp = '/<<( ?)(?! )[^\\n<>]*?[^\\n <>]\\1>>(?!>)/';
\preg_match_all($regexp, $this->text, $matches, \PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $m)
{
$left = $this->addTag($m[1], 2, "\xC2\xAB");
$right = $this->addTag($m[1] + \strlen($m[0]) - 2, 2, "\xC2\xBB");
$left->cascadeInvalidationTo($right);
}
}
protected function parseNotEqualSign()
{
if (\strpos($this->text, '!=') === \false && \strpos($this->text, '=/=') === \false)
return;
$regexp = '/\\b (?:!|=\\/)=(?= \\b)/';
\preg_match_all($regexp, $this->text, $matches, \PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $m)
$this->addTag($m[1] + 1, \strlen($m[0]) - 1, "\xE2\x89\xA0");
}
protected function parseQuotePairs($regexp, $leftQuote, $rightQuote)
{
\preg_match_all($regexp, $this->text, $matches, \PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $m)
{
$left = $this->addTag($m[1], 1, $leftQuote);
$right = $this->addTag($m[1] + \strlen($m[0]) - 1, 1, $rightQuote);
$left->cascadeInvalidationTo($right);
}
}
protected function parseSingleQuotePairs()
{
if ($this->hasSingleQuote)
$this->parseQuotePairs(
"/(?<![0-9\\pL])'[^'\\n]+'(?![0-9\\pL])/uS",
"\xE2\x80\x98",
"\xE2\x80\x99"
);
}
protected function parseSingleQuotes()
{
if (!$this->hasSingleQuote)
return;
$regexp = "/(?<=\\pL)'|(?<!\\S)'(?=\\pL|[0-9]{2})/uS";
\preg_match_all($regexp, $this->text, $matches, \PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $m)
$this->addTag($m[1], 1, "\xE2\x80\x99", 10);
}
protected function parseSymbolsAfterDigits()
{
if (!$this->hasSingleQuote && !$this->hasDoubleQuote && \strpos($this->text, 'x') === \false)
return;
$map = [
"'s" => "\xE2\x80\x99",
"'" => "\xE2\x80\xB2",
"' " => "\xE2\x80\xB2",
"'x" => "\xE2\x80\xB2",
'"' => "\xE2\x80\xB3",
'" ' => "\xE2\x80\xB3",
'"x' => "\xE2\x80\xB3"
];
$regexp = "/[0-9](?>'s|[\"']? ?x(?= ?[0-9])|[\"'])/S";
\preg_match_all($regexp, $this->text, $matches, \PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $m)
{
if (\substr($m[0], -1) === 'x')
$this->addTag($m[1] + \strlen($m[0]) - 1, 1, "\xC3\x97");
$str = \substr($m[0], 1, 2);
if (isset($map[$str]))
$this->addTag($m[1] + 1, 1, $map[$str]);
}
}
protected function parseSymbolsInParentheses()
{
if (\strpos($this->text, '(') === \false)
return;
$chrs = [
'(c)' => "\xC2\xA9",
'(r)' => "\xC2\xAE",
'(tm)' => "\xE2\x84\xA2"
];
$regexp = '/\\((?>c|r|tm)\\)/i';
\preg_match_all($regexp, $this->text, $matches, \PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $m)
$this->addTag($m[1], \strlen($m[0]), $chrs[\strtr($m[0], 'CMRT', 'cmrt')]);
}
}

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\Plugins\HTMLComments;
use s9e\TextFormatter\Plugins\ConfiguratorBase;
class Configurator extends ConfiguratorBase
{
protected $attrName = 'content';
protected $quickMatch = '<!--';
protected $regexp = '/<!--(?!\\[if).*?-->/is';
protected $tagName = 'HC';
protected function setUp()
{
$tag = $this->configurator->tags->add($this->tagName);
$tag->attributes->add($this->attrName);
$tag->rules->ignoreTags();
$tag->template = '<xsl:comment><xsl:value-of select="@' . \htmlspecialchars($this->attrName) . '"/></xsl:comment>';
}
}

View File

@ -0,0 +1,16 @@
var tagName = config.tagName,
attrName = config.attrName;
matches.forEach(function(m)
{
// Decode HTML entities
var content = html_entity_decode(m[0][0].substr(4, m[0][0].length - 7));
// Remove angle brackets from the content
content = content.replace(/[<>]/g, '');
// Remove the illegal sequence "--" from the content
content = content.replace(/--/g, '');
addSelfClosingTag(tagName, m[0][1], m[0][0].length).setAttribute(attrName, content);
});

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\Plugins\HTMLComments;
use s9e\TextFormatter\Plugins\ParserBase;
class Parser extends ParserBase
{
public function parse($text, array $matches)
{
$tagName = $this->config['tagName'];
$attrName = $this->config['attrName'];
foreach ($matches as $m)
{
$content = \html_entity_decode(\substr($m[0][0], 4, -3), \ENT_QUOTES, 'UTF-8');
$content = \str_replace(['<', '>'], '', $content);
$content = \str_replace('--', '', $content);
$this->parser->addSelfClosingTag($tagName, $m[0][1], \strlen($m[0][0]))->setAttribute($attrName, $content);
}
}
}

View File

@ -0,0 +1,166 @@
<?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\Plugins\HTMLElements;
use InvalidArgumentException;
use RuntimeException;
use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
use s9e\TextFormatter\Configurator\Items\Tag;
use s9e\TextFormatter\Configurator\Items\UnsafeTemplate;
use s9e\TextFormatter\Configurator\JavaScript\Dictionary;
use s9e\TextFormatter\Configurator\Validators\AttributeName;
use s9e\TextFormatter\Configurator\Validators\TagName;
use s9e\TextFormatter\Plugins\ConfiguratorBase;
class Configurator extends ConfiguratorBase
{
protected $aliases = [];
protected $attributeFilters = [
'action' => '#url',
'cite' => '#url',
'data' => '#url',
'formaction' => '#url',
'href' => '#url',
'icon' => '#url',
'longdesc' => '#url',
'manifest' => '#url',
'poster' => '#url',
'src' => '#url'
];
protected $elements = [];
protected $prefix = 'html';
protected $quickMatch = '<';
protected $unsafeElements = [
'base',
'embed',
'frame',
'iframe',
'meta',
'object',
'script'
];
protected $unsafeAttributes = [
'style',
'target'
];
public function aliasAttribute($elName, $attrName, $alias)
{
$elName = $this->normalizeElementName($elName);
$attrName = $this->normalizeAttributeName($attrName);
$this->aliases[$elName][$attrName] = AttributeName::normalize($alias);
}
public function aliasElement($elName, $tagName)
{
$elName = $this->normalizeElementName($elName);
$this->aliases[$elName][''] = TagName::normalize($tagName);
}
public function allowElement($elName)
{
return $this->allowElementWithSafety($elName, \false);
}
public function allowUnsafeElement($elName)
{
return $this->allowElementWithSafety($elName, \true);
}
protected function allowElementWithSafety($elName, $allowUnsafe)
{
$elName = $this->normalizeElementName($elName);
$tagName = $this->prefix . ':' . $elName;
if (!$allowUnsafe && \in_array($elName, $this->unsafeElements))
throw new RuntimeException("'" . $elName . "' elements are unsafe and are disabled by default. Please use " . __CLASS__ . '::allowUnsafeElement() to bypass this security measure');
$tag = ($this->configurator->tags->exists($tagName))
? $this->configurator->tags->get($tagName)
: $this->configurator->tags->add($tagName);
$this->rebuildTemplate($tag, $elName, $allowUnsafe);
$this->elements[$elName] = 1;
return $tag;
}
public function allowAttribute($elName, $attrName)
{
return $this->allowAttributeWithSafety($elName, $attrName, \false);
}
public function allowUnsafeAttribute($elName, $attrName)
{
return $this->allowAttributeWithSafety($elName, $attrName, \true);
}
protected function allowAttributeWithSafety($elName, $attrName, $allowUnsafe)
{
$elName = $this->normalizeElementName($elName);
$attrName = $this->normalizeAttributeName($attrName);
$tagName = $this->prefix . ':' . $elName;
if (!isset($this->elements[$elName]))
throw new RuntimeException("Element '" . $elName . "' has not been allowed");
if (!$allowUnsafe)
if (\substr($attrName, 0, 2) === 'on'
|| \in_array($attrName, $this->unsafeAttributes))
throw new RuntimeException("'" . $attrName . "' attributes are unsafe and are disabled by default. Please use " . __CLASS__ . '::allowUnsafeAttribute() to bypass this security measure');
$tag = $this->configurator->tags->get($tagName);
if (!isset($tag->attributes[$attrName]))
{
$attribute = $tag->attributes->add($attrName);
$attribute->required = \false;
if (isset($this->attributeFilters[$attrName]))
{
$filterName = $this->attributeFilters[$attrName];
$filter = $this->configurator->attributeFilters->get($filterName);
$attribute->filterChain->append($filter);
}
}
$this->rebuildTemplate($tag, $elName, $allowUnsafe);
return $tag->attributes[$attrName];
}
protected function normalizeElementName($elName)
{
if (!\preg_match('#^[a-z][a-z0-9]*$#Di', $elName))
throw new InvalidArgumentException ("Invalid element name '" . $elName . "'");
return \strtolower($elName);
}
protected function normalizeAttributeName($attrName)
{
if (!\preg_match('#^[a-z][-\\w]*$#Di', $attrName))
throw new InvalidArgumentException ("Invalid attribute name '" . $attrName . "'");
return \strtolower($attrName);
}
protected function rebuildTemplate(Tag $tag, $elName, $allowUnsafe)
{
$template = '<' . $elName . '>';
foreach ($tag->attributes as $attrName => $attribute)
$template .= '<xsl:copy-of select="@' . $attrName . '"/>';
$template .= '<xsl:apply-templates/></' . $elName . '>';
if ($allowUnsafe)
$template = new UnsafeTemplate($template);
$tag->setTemplate($template);
}
public function asConfig()
{
if (empty($this->elements) && empty($this->aliases))
return;
$attrRegexp = '[a-z][-a-z0-9]*(?>\\s*=\\s*(?>"[^"]*"|\'[^\']*\'|[^\\s"\'=<>`]+))?';
$tagRegexp = RegexpBuilder::fromList(\array_merge(
\array_keys($this->aliases),
\array_keys($this->elements)
));
$endTagRegexp = '/(' . $tagRegexp . ')';
$startTagRegexp = '(' . $tagRegexp . ')((?>\\s+' . $attrRegexp . ')*+)\\s*/?';
$regexp = '#<(?>' . $endTagRegexp . '|' . $startTagRegexp . ')\\s*>#i';
$config = [
'quickMatch' => $this->quickMatch,
'prefix' => $this->prefix,
'regexp' => $regexp
];
if (!empty($this->aliases))
{
$config['aliases'] = new Dictionary;
foreach ($this->aliases as $elName => $aliases)
$config['aliases'][$elName] = new Dictionary($aliases);
}
return $config;
}
public function getJSHints()
{
return ['HTMLELEMENTS_HAS_ALIASES' => (int) !empty($this->aliases)];
}
}

View File

@ -0,0 +1,86 @@
matches.forEach(function(m)
{
// Test whether this is an end tag
var isEnd = (text[m[0][1] + 1] === '/');
var pos = m[0][1],
len = m[0][0].length,
elName = m[2 - isEnd][0].toLowerCase();
// Use the element's alias if applicable, or the name of the element (with the
// configured prefix) otherwise
var tagName = (config.aliases && config.aliases[elName] && config.aliases[elName][''])
? config.aliases[elName]['']
: config.prefix + ':' + elName;
if (isEnd)
{
addEndTag(tagName, pos, len);
return;
}
// Test whether it's a self-closing tag or a start tag.
//
// A self-closing tag will become one start tag consuming all of the text followed by a
// 0-width end tag. Alternatively, it could be replaced by a pair of 0-width tags plus
// an ignore tag to prevent the text in between from being output
var tag = (/(<\S+|['"\s])\/>$/.test(m[0][0]))
? addTagPair(tagName, pos, len, pos + len, 0)
: addStartTag(tagName, pos, len);
captureAttributes(tag, elName, m[3][0]);
});
/**
* Capture all attributes in given string
*
* @param {!Tag} tag Target tag
* @param {!string} elName Name of the HTML element
* @param {!string} str String containing the attribute declarations
*/
function captureAttributes(tag, elName, str)
{
// Capture attributes
var attrRegexp = /[a-z][-a-z0-9]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?/gi,
attrName,
attrValue,
attrMatch,
pos;
while (attrMatch = attrRegexp.exec(str))
{
pos = attrMatch[0].indexOf('=');
/**
* If there's no equal sign, it's a boolean attribute and we generate a value equal
* to the attribute's name, lowercased
*
* @link http://www.w3.org/html/wg/drafts/html/master/single-page.html#boolean-attributes
*/
if (pos < 0)
{
pos = attrMatch[0].length;
attrMatch[0] += '=' + attrMatch[0].toLowerCase();
}
// Normalize the attribute name, remove the whitespace around its value to account
// for cases like <b title = "foo"/>
attrName = attrMatch[0].substr(0, pos).toLowerCase().replace(/^\s+/, '').replace('/\s+$/', '');
attrValue = attrMatch[0].substr(1 + pos).replace(/^\s+/, '').replace('/\s+$/', '');
// Use the attribute's alias if applicable
if (HINT.HTMLELEMENTS_HAS_ALIASES && config.aliases && config.aliases[elName] && config.aliases[elName][attrName])
{
attrName = config.aliases[elName][attrName];
}
// Remove quotes around the value
if (/^["']/.test(attrValue))
{
attrValue = attrValue.substr(1, attrValue.length - 2);
}
tag.setAttribute(attrName, html_entity_decode(attrValue));
}
}

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\Plugins\HTMLElements;
use s9e\TextFormatter\Parser\Tag;
use s9e\TextFormatter\Plugins\ParserBase;
class Parser extends ParserBase
{
public function parse($text, array $matches)
{
foreach ($matches as $m)
{
$isEnd = (bool) ($text[$m[0][1] + 1] === '/');
$pos = $m[0][1];
$len = \strlen($m[0][0]);
$elName = \strtolower($m[2 - $isEnd][0]);
$tagName = (isset($this->config['aliases'][$elName]['']))
? $this->config['aliases'][$elName]['']
: $this->config['prefix'] . ':' . $elName;
if ($isEnd)
{
$this->parser->addEndTag($tagName, $pos, $len);
continue;
}
$tag = (\preg_match('/(<\\S+|[\'"\\s])\\/>$/', $m[0][0]))
? $this->parser->addTagPair($tagName, $pos, $len, $pos + $len, 0)
: $this->parser->addStartTag($tagName, $pos, $len);
$this->captureAttributes($tag, $elName, $m[3][0]);
}
}
protected function captureAttributes(Tag $tag, $elName, $str)
{
\preg_match_all(
'/[a-z][-a-z0-9]*(?>\\s*=\\s*(?>"[^"]*"|\'[^\']*\'|[^\\s"\'=<>`]+))?/i',
$str,
$attrMatches
);
foreach ($attrMatches[0] as $attrMatch)
{
$pos = \strpos($attrMatch, '=');
if ($pos === \false)
{
$pos = \strlen($attrMatch);
$attrMatch .= '=' . \strtolower($attrMatch);
}
$attrName = \strtolower(\trim(\substr($attrMatch, 0, $pos)));
$attrValue = \trim(\substr($attrMatch, 1 + $pos));
if (isset($this->config['aliases'][$elName][$attrName]))
$attrName = $this->config['aliases'][$elName][$attrName];
if ($attrValue[0] === '"' || $attrValue[0] === "'")
$attrValue = \substr($attrValue, 1, -1);
$tag->setAttribute($attrName, \html_entity_decode($attrValue, \ENT_QUOTES, 'UTF-8'));
}
}
}

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\Plugins\HTMLEntities;
use s9e\TextFormatter\Plugins\ConfiguratorBase;
class Configurator extends ConfiguratorBase
{
protected $attrName = 'char';
protected $quickMatch = '&';
protected $regexp = '/&(?>[a-z]+|#(?>[0-9]+|x[0-9a-f]+));/i';
protected $tagName = 'HE';
protected function setUp()
{
$tag = $this->configurator->tags->add($this->tagName);
$tag->attributes->add($this->attrName);
$tag->template
= '<xsl:value-of select="@' . \htmlspecialchars($this->attrName) . '"/>';
}
}

View File

@ -0,0 +1,17 @@
var tagName = config.tagName,
attrName = config.attrName;
matches.forEach(function(m)
{
var entity = m[0][0],
chr = html_entity_decode(entity);
if (chr === entity || chr.charCodeAt(0) < 32)
{
// If the entity was not decoded, we assume it's not valid and we ignore it.
// Same thing if it's a control character
return;
}
addSelfClosingTag(tagName, m[0][1], entity.length).setAttribute(attrName, chr);
});

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\Plugins\HTMLEntities;
use s9e\TextFormatter\Plugins\ParserBase;
class Parser extends ParserBase
{
public function parse($text, array $matches)
{
$tagName = $this->config['tagName'];
$attrName = $this->config['attrName'];
foreach ($matches as $m)
{
$entity = $m[0][0];
$chr = \html_entity_decode($entity, \ENT_QUOTES, 'UTF-8');
if ($chr === $entity || \ord($chr) < 32)
continue;
$this->parser->addSelfClosingTag($tagName, $m[0][1], \strlen($entity))->setAttribute($attrName, $chr);
}
}
}

View File

@ -0,0 +1,68 @@
<?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\Plugins\Keywords;
use s9e\TextFormatter\Configurator\Collections\NormalizedList;
use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
use s9e\TextFormatter\Configurator\Items\Regexp;
use s9e\TextFormatter\Configurator\Traits\CollectionProxy;
use s9e\TextFormatter\Plugins\ConfiguratorBase;
class Configurator extends ConfiguratorBase
{
use CollectionProxy;
protected $attrName = 'value';
public $caseSensitive = \true;
protected $collection;
public $onlyFirst = \false;
protected $tagName = 'KEYWORD';
protected function setUp()
{
$this->collection = new NormalizedList;
$this->configurator->tags->add($this->tagName)->attributes->add($this->attrName);
}
public function asConfig()
{
if (!\count($this->collection))
return;
$config = [
'attrName' => $this->attrName,
'tagName' => $this->tagName
];
if (!empty($this->onlyFirst))
$config['onlyFirst'] = $this->onlyFirst;
$keywords = \array_unique(\iterator_to_array($this->collection));
\sort($keywords);
$groups = [];
$groupKey = 0;
$groupLen = 0;
foreach ($keywords as $keyword)
{
$keywordLen = 4 + \strlen($keyword);
$groupLen += $keywordLen;
if ($groupLen > 30000)
{
$groupLen = $keywordLen;
++$groupKey;
}
$groups[$groupKey][] = $keyword;
}
foreach ($groups as $keywords)
{
$regexp = RegexpBuilder::fromList(
$keywords,
['caseInsensitive' => !$this->caseSensitive]
);
$regexp = '/\\b' . $regexp . '\\b/S';
if (!$this->caseSensitive)
$regexp .= 'i';
if (\preg_match('/[^[:ascii:]]/', $regexp))
$regexp .= 'u';
$config['regexps'][] = new Regexp($regexp, \true);
}
return $config;
}
}

View File

@ -0,0 +1,31 @@
var regexps = config.regexps,
tagName = config.tagName,
attrName = config.attrName;
var onlyFirst = config.onlyFirst,
keywords = {};
regexps.forEach(function(regexp)
{
var m;
regexp.lastIndex = 0;
while (m = regexp.exec(text))
{
// NOTE: coercing m.index to a number because Closure Compiler thinks pos is a string otherwise
var value = m[0],
pos = +m['index'];
if (onlyFirst)
{
if (value in keywords)
{
continue;
}
keywords[value] = 1;
}
addSelfClosingTag(tagName, pos, value.length).setAttribute(attrName, value);
}
});

View File

@ -0,0 +1,36 @@
<?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\Plugins\Keywords;
use s9e\TextFormatter\Plugins\ParserBase;
class Parser extends ParserBase
{
public function parse($text, array $matches)
{
$regexps = $this->config['regexps'];
$tagName = $this->config['tagName'];
$attrName = $this->config['attrName'];
$onlyFirst = !empty($this->config['onlyFirst']);
$keywords = [];
foreach ($regexps as $regexp)
{
\preg_match_all($regexp, $text, $matches, \PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $_11955a1f)
{
list($value, $pos) = $_11955a1f;
if ($onlyFirst)
{
if (isset($keywords[$value]))
continue;
$keywords[$value] = 1;
}
$this->parser->addSelfClosingTag($tagName, $pos, \strlen($value))
->setAttribute($attrName, $value);
}
}
}
}

View File

@ -0,0 +1,128 @@
<?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\Plugins\Litedown;
use s9e\TextFormatter\Plugins\ConfiguratorBase;
class Configurator extends ConfiguratorBase
{
public $decodeHtmlEntities = \false;
protected $tags = [
'C' => '<code><xsl:apply-templates/></code>',
'CODE' => [
'attributes' => [
'lang' => [
'filterChain' => ['#simpletext'],
'required' => \false
]
],
'template' =>
'<pre>
<code>
<xsl:if test="@lang">
<xsl:attribute name="class">
<xsl:text>language-</xsl:text>
<xsl:value-of select="@lang"/>
</xsl:attribute>
</xsl:if>
<xsl:apply-templates/>
</code>
</pre>'
],
'DEL' => '<del><xsl:apply-templates/></del>',
'EM' => '<em><xsl:apply-templates/></em>',
'H1' => '<h1><xsl:apply-templates/></h1>',
'H2' => '<h2><xsl:apply-templates/></h2>',
'H3' => '<h3><xsl:apply-templates/></h3>',
'H4' => '<h4><xsl:apply-templates/></h4>',
'H5' => '<h5><xsl:apply-templates/></h5>',
'H6' => '<h6><xsl:apply-templates/></h6>',
'HR' => '<hr/>',
'IMG' => [
'attributes' => [
'alt' => ['required' => \false ],
'src' => ['filterChain' => ['#url']],
'title' => ['required' => \false ]
],
'template' => '<img src="{@src}"><xsl:copy-of select="@alt"/><xsl:copy-of select="@title"/></img>'
],
'LI' => '<li><xsl:apply-templates/></li>',
'LIST' => [
'attributes' => [
'start' => [
'filterChain' => ['#uint'],
'required' => \false
],
'type' => [
'filterChain' => ['#simpletext'],
'required' => \false
]
],
'template' =>
'<xsl:choose>
<xsl:when test="not(@type)">
<ul><xsl:apply-templates/></ul>
</xsl:when>
<xsl:otherwise>
<ol><xsl:copy-of select="@start"/><xsl:apply-templates/></ol>
</xsl:otherwise>
</xsl:choose>'
],
'QUOTE' => '<blockquote><xsl:apply-templates/></blockquote>',
'STRONG' => '<strong><xsl:apply-templates/></strong>',
'SUB' => '<sub><xsl:apply-templates/></sub>',
'SUP' => '<sup><xsl:apply-templates/></sup>',
'URL' => [
'attributes' => [
'title' => ['required' => \false ],
'url' => ['filterChain' => ['#url']]
],
'template' => '<a href="{@url}"><xsl:copy-of select="@title"/><xsl:apply-templates/></a>'
]
];
protected function setUp()
{
$this->configurator->rulesGenerator->append('ManageParagraphs');
foreach ($this->tags as $tagName => $tagConfig)
{
if (isset($this->configurator->tags[$tagName]))
continue;
if (\is_string($tagConfig))
$tagConfig = ['template' => $tagConfig];
$this->configurator->tags->add($tagName, $tagConfig);
}
}
public function asConfig()
{
return ['decodeHtmlEntities' => (bool) $this->decodeHtmlEntities];
}
public function getJSHints()
{
return ['LITEDOWN_DECODE_HTML_ENTITIES' => (int) $this->decodeHtmlEntities];
}
public function getJSParser()
{
$js = \file_get_contents(__DIR__ . '/Parser/ParsedText.js') . "\n"
. \file_get_contents(__DIR__ . '/Parser/Passes/AbstractScript.js') . "\n"
. \file_get_contents(__DIR__ . '/Parser/LinkAttributesSetter.js');
$passes = [
'Blocks',
'LinkReferences',
'InlineCode',
'Images',
'Links',
'Strikethrough',
'Subscript',
'Superscript',
'Emphasis',
'ForcedLineBreaks'
];
foreach ($passes as $pass)
$js .= "\n(function(){\n"
. \file_get_contents(__DIR__ . '/Parser/Passes/' . $pass . '.js') . "\nparse();\n})();";
return $js;
}
}

View File

@ -0,0 +1,39 @@
<?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\Plugins\Litedown;
use s9e\TextFormatter\Parser\Tag;
use s9e\TextFormatter\Plugins\Litedown\Parser\ParsedText;
use s9e\TextFormatter\Plugins\Litedown\Parser\Passes\Blocks;
use s9e\TextFormatter\Plugins\Litedown\Parser\Passes\Emphasis;
use s9e\TextFormatter\Plugins\Litedown\Parser\Passes\ForcedLineBreaks;
use s9e\TextFormatter\Plugins\Litedown\Parser\Passes\Images;
use s9e\TextFormatter\Plugins\Litedown\Parser\Passes\InlineCode;
use s9e\TextFormatter\Plugins\Litedown\Parser\Passes\LinkReferences;
use s9e\TextFormatter\Plugins\Litedown\Parser\Passes\Links;
use s9e\TextFormatter\Plugins\Litedown\Parser\Passes\Strikethrough;
use s9e\TextFormatter\Plugins\Litedown\Parser\Passes\Subscript;
use s9e\TextFormatter\Plugins\Litedown\Parser\Passes\Superscript;
use s9e\TextFormatter\Plugins\ParserBase;
class Parser extends ParserBase
{
public function parse($text, array $matches)
{
$text = new ParsedText($text);
$text->decodeHtmlEntities = $this->config['decodeHtmlEntities'];
(new Blocks($this->parser, $text))->parse();
(new LinkReferences($this->parser, $text))->parse();
(new InlineCode($this->parser, $text))->parse();
(new Images($this->parser, $text))->parse();
(new Links($this->parser, $text))->parse();
(new Strikethrough($this->parser, $text))->parse();
(new Subscript($this->parser, $text))->parse();
(new Superscript($this->parser, $text))->parse();
(new Emphasis($this->parser, $text))->parse();
(new ForcedLineBreaks($this->parser, $text))->parse();
}
}

View File

@ -0,0 +1,24 @@
/**
* Set a URL or IMG tag's attributes
*
* @param {!Tag} tag URL or IMG tag
* @param {string} linkInfo Link's info: an URL optionally followed by spaces and a title
* @param {string} attrName Name of the URL attribute
*/
function setLinkAttributes(tag, linkInfo, attrName)
{
var url = linkInfo.replace(/^\s*/, '').replace(/\s*$/, ''),
title = '',
pos = url.indexOf(' ')
if (pos !== -1)
{
title = url.substr(pos).replace(/^\s*\S/, '').replace(/\S\s*$/, '');
url = url.substr(0, pos);
}
tag.setAttribute(attrName, decode(url));
if (title > '')
{
tag.setAttribute('title', decode(title));
}
}

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\Plugins\Litedown\Parser;
use s9e\TextFormatter\Parser\Tag;
trait LinkAttributesSetter
{
protected function setLinkAttributes(Tag $tag, $linkInfo, $attrName)
{
$url = \trim($linkInfo);
$title = '';
$pos = \strpos($url, ' ');
if ($pos !== \false)
{
$title = \substr(\trim(\substr($url, $pos)), 1, -1);
$url = \substr($url, 0, $pos);
}
$tag->setAttribute($attrName, $this->text->decode($url));
if ($title > '')
$tag->setAttribute('title', $this->text->decode($title));
}
}

View File

@ -0,0 +1,160 @@
/**
* @var {boolean} Whether to decode HTML entities when decoding text
*/
var decodeHtmlEntities = config.decodeHtmlEntities;
/**
* @var {bool} Whether text contains escape characters
*/
var hasEscapedChars = false;
/**
* @var {bool} Whether text contains link references
*/
var hasReferences = false;
/**
* @dict
*/
var linkReferences = {};
if (text.indexOf('\\') >= 0)
{
hasEscapedChars = true;
// Encode escaped literals that have a special meaning otherwise, so that we don't have
// to take them into account in regexps
text = text.replace(
/\\[!"'()*[\\\]^_`~]/g,
function (str)
{
return {
'\\!': "\x1B0", '\\"': "\x1B1", "\\'": "\x1B2", '\\(' : "\x1B3",
'\\)': "\x1B4", '\\*': "\x1B5", '\\[': "\x1B6", '\\\\': "\x1B7",
'\\]': "\x1B8", '\\^': "\x1B9", '\\_': "\x1BA", '\\`' : "\x1BB",
'\\~': "\x1BC"
}[str];
}
);
}
// We append a couple of lines and a non-whitespace character at the end of the text in
// order to trigger the closure of all open blocks such as quotes and lists
text += "\n\n\x17";
/**
* Decode a chunk of encoded text to be used as an attribute value
*
* Decodes escaped literals and removes slashes and 0x1A characters
*
* @param {string} str Encoded text
* @return {string} Decoded text
*/
function decode(str)
{
if (HINT.LITEDOWN_DECODE_HTML_ENTITIES && decodeHtmlEntities && str.indexOf('&') > -1)
{
str = html_entity_decode(str);
}
str = str.replace(/\x1A/g, '');
if (hasEscapedChars)
{
str = str.replace(
/\x1B./g,
function (seq)
{
return {
"\x1B0": '!', "\x1B1": '"', "\x1B2": "'", "\x1B3": '(',
"\x1B4": ')', "\x1B5": '*', "\x1B6": '[', "\x1B7": '\\',
"\x1B8": ']', "\x1B9": '^', "\x1BA": '_', "\x1BB": '`',
"\x1BC": '~'
}[seq];
}
);
}
return str;
}
/**
* Test whether given position is preceded by whitespace
*
* @param {number} pos
* @return {boolean}
*/
function isAfterWhitespace(pos)
{
return (pos > 0 && isWhitespace(text.charAt(pos - 1)));
}
/**
* Test whether given character is alphanumeric
*
* @param {string} chr
* @return {boolean}
*/
function isAlnum(chr)
{
return (' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'.indexOf(chr) > 0);
}
/**
* Test whether given position is followed by whitespace
*
* @param {number} pos
* @return {boolean}
*/
function isBeforeWhitespace(pos)
{
return isWhitespace(text[pos + 1]);
}
/**
* Test whether a length of text is surrounded by alphanumeric characters
*
* @param {number} pos Start of the text
* @param {number} len Length of the text
* @return {boolean}
*/
function isSurroundedByAlnum(pos, len)
{
return (pos > 0 && isAlnum(text[pos - 1]) && isAlnum(text[pos + len]));
}
/**
* Test whether given character is an ASCII whitespace character
*
* NOTE: newlines are normalized to LF before parsing so we don't have to check for CR
*
* @param {string} chr
* @return {boolean}
*/
function isWhitespace(chr)
{
return (" \n\t".indexOf(chr) > -1);
}
/**
* Mark the boundary of a block in the original text
*
* @param {number} pos
*/
function markBoundary(pos)
{
text = text.substr(0, pos) + "\x17" + text.substr(pos + 1);
}
/**
* Overwrite part of the text with substitution characters ^Z (0x1A)
*
* @param {number} pos Start of the range
* @param {number} len Length of text to overwrite
*/
function overwrite(pos, len)
{
if (len > 0)
{
text = text.substr(0, pos) + new Array(1 + len).join("\x1A") + text.substr(pos + len);
}
}

View File

@ -0,0 +1,91 @@
<?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\Plugins\Litedown\Parser;
class ParsedText
{
public $decodeHtmlEntities = \false;
protected $hasEscapedChars = \false;
public $hasReferences = \false;
public $linkReferences = [];
protected $text;
public function __construct($text)
{
if (\strpos($text, '\\') !== \false && \preg_match('/\\\\[!"\'()*[\\\\\\]^_`~]/', $text))
{
$this->hasEscapedChars = \true;
$text = \strtr(
$text,
[
'\\!' => "\x1B0", '\\"' => "\x1B1", "\\'" => "\x1B2", '\\(' => "\x1B3",
'\\)' => "\x1B4", '\\*' => "\x1B5", '\\[' => "\x1B6", '\\\\' => "\x1B7",
'\\]' => "\x1B8", '\\^' => "\x1B9", '\\_' => "\x1BA", '\\`' => "\x1BB",
'\\~' => "\x1BC"
]
);
}
$this->text = $text . "\n\n\x17";
}
public function __toString()
{
return $this->text;
}
public function charAt($pos)
{
return $this->text[$pos];
}
public function decode($str)
{
if ($this->decodeHtmlEntities && \strpos($str, '&') !== \false)
$str = \html_entity_decode($str, \ENT_QUOTES, 'UTF-8');
$str = \str_replace("\x1A", '', $str);
if ($this->hasEscapedChars)
$str = \strtr(
$str,
[
"\x1B0" => '!', "\x1B1" => '"', "\x1B2" => "'", "\x1B3" => '(',
"\x1B4" => ')', "\x1B5" => '*', "\x1B6" => '[', "\x1B7" => '\\',
"\x1B8" => ']', "\x1B9" => '^', "\x1BA" => '_', "\x1BB" => '`',
"\x1BC" => '~'
]
);
return $str;
}
public function indexOf($str, $pos = 0)
{
return \strpos($this->text, $str, $pos);
}
public function isAfterWhitespace($pos)
{
return ($pos > 0 && $this->isWhitespace($this->text[$pos - 1]));
}
public function isAlnum($chr)
{
return (\strpos(' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', $chr) > 0);
}
public function isBeforeWhitespace($pos)
{
return $this->isWhitespace($this->text[$pos + 1]);
}
public function isSurroundedByAlnum($pos, $len)
{
return ($pos > 0 && $this->isAlnum($this->text[$pos - 1]) && $this->isAlnum($this->text[$pos + $len]));
}
public function isWhitespace($chr)
{
return (\strpos(" \n\t", $chr) !== \false);
}
public function markBoundary($pos)
{
$this->text[$pos] = "\x17";
}
public function overwrite($pos, $len)
{
if ($len > 0)
$this->text = \substr($this->text, 0, $pos) . \str_repeat("\x1A", $len) . \substr($this->text, $pos + $len);
}
}

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\Plugins\Litedown\Parser\Passes;
use s9e\TextFormatter\Parser;
use s9e\TextFormatter\Plugins\Litedown\Parser\ParsedText;
abstract class AbstractPass
{
protected $parser;
protected $text;
public function __construct(Parser $parser, ParsedText $text)
{
$this->parser = $parser;
$this->text = $text;
}
abstract public function parse();
}

View File

@ -0,0 +1,73 @@
/**
* @param {string} tagName Name of the tag used by this pass
* @param {string} syntaxChar Relevant character used by this syntax
* @param {!Regexp} shortRegexp Regexp used for the short form syntax
* @param {!Regexp} longRegexp Regexp used for the long form syntax
*/
function parseAbstractScript(tagName, syntaxChar, shortRegexp, longRegexp)
{
var pos = text.indexOf(syntaxChar);
if (pos === -1)
{
return;
}
parseShortForm(pos);
parseLongForm(pos);
/**
* Parse the long form x^(x)
*
* This syntax is supported by RDiscount
*
* @param {number} pos Position of the first relevant character
*/
function parseLongForm(pos)
{
pos = text.indexOf(syntaxChar + '(', pos);
if (pos === -1)
{
return;
}
var m, regexp = longRegexp;
regexp.lastIndex = pos;
while (m = regexp.exec(text))
{
var match = m[0],
matchPos = +m['index'],
matchLen = match.length;
addTagPair(tagName, matchPos, 2, matchPos + matchLen - 1, 1);
overwrite(matchPos, matchLen);
}
if (match)
{
parseLongForm(pos);
}
}
/**
* Parse the short form x^x and x^x^
*
* This syntax is supported by most implementations that support superscript
*
* @param {number} pos Position of the first relevant character
*/
function parseShortForm(pos)
{
var m, regexp = shortRegexp;
regexp.lastIndex = pos;
while (m = regexp.exec(text))
{
var match = m[0],
matchPos = +m['index'],
matchLen = match.length,
startPos = matchPos,
endLen = (match.substr(-1) === syntaxChar) ? 1 : 0,
endPos = matchPos + matchLen - endLen;
addTagPair(tagName, startPos, 1, endPos, endLen);
}
}
}

View File

@ -0,0 +1,57 @@
<?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\Plugins\Litedown\Parser\Passes;
abstract class AbstractScript extends AbstractPass
{
protected $longRegexp;
protected $shortRegexp;
protected $syntaxChar;
protected $tagName;
protected function parseAbstractScript($tagName, $syntaxChar, $shortRegexp, $longRegexp)
{
$this->tagName = $tagName;
$this->syntaxChar = $syntaxChar;
$this->shortRegexp = $shortRegexp;
$this->longRegexp = $longRegexp;
$pos = $this->text->indexOf($this->syntaxChar);
if ($pos === \false)
return;
$this->parseShortForm($pos);
$this->parseLongForm($pos);
}
protected function parseLongForm($pos)
{
$pos = $this->text->indexOf($this->syntaxChar . '(', $pos);
if ($pos === \false)
return;
\preg_match_all($this->longRegexp, $this->text, $matches, \PREG_OFFSET_CAPTURE, $pos);
foreach ($matches[0] as $_4b034d25)
{
list($match, $matchPos) = $_4b034d25;
$matchLen = \strlen($match);
$this->parser->addTagPair($this->tagName, $matchPos, 2, $matchPos + $matchLen - 1, 1);
$this->text->overwrite($matchPos, $matchLen);
}
if (!empty($matches[0]))
$this->parseLongForm($pos);
}
protected function parseShortForm($pos)
{
\preg_match_all($this->shortRegexp, $this->text, $matches, \PREG_OFFSET_CAPTURE, $pos);
foreach ($matches[0] as $_4b034d25)
{
list($match, $matchPos) = $_4b034d25;
$matchLen = \strlen($match);
$startPos = $matchPos;
$endLen = (\substr($match, -1) === $this->syntaxChar) ? 1 : 0;
$endPos = $matchPos + $matchLen - $endLen;
$this->parser->addTagPair($this->tagName, $startPos, 1, $endPos, $endLen);
}
}
}

View File

@ -0,0 +1,515 @@
var setextLines = {};
function parse()
{
matchSetextLines();
var codeFence,
codeIndent = 4,
codeTag,
lineIsEmpty = true,
lists = [],
listsCnt = 0,
newContext = false,
quotes = [],
quotesCnt = 0,
textBoundary = 0,
breakParagraph,
continuation,
endTag,
ignoreLen,
indentStr,
indentLen,
lfPos,
listIndex,
maxIndent,
minIndent,
quoteDepth,
tagPos,
tagLen;
// Capture all the lines at once so that we can overwrite newlines safely, without preventing
// further matches
var matches = [],
m,
regexp = /^(?:(?=[-*+\d \t>`~#_])((?: {0,3}> ?)+)?([ \t]+)?(\* *\* *\*[* ]*$|- *- *-[- ]*$|_ *_ *_[_ ]*$)?((?:[-*+]|\d+\.)[ \t]+(?=\S))?[ \t]*(#{1,6}[ \t]+|```+[^`\n]*$|~~~+[^~\n]*$)?)?/gm;
while (m = regexp.exec(text))
{
matches.push(m);
// Move regexp.lastIndex if the current match is empty
if (m['index'] === regexp['lastIndex'])
{
++regexp['lastIndex'];
}
}
matches.forEach(function(m)
{
var matchPos = m['index'],
matchLen = m[0].length,
startPos,
startLen,
endPos,
endLen;
ignoreLen = 0;
quoteDepth = 0;
// If the last line was empty then this is not a continuation, and vice-versa
continuation = !lineIsEmpty;
// Capture the position of the end of the line and determine whether the line is empty
lfPos = text.indexOf("\n", matchPos);
lineIsEmpty = (lfPos === matchPos + matchLen && !m[3] && !m[4] && !m[5]);
// If the match is empty we need to move the cursor manually
if (!matchLen)
{
++regexp.lastIndex;
}
// If the line is empty and it's the first empty line then we break current paragraph.
breakParagraph = (lineIsEmpty && continuation);
// Count quote marks
if (m[1])
{
quoteDepth = m[1].length - m[1].replace(/>/g, '').length;
ignoreLen = m[1].length;
if (codeTag && codeTag.hasAttribute('quoteDepth'))
{
quoteDepth = Math.min(quoteDepth, codeTag.getAttribute('quoteDepth'));
ignoreLen = computeQuoteIgnoreLen(m[1], quoteDepth);
}
// Overwrite quote markup
overwrite(matchPos, ignoreLen);
}
// Close supernumerary quotes
if (quoteDepth < quotesCnt && !continuation)
{
newContext = true;
do
{
addEndTag('QUOTE', textBoundary, 0).pairWith(quotes.pop());
}
while (quoteDepth < --quotesCnt);
}
// Open new quotes
if (quoteDepth > quotesCnt && !lineIsEmpty)
{
newContext = true;
do
{
var tag = addStartTag('QUOTE', matchPos, 0, quotesCnt - 999);
quotes.push(tag);
}
while (quoteDepth > ++quotesCnt);
}
// Compute the width of the indentation
var indentWidth = 0,
indentPos = 0;
if (m[2] && !codeFence)
{
indentStr = m[2];
indentLen = indentStr.length;
do
{
if (indentStr[indentPos] === ' ')
{
++indentWidth;
}
else
{
indentWidth = (indentWidth + 4) & ~3;
}
}
while (++indentPos < indentLen && indentWidth < codeIndent);
}
// Test whether we're out of a code block
if (codeTag && !codeFence && indentWidth < codeIndent && !lineIsEmpty)
{
newContext = true;
}
if (newContext)
{
newContext = false;
// Close the code block if applicable
if (codeTag)
{
if (textBoundary > codeTag.getPos())
{
// Overwrite the whole block
overwrite(codeTag.getPos(), textBoundary - codeTag.getPos());
endTag = addEndTag('CODE', textBoundary, 0, -1);
endTag.pairWith(codeTag);
}
else
{
// The code block is empty
codeTag.invalidate();
}
codeTag = null;
codeFence = null;
}
// Close all the lists
lists.forEach(function(list)
{
closeList(list, textBoundary);
});
lists = [];
listsCnt = 0;
// Mark the block boundary
if (matchPos)
{
markBoundary(matchPos - 1);
}
}
if (indentWidth >= codeIndent)
{
if (codeTag || !continuation)
{
// Adjust the amount of text being ignored
ignoreLen = (m[1] || '').length + indentPos;
if (!codeTag)
{
// Create code block
codeTag = addStartTag('CODE', matchPos + ignoreLen, 0, -999);
}
// Clear the captures to prevent any further processing
m = {};
}
}
else
{
var hasListItem = !!m[4];
if (!indentWidth && !continuation && !hasListItem)
{
// Start of a new context
listIndex = -1;
}
else if (continuation && !hasListItem)
{
// Continuation of current list item or paragraph
listIndex = listsCnt - 1;
}
else if (!listsCnt)
{
// We're not inside of a list already, we can start one if there's a list item
listIndex = (hasListItem) ? 0 : -1
}
else
{
// We're inside of a list but we need to compute the depth
listIndex = 0;
while (listIndex < listsCnt && indentWidth > lists[listIndex].maxIndent)
{
++listIndex;
}
}
// Close deeper lists
while (listIndex < listsCnt - 1)
{
closeList(lists.pop(), textBoundary);
--listsCnt;
}
// If there's no list item at current index, we'll need to either create one or
// drop down to previous index, in which case we have to adjust maxIndent
if (listIndex === listsCnt && !hasListItem)
{
--listIndex;
}
if (hasListItem && listIndex >= 0)
{
breakParagraph = true;
// Compute the position and amount of text consumed by the item tag
tagPos = matchPos + ignoreLen + indentPos
tagLen = m[4].length;
// Create a LI tag that consumes its markup
var itemTag = addStartTag('LI', tagPos, tagLen);
// Overwrite the markup
overwrite(tagPos, tagLen);
// If the list index is within current lists count it means this is not a new
// list and we have to close the last item. Otherwise, it's a new list that we
// have to create
if (listIndex < listsCnt)
{
addEndTag('LI', textBoundary, 0).pairWith(lists[listIndex].itemTag);
// Record the item in the list
lists[listIndex].itemTag = itemTag;
lists[listIndex].itemTags.push(itemTag);
}
else
{
++listsCnt;
if (listIndex)
{
minIndent = lists[listIndex - 1].maxIndent + 1;
maxIndent = Math.max(minIndent, listIndex * 4);
}
else
{
minIndent = 0;
maxIndent = indentWidth;
}
// Create a 0-width LIST tag right before the item tag LI
var listTag = addStartTag('LIST', tagPos, 0);
// Test whether the list item ends with a dot, as in "1."
if (m[4].indexOf('.') > -1)
{
listTag.setAttribute('type', 'decimal');
var start = +m[4];
if (start !== 1)
{
listTag.setAttribute('start', start);
}
}
// Record the new list depth
lists.push({
listTag : listTag,
itemTag : itemTag,
itemTags : [itemTag],
minIndent : minIndent,
maxIndent : maxIndent,
tight : true
});
}
}
// If we're in a list, on a non-empty line preceded with a blank line...
if (listsCnt && !continuation && !lineIsEmpty)
{
// ...and this is not the first item of the list...
if (lists[0].itemTags.length > 1 || !hasListItem)
{
// ...every list that is currently open becomes loose
lists.forEach(function(list)
{
list.tight = false;
});
}
}
codeIndent = (listsCnt + 1) * 4;
}
if (m[5])
{
// Headers
if (m[5][0] === '#')
{
startLen = m[5].length;
startPos = matchPos + matchLen - startLen;
endLen = getAtxHeaderEndTagLen(matchPos + matchLen, lfPos);
endPos = lfPos - endLen;
addTagPair('H' + /#{1,6}/.exec(m[5])[0].length, startPos, startLen, endPos, endLen);
// Mark the start and the end of the header as boundaries
markBoundary(startPos);
markBoundary(lfPos);
if (continuation)
{
breakParagraph = true;
}
}
// Code fence
else if (m[5][0] === '`' || m[5][0] === '~')
{
tagPos = matchPos + ignoreLen;
tagLen = lfPos - tagPos;
if (codeTag && m[5] === codeFence)
{
endTag = addEndTag('CODE', tagPos, tagLen, -1);
endTag.pairWith(codeTag);
addIgnoreTag(textBoundary, tagPos - textBoundary);
// Overwrite the whole block
overwrite(codeTag.getPos(), tagPos + tagLen - codeTag.getPos());
codeTag = null;
codeFence = null;
}
else if (!codeTag)
{
// Create code block
codeTag = addStartTag('CODE', tagPos, tagLen);
codeFence = m[5].replace(/[^`~]+/, '');
codeTag.setAttribute('quoteDepth', quoteDepth);
// Ignore the next character, which should be a newline
addIgnoreTag(tagPos + tagLen, 1);
// Add the language if present, e.g. ```php
var lang = m[5].replace(/^[`~\s]*/, '').replace(/\s+$/, '');
if (lang !== '')
{
codeTag.setAttribute('lang', lang);
}
}
}
}
else if (m[3] && !listsCnt && text[matchPos + matchLen] !== "\x17")
{
// Horizontal rule
addSelfClosingTag('HR', matchPos + ignoreLen, matchLen - ignoreLen);
breakParagraph = true;
// Mark the end of the line as a boundary
markBoundary(lfPos);
}
else if (setextLines[lfPos] && setextLines[lfPos].quoteDepth === quoteDepth && !lineIsEmpty && !listsCnt && !codeTag)
{
// Setext-style header
addTagPair(
setextLines[lfPos].tagName,
matchPos + ignoreLen,
0,
setextLines[lfPos].endPos,
setextLines[lfPos].endLen
);
// Mark the end of the Setext line
markBoundary(setextLines[lfPos].endPos + setextLines[lfPos].endLen);
}
if (breakParagraph)
{
addParagraphBreak(textBoundary);
markBoundary(textBoundary);
}
if (!lineIsEmpty)
{
textBoundary = lfPos;
}
if (ignoreLen)
{
addIgnoreTag(matchPos, ignoreLen, 1000);
}
});
}
/**
* Close a list at given offset
*
* @param {!Array} list
* @param {number} textBoundary
*/
function closeList(list, textBoundary)
{
addEndTag('LIST', textBoundary, 0).pairWith(list.listTag);
addEndTag('LI', textBoundary, 0).pairWith(list.itemTag);
if (list.tight)
{
list.itemTags.forEach(function(itemTag)
{
itemTag.removeFlags(RULE_CREATE_PARAGRAPHS);
});
}
}
/**
* Compute the amount of text to ignore at the start of a quote line
*
* @param {string} str Original quote markup
* @param {number} maxQuoteDepth Maximum quote depth
* @return {number} Number of characters to ignore
*/
function computeQuoteIgnoreLen(str, maxQuoteDepth)
{
var remaining = str;
while (--maxQuoteDepth >= 0)
{
remaining = remaining.replace(/^ *> ?/, '');
}
return str.length - remaining.length;
}
/**
* Return the length of the markup at the end of an ATX header
*
* @param {number} startPos Start of the header's text
* @param {number} endPos End of the header's text
* @return {number}
*/
function getAtxHeaderEndTagLen(startPos, endPos)
{
var content = text.substr(startPos, endPos - startPos),
m = /[ \t]*#*[ \t]*$/.exec(content);
return m[0].length;
}
/**
* Capture and store lines that contain a Setext-tyle header
*/
function matchSetextLines()
{
// Capture the underlines used for Setext-style headers
if (text.indexOf('-') === -1 && text.indexOf('=') === -1)
{
return;
}
// Capture the any series of - or = alone on a line, optionally preceded with the
// angle brackets notation used in blockquotes
var m, regexp = /^(?=[-=>])(?:> ?)*(?=[-=])(?:-+|=+) *$/gm;
while (m = regexp.exec(text))
{
var match = m[0],
matchPos = m['index'];
// Compute the position of the end tag. We start on the LF character before the
// match and keep rewinding until we find a non-space character
var endPos = matchPos - 1;
while (endPos > 0 && text[endPos - 1] === ' ')
{
--endPos;
}
// Store at the offset of the LF character
setextLines[matchPos - 1] = {
endLen : matchPos + match.length - endPos,
endPos : endPos,
quoteDepth : match.length - match.replace(/>/g, '').length,
tagName : (match[0] === '=') ? 'H1' : 'H2'
};
}
}

View File

@ -0,0 +1,304 @@
<?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\Plugins\Litedown\Parser\Passes;
use s9e\TextFormatter\Parser as Rules;
class Blocks extends AbstractPass
{
protected $setextLines = [];
public function parse()
{
$this->matchSetextLines();
$codeFence = \null;
$codeIndent = 4;
$codeTag = \null;
$lineIsEmpty = \true;
$lists = [];
$listsCnt = 0;
$newContext = \false;
$quotes = [];
$quotesCnt = 0;
$textBoundary = 0;
$regexp = '/^(?:(?=[-*+\\d \\t>`~#_])((?: {0,3}> ?)+)?([ \\t]+)?(\\* *\\* *\\*[* ]*$|- *- *-[- ]*$|_ *_ *_[_ ]*$|=+$)?((?:[-*+]|\\d+\\.)[ \\t]+(?=\\S))?[ \\t]*(#{1,6}[ \\t]+|```+[^`\\n]*$|~~~+[^~\\n]*$)?)?/m';
\preg_match_all($regexp, $this->text, $matches, \PREG_OFFSET_CAPTURE | \PREG_SET_ORDER);
foreach ($matches as $m)
{
$matchPos = $m[0][1];
$matchLen = \strlen($m[0][0]);
$ignoreLen = 0;
$quoteDepth = 0;
$continuation = !$lineIsEmpty;
$lfPos = $this->text->indexOf("\n", $matchPos);
$lineIsEmpty = ($lfPos === $matchPos + $matchLen && empty($m[3][0]) && empty($m[4][0]) && empty($m[5][0]));
$breakParagraph = ($lineIsEmpty && $continuation);
if (!empty($m[1][0]))
{
$quoteDepth = \substr_count($m[1][0], '>');
$ignoreLen = \strlen($m[1][0]);
if (isset($codeTag) && $codeTag->hasAttribute('quoteDepth'))
{
$quoteDepth = \min($quoteDepth, $codeTag->getAttribute('quoteDepth'));
$ignoreLen = $this->computeQuoteIgnoreLen($m[1][0], $quoteDepth);
}
$this->text->overwrite($matchPos, $ignoreLen);
}
if ($quoteDepth < $quotesCnt && !$continuation)
{
$newContext = \true;
do
{
$this->parser->addEndTag('QUOTE', $textBoundary, 0)
->pairWith(\array_pop($quotes));
}
while ($quoteDepth < --$quotesCnt);
}
if ($quoteDepth > $quotesCnt && !$lineIsEmpty)
{
$newContext = \true;
do
{
$tag = $this->parser->addStartTag('QUOTE', $matchPos, 0, $quotesCnt - 999);
$quotes[] = $tag;
}
while ($quoteDepth > ++$quotesCnt);
}
$indentWidth = 0;
$indentPos = 0;
if (!empty($m[2][0]) && !$codeFence)
{
$indentStr = $m[2][0];
$indentLen = \strlen($indentStr);
do
{
if ($indentStr[$indentPos] === ' ')
++$indentWidth;
else
$indentWidth = ($indentWidth + 4) & ~3;
}
while (++$indentPos < $indentLen && $indentWidth < $codeIndent);
}
if (isset($codeTag) && !$codeFence && $indentWidth < $codeIndent && !$lineIsEmpty)
$newContext = \true;
if ($newContext)
{
$newContext = \false;
if (isset($codeTag))
{
if ($textBoundary > $codeTag->getPos())
{
$this->text->overwrite($codeTag->getPos(), $textBoundary - $codeTag->getPos());
$endTag = $this->parser->addEndTag('CODE', $textBoundary, 0, -1);
$endTag->pairWith($codeTag);
}
else
$codeTag->invalidate();
$codeTag = \null;
$codeFence = \null;
}
foreach ($lists as $list)
$this->closeList($list, $textBoundary);
$lists = [];
$listsCnt = 0;
if ($matchPos)
$this->text->markBoundary($matchPos - 1);
}
if ($indentWidth >= $codeIndent)
{
if (isset($codeTag) || !$continuation)
{
$ignoreLen += $indentPos;
if (!isset($codeTag))
$codeTag = $this->parser->addStartTag('CODE', $matchPos + $ignoreLen, 0, -999);
$m = [];
}
}
else
{
$hasListItem = !empty($m[4][0]);
if (!$indentWidth && !$continuation && !$hasListItem)
$listIndex = -1;
elseif ($continuation && !$hasListItem)
$listIndex = $listsCnt - 1;
elseif (!$listsCnt)
$listIndex = ($hasListItem) ? 0 : -1;
else
{
$listIndex = 0;
while ($listIndex < $listsCnt && $indentWidth > $lists[$listIndex]['maxIndent'])
++$listIndex;
}
while ($listIndex < $listsCnt - 1)
{
$this->closeList(\array_pop($lists), $textBoundary);
--$listsCnt;
}
if ($listIndex === $listsCnt && !$hasListItem)
--$listIndex;
if ($hasListItem && $listIndex >= 0)
{
$breakParagraph = \true;
$tagPos = $matchPos + $ignoreLen + $indentPos;
$tagLen = \strlen($m[4][0]);
$itemTag = $this->parser->addStartTag('LI', $tagPos, $tagLen);
$this->text->overwrite($tagPos, $tagLen);
if ($listIndex < $listsCnt)
{
$this->parser->addEndTag('LI', $textBoundary, 0)
->pairWith($lists[$listIndex]['itemTag']);
$lists[$listIndex]['itemTag'] = $itemTag;
$lists[$listIndex]['itemTags'][] = $itemTag;
}
else
{
++$listsCnt;
if ($listIndex)
{
$minIndent = $lists[$listIndex - 1]['maxIndent'] + 1;
$maxIndent = \max($minIndent, $listIndex * 4);
}
else
{
$minIndent = 0;
$maxIndent = $indentWidth;
}
$listTag = $this->parser->addStartTag('LIST', $tagPos, 0);
if (\strpos($m[4][0], '.') !== \false)
{
$listTag->setAttribute('type', 'decimal');
$start = (int) $m[4][0];
if ($start !== 1)
$listTag->setAttribute('start', $start);
}
$lists[] = [
'listTag' => $listTag,
'itemTag' => $itemTag,
'itemTags' => [$itemTag],
'minIndent' => $minIndent,
'maxIndent' => $maxIndent,
'tight' => \true
];
}
}
if ($listsCnt && !$continuation && !$lineIsEmpty)
if (\count($lists[0]['itemTags']) > 1 || !$hasListItem)
{
foreach ($lists as &$list)
$list['tight'] = \false;
unset($list);
}
$codeIndent = ($listsCnt + 1) * 4;
}
if (isset($m[5]))
{
if ($m[5][0][0] === '#')
{
$startLen = \strlen($m[5][0]);
$startPos = $matchPos + $matchLen - $startLen;
$endLen = $this->getAtxHeaderEndTagLen($matchPos + $matchLen, $lfPos);
$endPos = $lfPos - $endLen;
$this->parser->addTagPair('H' . \strspn($m[5][0], '#', 0, 6), $startPos, $startLen, $endPos, $endLen);
$this->text->markBoundary($startPos);
$this->text->markBoundary($lfPos);
if ($continuation)
$breakParagraph = \true;
}
elseif ($m[5][0][0] === '`' || $m[5][0][0] === '~')
{
$tagPos = $matchPos + $ignoreLen;
$tagLen = $lfPos - $tagPos;
if (isset($codeTag) && $m[5][0] === $codeFence)
{
$endTag = $this->parser->addEndTag('CODE', $tagPos, $tagLen, -1);
$endTag->pairWith($codeTag);
$this->parser->addIgnoreTag($textBoundary, $tagPos - $textBoundary);
$this->text->overwrite($codeTag->getPos(), $tagPos + $tagLen - $codeTag->getPos());
$codeTag = \null;
$codeFence = \null;
}
elseif (!isset($codeTag))
{
$codeTag = $this->parser->addStartTag('CODE', $tagPos, $tagLen);
$codeFence = \substr($m[5][0], 0, \strspn($m[5][0], '`~'));
$codeTag->setAttribute('quoteDepth', $quoteDepth);
$this->parser->addIgnoreTag($tagPos + $tagLen, 1);
$lang = \trim(\trim($m[5][0], '`~'));
if ($lang !== '')
$codeTag->setAttribute('lang', $lang);
}
}
}
elseif (!empty($m[3][0]) && !$listsCnt && $this->text->charAt($matchPos + $matchLen) !== "\x17")
{
$this->parser->addSelfClosingTag('HR', $matchPos + $ignoreLen, $matchLen - $ignoreLen);
$breakParagraph = \true;
$this->text->markBoundary($lfPos);
}
elseif (isset($this->setextLines[$lfPos]) && $this->setextLines[$lfPos]['quoteDepth'] === $quoteDepth && !$lineIsEmpty && !$listsCnt && !isset($codeTag))
{
$this->parser->addTagPair(
$this->setextLines[$lfPos]['tagName'],
$matchPos + $ignoreLen,
0,
$this->setextLines[$lfPos]['endPos'],
$this->setextLines[$lfPos]['endLen']
);
$this->text->markBoundary($this->setextLines[$lfPos]['endPos'] + $this->setextLines[$lfPos]['endLen']);
}
if ($breakParagraph)
{
$this->parser->addParagraphBreak($textBoundary);
$this->text->markBoundary($textBoundary);
}
if (!$lineIsEmpty)
$textBoundary = $lfPos;
if ($ignoreLen)
$this->parser->addIgnoreTag($matchPos, $ignoreLen, 1000);
}
}
protected function closeList(array $list, $textBoundary)
{
$this->parser->addEndTag('LIST', $textBoundary, 0)->pairWith($list['listTag']);
$this->parser->addEndTag('LI', $textBoundary, 0)->pairWith($list['itemTag']);
if ($list['tight'])
foreach ($list['itemTags'] as $itemTag)
$itemTag->removeFlags(Rules::RULE_CREATE_PARAGRAPHS);
}
protected function computeQuoteIgnoreLen($str, $maxQuoteDepth)
{
$remaining = $str;
while (--$maxQuoteDepth >= 0)
$remaining = \preg_replace('/^ *> ?/', '', $remaining);
return \strlen($str) - \strlen($remaining);
}
protected function getAtxHeaderEndTagLen($startPos, $endPos)
{
$content = \substr($this->text, $startPos, $endPos - $startPos);
\preg_match('/[ \\t]*#*[ \\t]*$/', $content, $m);
return \strlen($m[0]);
}
protected function matchSetextLines()
{
if ($this->text->indexOf('-') === \false && $this->text->indexOf('=') === \false)
return;
$regexp = '/^(?=[-=>])(?:> ?)*(?=[-=])(?:-+|=+) *$/m';
if (!\preg_match_all($regexp, $this->text, $matches, \PREG_OFFSET_CAPTURE))
return;
foreach ($matches[0] as $_4b034d25)
{
list($match, $matchPos) = $_4b034d25;
$endPos = $matchPos - 1;
while ($endPos > 0 && $this->text->charAt($endPos - 1) === ' ')
--$endPos;
$this->setextLines[$matchPos - 1] = [
'endLen' => $matchPos + \strlen($match) - $endPos,
'endPos' => $endPos,
'quoteDepth' => \substr_count($match, '>'),
'tagName' => ($match[0] === '=') ? 'H1' : 'H2'
];
}
}
}

View File

@ -0,0 +1,230 @@
/**
* @param {boolean} Whether current EM span is being closed by current emphasis mark
*/
var closeEm;
/**
* @param {boolean} Whether current EM span is being closed by current emphasis mark
*/
var closeStrong;
/**
* @param {number} Starting position of the current EM span in the text
*/
var emPos;
/**
* @param {number} Ending position of the current EM span in the text
*/
var emEndPos;
/**
* @param {number} Number of emphasis characters unused in current span
*/
var remaining;
/**
* @param {number} Starting position of the current STRONG span in the text
*/
var strongPos;
/**
* @param {number} Ending position of the current STRONG span in the text
*/
var strongEndPos;
function parse()
{
parseEmphasisByCharacter('*', /\*+/g);
parseEmphasisByCharacter('_', /_+/g);
}
/**
* Adjust the ending position of current EM and STRONG spans
*/
function adjustEndingPositions()
{
if (closeEm && closeStrong)
{
if (emPos < strongPos)
{
emEndPos += 2;
}
else
{
++strongEndPos;
}
}
}
/**
* Adjust the starting position of current EM and STRONG spans
*
* If both EM and STRONG are set to start at the same position, we adjust their position
* to match the order they are closed. If they start and end at the same position, STRONG
* starts before EM to match Markdown's behaviour
*/
function adjustStartingPositions()
{
if (emPos !== null && emPos === strongPos)
{
if (closeEm)
{
emPos += 2;
}
else
{
++strongPos;
}
}
}
/**
* End current valid EM and STRONG spans
*/
function closeSpans()
{
if (closeEm)
{
--remaining;
addTagPair('EM', emPos, 1, emEndPos, 1);
emPos = null;
}
if (closeStrong)
{
remaining -= 2;
addTagPair('STRONG', strongPos, 2, strongEndPos, 2);
strongPos = null;
}
}
/**
* Get emphasis markup split by block
*
* @param {!RegExp} regexp Regexp used to match emphasis
* @param {number} pos Position in the text of the first emphasis character
* @return {!Array} Each array contains a list of [matchPos, matchLen] pairs
*/
function getEmphasisByBlock(regexp, pos)
{
var block = [],
blocks = [],
breakPos = text.indexOf("\x17", pos),
m;
regexp.lastIndex = pos;
while (m = regexp.exec(text))
{
var matchPos = m['index'],
matchLen = m[0].length;
// Test whether we've just passed the limits of a block
if (matchPos > breakPos)
{
blocks.push(block);
block = [];
breakPos = text.indexOf("\x17", matchPos);
}
// Test whether we should ignore this markup
if (!ignoreEmphasis(matchPos, matchLen))
{
block.push([matchPos, matchLen]);
}
}
blocks.push(block);
return blocks;
}
/**
* Test whether emphasis should be ignored at the given position in the text
*
* @param {number} pos Position of the emphasis in the text
* @param {number} len Length of the emphasis
* @return {boolean}
*/
function ignoreEmphasis(pos, len)
{
// Ignore single underscores between alphanumeric characters
return (text.charAt(pos) === '_' && len === 1 && isSurroundedByAlnum(pos, len));
}
/**
* Open EM and STRONG spans whose content starts at given position
*
* @param {number} pos
*/
function openSpans(pos)
{
if (remaining & 1)
{
emPos = pos - remaining;
}
if (remaining & 2)
{
strongPos = pos - remaining;
}
}
/**
* Parse emphasis and strong applied using given character
*
* @param {string} character Markup character, either * or _
* @param {!RegExp} regexp Regexp used to match the series of emphasis character
*/
function parseEmphasisByCharacter(character, regexp)
{
var pos = text.indexOf(character);
if (pos === -1)
{
return;
}
getEmphasisByBlock(regexp, pos).forEach(processEmphasisBlock);
}
/**
* Process a list of emphasis markup strings
*
* @param {!Array<!Array<!number>>} block List of [matchPos, matchLen] pairs
*/
function processEmphasisBlock(block)
{
emPos = null,
strongPos = null;
block.forEach(function(pair)
{
processEmphasisMatch(pair[0], pair[1]);
});
}
/**
* Process an emphasis mark
*
* @param {number} matchPos
* @param {number} matchLen
*/
function processEmphasisMatch(matchPos, matchLen)
{
var canOpen = !isBeforeWhitespace(matchPos + matchLen - 1),
canClose = !isAfterWhitespace(matchPos),
closeLen = (canClose) ? Math.min(matchLen, 3) : 0;
closeEm = (closeLen & 1) && emPos !== null;
closeStrong = (closeLen & 2) && strongPos !== null;
emEndPos = matchPos;
strongEndPos = matchPos;
remaining = matchLen;
adjustStartingPositions();
adjustEndingPositions();
closeSpans();
// Adjust the length of unused markup remaining in current match
remaining = (canOpen) ? Math.min(remaining, 3) : 0;
openSpans(matchPos + matchLen);
}

View File

@ -0,0 +1,121 @@
<?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\Plugins\Litedown\Parser\Passes;
class Emphasis extends AbstractPass
{
protected $closeEm;
protected $closeStrong;
protected $emPos;
protected $emEndPos;
protected $remaining;
protected $strongPos;
protected $strongEndPos;
public function parse()
{
$this->parseEmphasisByCharacter('*', '/\\*+/');
$this->parseEmphasisByCharacter('_', '/_+/');
}
protected function adjustEndingPositions()
{
if ($this->closeEm && $this->closeStrong)
if ($this->emPos < $this->strongPos)
$this->emEndPos += 2;
else
++$this->strongEndPos;
}
protected function adjustStartingPositions()
{
if (isset($this->emPos) && $this->emPos === $this->strongPos)
if ($this->closeEm)
$this->emPos += 2;
else
++$this->strongPos;
}
protected function closeSpans()
{
if ($this->closeEm)
{
--$this->remaining;
$this->parser->addTagPair('EM', $this->emPos, 1, $this->emEndPos, 1);
$this->emPos = \null;
}
if ($this->closeStrong)
{
$this->remaining -= 2;
$this->parser->addTagPair('STRONG', $this->strongPos, 2, $this->strongEndPos, 2);
$this->strongPos = \null;
}
}
protected function parseEmphasisByCharacter($character, $regexp)
{
$pos = $this->text->indexOf($character);
if ($pos === \false)
return;
foreach ($this->getEmphasisByBlock($regexp, $pos) as $block)
$this->processEmphasisBlock($block);
}
protected function getEmphasisByBlock($regexp, $pos)
{
$block = [];
$blocks = [];
$breakPos = $this->text->indexOf("\x17", $pos);
\preg_match_all($regexp, $this->text, $matches, \PREG_OFFSET_CAPTURE, $pos);
foreach ($matches[0] as $m)
{
$matchPos = $m[1];
$matchLen = \strlen($m[0]);
if ($matchPos > $breakPos)
{
$blocks[] = $block;
$block = [];
$breakPos = $this->text->indexOf("\x17", $matchPos);
}
if (!$this->ignoreEmphasis($matchPos, $matchLen))
$block[] = [$matchPos, $matchLen];
}
$blocks[] = $block;
return $blocks;
}
protected function ignoreEmphasis($matchPos, $matchLen)
{
return ($this->text->charAt($matchPos) === '_' && $matchLen === 1 && $this->text->isSurroundedByAlnum($matchPos, $matchLen));
}
protected function openSpans($pos)
{
if ($this->remaining & 1)
$this->emPos = $pos - $this->remaining;
if ($this->remaining & 2)
$this->strongPos = $pos - $this->remaining;
}
protected function processEmphasisBlock(array $block)
{
$this->emPos = \null;
$this->strongPos = \null;
foreach ($block as $_aab3a45e)
{
list($matchPos, $matchLen) = $_aab3a45e;
$this->processEmphasisMatch($matchPos, $matchLen);
}
}
protected function processEmphasisMatch($matchPos, $matchLen)
{
$canOpen = !$this->text->isBeforeWhitespace($matchPos + $matchLen - 1);
$canClose = !$this->text->isAfterWhitespace($matchPos);
$closeLen = ($canClose) ? \min($matchLen, 3) : 0;
$this->closeEm = ($closeLen & 1) && isset($this->emPos);
$this->closeStrong = ($closeLen & 2) && isset($this->strongPos);
$this->emEndPos = $matchPos;
$this->strongEndPos = $matchPos;
$this->remaining = $matchLen;
$this->adjustStartingPositions();
$this->adjustEndingPositions();
$this->closeSpans();
$this->remaining = ($canOpen) ? \min($this->remaining, 3) : 0;
$this->openSpans($matchPos + $matchLen);
}
}

View File

@ -0,0 +1,9 @@
function parse()
{
var pos = text.indexOf(" \n");
while (pos > 0)
{
addBrTag(pos + 2);
pos = text.indexOf(" \n", pos + 3);
}
}

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\Plugins\Litedown\Parser\Passes;
class ForcedLineBreaks extends AbstractPass
{
public function parse()
{
$pos = $this->text->indexOf(" \n");
while ($pos !== \false)
{
$this->parser->addBrTag($pos + 2);
$pos = $this->text->indexOf(" \n", $pos + 3);
}
}
}

View File

@ -0,0 +1,81 @@
function parse()
{
var pos = text.indexOf('![');
if (pos === -1)
{
return;
}
if (text.indexOf('](', pos) > 0)
{
parseInlineImages();
}
if (hasReferences)
{
parseReferenceImages();
}
}
/**
* Add an image tag for given text span
*
* @param {number} startPos Start tag position
* @param {number} endPos End tag position
* @param {number} endLen End tag length
* @param {string} linkInfo URL optionally followed by space and a title
* @param {string} alt Value for the alt attribute
*/
function addImageTag(startPos, endPos, endLen, linkInfo, alt)
{
var tag = addTagPair('IMG', startPos, 2, endPos, endLen);
setLinkAttributes(tag, linkInfo, 'src');
tag.setAttribute('alt', decode(alt));
// Overwrite the markup
overwrite(startPos, endPos + endLen - startPos);
}
/**
* Parse inline images markup
*/
function parseInlineImages()
{
var m, regexp = /!\[(?:[^\x17[\]]|\[[^\x17[\]]*\])*\]\(( *(?:[^\x17\s()]|\([^\x17\s()]*\))*(?=[ )]) *(?:"[^\x17]*?"|'[^\x17]*?'|\([^\x17)]*\))? *)\)/g;
while (m = regexp.exec(text))
{
var linkInfo = m[1],
startPos = m['index'],
endLen = 3 + linkInfo.length,
endPos = startPos + m[0].length - endLen,
alt = m[0].substr(2, m[0].length - endLen - 2);
addImageTag(startPos, endPos, endLen, linkInfo, alt);
}
}
/**
* Parse reference images markup
*/
function parseReferenceImages()
{
var m, regexp = /!\[((?:[^\x17[\]]|\[[^\x17[\]]*\])*)\](?: ?\[([^\x17[\]]+)\])?/g;
while (m = regexp.exec(text))
{
var startPos = +m['index'],
endPos = startPos + 2 + m[1].length,
endLen = 1,
alt = m[1],
id = alt;
if (m[2] > '' && linkReferences[m[2]])
{
endLen = m[0].length - alt.length - 2;
id = m[2];
}
else if (!linkReferences[id])
{
continue;
}
addImageTag(startPos, endPos, endLen, linkReferences[id], alt);
}
}

View File

@ -0,0 +1,73 @@
<?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\Plugins\Litedown\Parser\Passes;
use s9e\TextFormatter\Plugins\Litedown\Parser\LinkAttributesSetter;
class Images extends AbstractPass
{
use LinkAttributesSetter;
public function parse()
{
$pos = $this->text->indexOf('![');
if ($pos === \false)
return;
if ($this->text->indexOf('](', $pos) !== \false)
$this->parseInlineImages();
if ($this->text->hasReferences)
$this->parseReferenceImages();
}
protected function addImageTag($startPos, $endPos, $endLen, $linkInfo, $alt)
{
$tag = $this->parser->addTagPair('IMG', $startPos, 2, $endPos, $endLen);
$this->setLinkAttributes($tag, $linkInfo, 'src');
$tag->setAttribute('alt', $this->text->decode($alt));
$this->text->overwrite($startPos, $endPos + $endLen - $startPos);
}
protected function parseInlineImages()
{
\preg_match_all(
'/!\\[(?:[^\\x17[\\]]|\\[[^\\x17[\\]]*\\])*\\]\\(( *(?:[^\\x17\\s()]|\\([^\\x17\\s()]*\\))*(?=[ )]) *(?:"[^\\x17]*?"|\'[^\\x17]*?\'|\\([^\\x17)]*\\))? *)\\)/',
$this->text,
$matches,
\PREG_OFFSET_CAPTURE | \PREG_SET_ORDER
);
foreach ($matches as $m)
{
$linkInfo = $m[1][0];
$startPos = $m[0][1];
$endLen = 3 + \strlen($linkInfo);
$endPos = $startPos + \strlen($m[0][0]) - $endLen;
$alt = \substr($m[0][0], 2, \strlen($m[0][0]) - $endLen - 2);
$this->addImageTag($startPos, $endPos, $endLen, $linkInfo, $alt);
}
}
protected function parseReferenceImages()
{
\preg_match_all(
'/!\\[((?:[^\\x17[\\]]|\\[[^\\x17[\\]]*\\])*)\\](?: ?\\[([^\\x17[\\]]+)\\])?/',
$this->text,
$matches,
\PREG_OFFSET_CAPTURE | \PREG_SET_ORDER
);
foreach ($matches as $m)
{
$startPos = $m[0][1];
$endPos = $startPos + 2 + \strlen($m[1][0]);
$endLen = 1;
$alt = $m[1][0];
$id = $alt;
if (isset($m[2][0], $this->text->linkReferences[$m[2][0]]))
{
$endLen = \strlen($m[0][0]) - \strlen($alt) - 2;
$id = $m[2][0];
}
elseif (!isset($this->text->linkReferences[$id]))
continue;
$this->addImageTag($startPos, $endPos, $endLen, $this->text->linkReferences[$id], $alt);
}
}
}

View File

@ -0,0 +1,78 @@
function parse()
{
var markers = getInlineCodeMarkers(),
i = -1,
cnt = markers.length;
while (++i < (cnt - 1))
{
var pos = markers[i].next,
j = i;
if (text[markers[i].pos] !== '`')
{
// Adjust the left marker if its first backtick was escaped
++markers[i].pos;
--markers[i].len;
}
while (++j < cnt && markers[j].pos === pos)
{
if (markers[j].len === markers[i].len)
{
addInlineCodeTags(markers[i], markers[j]);
i = j;
break;
}
pos = markers[j].next;
}
}
}
/**
* Add the tag pair for an inline code span
*
* @param {!Object} left Left marker
* @param {!Object} right Right marker
*/
function addInlineCodeTags(left, right)
{
var startPos = left.pos,
startLen = left.len + left.trimAfter,
endPos = right.pos - right.trimBefore,
endLen = right.len + right.trimBefore;
addTagPair('C', startPos, startLen, endPos, endLen);
overwrite(startPos, endPos + endLen - startPos);
}
/**
* Capture and return inline code markers
*
* @return {!Array<!Object>}
*/
function getInlineCodeMarkers()
{
var pos = text.indexOf('`');
if (pos < 0)
{
return [];
}
var regexp = /(`+)(\s*)[^\x17`]*/g,
trimNext = 0,
markers = [],
_text = text.replace(/\x1BB/g, '\\`'),
m;
regexp.lastIndex = pos;
while (m = regexp.exec(_text))
{
markers.push({
pos : m['index'],
len : m[1].length,
trimBefore : trimNext,
trimAfter : m[2].length,
next : m['index'] + m[0].length
});
trimNext = m[0].length - m[0].replace(/\s+$/, '').length;
}
return markers;
}

View File

@ -0,0 +1,73 @@
<?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\Plugins\Litedown\Parser\Passes;
class InlineCode extends AbstractPass
{
public function parse()
{
$markers = $this->getInlineCodeMarkers();
$i = -1;
$cnt = \count($markers);
while (++$i < ($cnt - 1))
{
$pos = $markers[$i]['next'];
$j = $i;
if ($this->text->charAt($markers[$i]['pos']) !== '`')
{
++$markers[$i]['pos'];
--$markers[$i]['len'];
}
while (++$j < $cnt && $markers[$j]['pos'] === $pos)
{
if ($markers[$j]['len'] === $markers[$i]['len'])
{
$this->addInlineCodeTags($markers[$i], $markers[$j]);
$i = $j;
break;
}
$pos = $markers[$j]['next'];
}
}
}
protected function addInlineCodeTags($left, $right)
{
$startPos = $left['pos'];
$startLen = $left['len'] + $left['trimAfter'];
$endPos = $right['pos'] - $right['trimBefore'];
$endLen = $right['len'] + $right['trimBefore'];
$this->parser->addTagPair('C', $startPos, $startLen, $endPos, $endLen);
$this->text->overwrite($startPos, $endPos + $endLen - $startPos);
}
protected function getInlineCodeMarkers()
{
$pos = $this->text->indexOf('`');
if ($pos === \false)
return [];
\preg_match_all(
'/(`+)(\\s*)[^\\x17`]*/',
\str_replace("\x1BB", '\\`', $this->text),
$matches,
\PREG_OFFSET_CAPTURE | \PREG_SET_ORDER,
$pos
);
$trimNext = 0;
$markers = [];
foreach ($matches as $m)
{
$markers[] = [
'pos' => $m[0][1],
'len' => \strlen($m[1][0]),
'trimBefore' => $trimNext,
'trimAfter' => \strlen($m[2][0]),
'next' => $m[0][1] + \strlen($m[0][0])
];
$trimNext = \strlen($m[0][0]) - \strlen(\rtrim($m[0][0]));
}
return $markers;
}
}

View File

@ -0,0 +1,21 @@
function parse()
{
if (text.indexOf(']:') < 0)
{
return;
}
var m, regexp = /^\x1A* {0,3}\[([^\x17\]]+)\]: *([^\s\x17]+ *(?:"[^\x17]*?"|'[^\x17]*?'|\([^\x17)]*\))?)[^\x17\n]*\n?/gm;
while (m = regexp.exec(text))
{
addIgnoreTag(m['index'], m[0].length, -2);
// Only add the reference if it does not already exist
var id = m[1].toLowerCase();
if (!linkReferences[id])
{
hasReferences = true;
linkReferences[id] = m[2];
}
}
}

View File

@ -0,0 +1,28 @@
<?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\Plugins\Litedown\Parser\Passes;
class LinkReferences extends AbstractPass
{
public function parse()
{
if ($this->text->indexOf(']:') === \false)
return;
$regexp = '/^\\x1A* {0,3}\\[([^\\x17\\]]+)\\]: *([^\\s\\x17]+ *(?:"[^\\x17]*?"|\'[^\\x17]*?\'|\\([^\\x17)]*\\))?)[^\\x17\\n]*\\n?/m';
\preg_match_all($regexp, $this->text, $matches, \PREG_OFFSET_CAPTURE | \PREG_SET_ORDER);
foreach ($matches as $m)
{
$this->parser->addIgnoreTag($m[0][1], \strlen($m[0][0]), -2);
$id = \strtolower($m[1][0]);
if (!isset($this->text->linkReferences[$id]))
{
$this->text->hasReferences = \true;
$this->text->linkReferences[$id] = $m[2][0];
}
}
}
}

View File

@ -0,0 +1,96 @@
function parse()
{
if (text.indexOf('](') !== -1)
{
parseInlineLinks();
}
if (hasReferences)
{
parseReferenceLinks();
}
}
/**
* Add an image tag for given text span
*
* @param {number} startPos Start tag position
* @param {number} endPos End tag position
* @param {number} endLen End tag length
* @param {string} linkInfo URL optionally followed by space and a title
*/
function addLinkTag(startPos, endPos, endLen, linkInfo)
{
// Give the link a slightly worse priority if this is a implicit reference and a slightly
// better priority if it's an explicit reference or an inline link or to give it precedence
// over possible BBCodes such as [b](https://en.wikipedia.org/wiki/B)
var priority = (endLen === 1) ? 1 : -1;
var tag = addTagPair('URL', startPos, 1, endPos, endLen, priority);
setLinkAttributes(tag, linkInfo, 'url');
// Overwrite the markup without touching the link's text
overwrite(startPos, 1);
overwrite(endPos, endLen);
}
/**
* Capture and return labels used in current text
*
* @return {!Object} Labels' text position as keys, lowercased text content as values
*/
function getLabels()
{
var labels = {}, m, regexp = /\[((?:[^\x17[\]]|\[[^\x17[\]]*\])*)\]/g;
while (m = regexp.exec(text))
{
labels[m['index']] = m[1].toLowerCase();
}
return labels;
}
/**
* Parse inline links markup
*/
function parseInlineLinks()
{
var m, regexp = /\[(?:[^\x17[\]]|\[[^\x17[\]]*\])*\]\(( *(?:[^\x17\s()]|\([^\x17\s()]*\))*(?=[ )]) *(?:"[^\x17]*?"|'[^\x17]*?'|\([^\x17)]*\))? *)\)/g;
while (m = regexp.exec(text))
{
var linkInfo = m[1],
startPos = m['index'],
endLen = 3 + linkInfo.length,
endPos = startPos + m[0].length - endLen;
addLinkTag(startPos, endPos, endLen, linkInfo);
}
}
/**
* Parse reference links markup
*/
function parseReferenceLinks()
{
var labels = getLabels(), startPos;
for (startPos in labels)
{
var id = labels[startPos],
labelPos = +startPos + 2 + id.length,
endPos = labelPos - 1,
endLen = 1;
if (text[labelPos] === ' ')
{
++labelPos;
}
if (labels[labelPos] > '' && linkReferences[labels[labelPos]])
{
id = labels[labelPos];
endLen = labelPos + 2 + id.length - endPos;
}
if (linkReferences[id])
{
addLinkTag(+startPos, endPos, endLen, linkReferences[id]);
}
}
}

View File

@ -0,0 +1,77 @@
<?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\Plugins\Litedown\Parser\Passes;
use s9e\TextFormatter\Plugins\Litedown\Parser\LinkAttributesSetter;
class Links extends AbstractPass
{
use LinkAttributesSetter;
public function parse()
{
if ($this->text->indexOf('](') !== \false)
$this->parseInlineLinks();
if ($this->text->hasReferences)
$this->parseReferenceLinks();
}
protected function addLinkTag($startPos, $endPos, $endLen, $linkInfo)
{
$priority = ($endLen === 1) ? 1 : -1;
$tag = $this->parser->addTagPair('URL', $startPos, 1, $endPos, $endLen, $priority);
$this->setLinkAttributes($tag, $linkInfo, 'url');
$this->text->overwrite($startPos, 1);
$this->text->overwrite($endPos, $endLen);
}
protected function getLabels()
{
\preg_match_all(
'/\\[((?:[^\\x17[\\]]|\\[[^\\x17[\\]]*\\])*)\\]/',
$this->text,
$matches,
\PREG_OFFSET_CAPTURE
);
$labels = [];
foreach ($matches[1] as $m)
$labels[$m[1] - 1] = \strtolower($m[0]);
return $labels;
}
protected function parseInlineLinks()
{
\preg_match_all(
'/\\[(?:[^\\x17[\\]]|\\[[^\\x17[\\]]*\\])*\\]\\(( *(?:[^\\x17\\s()]|\\([^\\x17\\s()]*\\))*(?=[ )]) *(?:"[^\\x17]*?"|\'[^\\x17]*?\'|\\([^\\x17)]*\\))? *)\\)/',
$this->text,
$matches,
\PREG_OFFSET_CAPTURE | \PREG_SET_ORDER
);
foreach ($matches as $m)
{
$linkInfo = $m[1][0];
$startPos = $m[0][1];
$endLen = 3 + \strlen($linkInfo);
$endPos = $startPos + \strlen($m[0][0]) - $endLen;
$this->addLinkTag($startPos, $endPos, $endLen, $linkInfo);
}
}
protected function parseReferenceLinks()
{
$labels = $this->getLabels();
foreach ($labels as $startPos => $id)
{
$labelPos = $startPos + 2 + \strlen($id);
$endPos = $labelPos - 1;
$endLen = 1;
if ($this->text->charAt($labelPos) === ' ')
++$labelPos;
if (isset($labels[$labelPos], $this->text->linkReferences[$labels[$labelPos]]))
{
$id = $labels[$labelPos];
$endLen = $labelPos + 2 + \strlen($id) - $endPos;
}
if (isset($this->text->linkReferences[$id]))
$this->addLinkTag($startPos, $endPos, $endLen, $this->text->linkReferences[$id]);
}
}
}

View File

@ -0,0 +1,20 @@
function parse()
{
if (text.indexOf('~~') === -1)
{
return;
}
var m, regexp = /~~[^\x17]+?~~(?!~)/g;
while (m = regexp.exec(text))
{
var match = m[0],
matchPos = m['index'],
matchLen = match.length,
endPos = matchPos + matchLen - 2;
addTagPair('DEL', matchPos, 2, endPos, 2);
overwrite(matchPos, 2);
overwrite(endPos, 2);
}
}

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\Plugins\Litedown\Parser\Passes;
class Strikethrough extends AbstractPass
{
public function parse()
{
$pos = $this->text->indexOf('~~');
if ($pos === \false)
return;
\preg_match_all('/~~[^\\x17]+?~~(?!~)/', $this->text, $matches, \PREG_OFFSET_CAPTURE, $pos);
foreach ($matches[0] as $_4b034d25)
{
list($match, $matchPos) = $_4b034d25;
$matchLen = \strlen($match);
$endPos = $matchPos + $matchLen - 2;
$this->parser->addTagPair('DEL', $matchPos, 2, $endPos, 2);
$this->text->overwrite($matchPos, 2);
$this->text->overwrite($endPos, 2);
}
}
}

View File

@ -0,0 +1,4 @@
function parse()
{
parseAbstractScript('SUB', '~', /~(?!\()[^\x17\s~()]+~?/g, /~\([^\x17()]+\)/g);
}

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\Plugins\Litedown\Parser\Passes;
class Subscript extends AbstractScript
{
public function parse()
{
$this->parseAbstractScript('SUB', '~', '/~(?!\\()[^\\x17\\s~()]++~?/', '/~\\([^\\x17()]+\\)/');
}
}

View File

@ -0,0 +1,4 @@
function parse()
{
parseAbstractScript('SUP', '^', /\^(?!\()[^\x17\s^()]+\^?/g, /\^\([^\x17()]+\)/g);
}

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\Plugins\Litedown\Parser\Passes;
class Superscript extends AbstractScript
{
public function parse()
{
$this->parseAbstractScript('SUP', '^', '/\\^(?!\\()[^\\x17\\s^()]++\\^?/', '/\\^\\([^\\x17()]++\\)/');
}
}

View File

@ -0,0 +1,204 @@
<?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\Plugins\MediaEmbed;
use InvalidArgumentException;
use RuntimeException;
use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
use s9e\TextFormatter\Configurator\Items\Attribute;
use s9e\TextFormatter\Configurator\Items\AttributePreprocessor;
use s9e\TextFormatter\Configurator\Items\Tag;
use s9e\TextFormatter\Plugins\ConfiguratorBase;
use s9e\TextFormatter\Plugins\MediaEmbed\Configurator\Collections\CachedDefinitionCollection;
use s9e\TextFormatter\Plugins\MediaEmbed\Configurator\Collections\SiteCollection;
use s9e\TextFormatter\Plugins\MediaEmbed\Configurator\TemplateBuilder;
class Configurator extends ConfiguratorBase
{
public $allowedFilters = [
'hexdec',
'stripslashes',
'urldecode'
];
protected $appendTemplate = '';
public $captureURLs = \true;
protected $collection;
protected $createMediaBBCode = \true;
public $createIndividualBBCodes = \false;
public $defaultSites;
protected $tagName = 'MEDIA';
protected $templateBuilder;
protected function setUp()
{
$this->collection = new SiteCollection;
$this->configurator->registeredVars['mediasites'] = $this->collection;
$tag = $this->configurator->tags->add($this->tagName);
$tag->rules->autoClose();
$tag->rules->denyChild($this->tagName);
$tag->filterChain->clear();
$tag->filterChain
->append([__NAMESPACE__ . '\\Parser', 'filterTag'])
->addParameterByName('parser')
->addParameterByName('mediasites')
->setJS(\file_get_contents(__DIR__ . '/Parser/tagFilter.js'));
if ($this->createMediaBBCode)
$this->configurator->BBCodes->set(
$this->tagName,
[
'contentAttributes' => ['url'],
'defaultAttribute' => 'site'
]
);
if (!isset($this->defaultSites))
$this->defaultSites = new CachedDefinitionCollection;
$this->templateBuilder = new TemplateBuilder;
}
public function asConfig()
{
if (!$this->captureURLs || !\count($this->collection))
return;
$regexp = 'https?:\\/\\/';
$schemes = $this->getSchemes();
if (!empty($schemes))
$regexp = '(?>' . RegexpBuilder::fromList($schemes) . ':|' . $regexp . ')';
return [
'quickMatch' => (empty($schemes)) ? '://' : ':',
'regexp' => '/\\b' . $regexp . '[^["\'\\s]+/Si',
'tagName' => $this->tagName
];
}
public function add($siteId, array $siteConfig = \null)
{
$siteId = $this->normalizeId($siteId);
$siteConfig = (isset($siteConfig)) ? $this->defaultSites->normalizeValue($siteConfig) : $this->defaultSites->get($siteId);
$this->collection[$siteId] = $siteConfig;
$tag = new Tag;
$tag->rules->allowChild('URL');
$tag->rules->autoClose();
$tag->rules->denyChild($siteId);
$tag->rules->denyChild($this->tagName);
$attributes = [
'url' => ['type' => 'url']
];
$attributes += $this->addScrapes($tag, $siteConfig['scrape']);
foreach ($siteConfig['extract'] as $regexp)
{
$attrRegexps = $tag->attributePreprocessors->add('url', $regexp)->getAttributes();
foreach ($attrRegexps as $attrName => $attrRegexp)
$attributes[$attrName]['regexp'] = $attrRegexp;
}
if (isset($siteConfig['attributes']))
foreach ($siteConfig['attributes'] as $attrName => $attrConfig)
foreach ($attrConfig as $configName => $configValue)
$attributes[$attrName][$configName] = $configValue;
$hasRequiredAttribute = \false;
foreach ($attributes as $attrName => $attrConfig)
{
$attribute = $this->addAttribute($tag, $attrName, $attrConfig);
$hasRequiredAttribute |= $attribute->required;
}
if (isset($attributes['id']['regexp']))
{
$attrRegexp = \preg_replace('(\\^(.*)\\$)s', "^(?'id'$1)$", $attributes['id']['regexp']);
$tag->attributePreprocessors->add('url', $attrRegexp);
}
if (!$hasRequiredAttribute)
$tag->filterChain
->append([__NAMESPACE__ . '\\Parser', 'hasNonDefaultAttribute'])
->setJS(\file_get_contents(__DIR__ . '/Parser/hasNonDefaultAttribute.js'));
$tag->template = $this->templateBuilder->build($siteId, $siteConfig) . $this->appendTemplate;
$this->configurator->templateNormalizer->normalizeTag($tag);
$this->configurator->templateChecker->checkTag($tag);
$this->configurator->tags->add($siteId, $tag);
if ($this->createIndividualBBCodes)
$this->configurator->BBCodes->add(
$siteId,
[
'defaultAttribute' => 'url',
'contentAttributes' => ['url']
]
);
return $tag;
}
public function appendTemplate($template = '')
{
$this->appendTemplate = $this->configurator->templateNormalizer->normalizeTemplate($template);
}
protected function addAttribute(Tag $tag, $attrName, array $attrConfig)
{
$attribute = $tag->attributes->add($attrName);
if (isset($attrConfig['preFilter']))
$this->appendFilter($attribute, $attrConfig['preFilter']);
if (isset($attrConfig['type']))
{
$filter = $this->configurator->attributeFilters['#' . $attrConfig['type']];
$attribute->filterChain->append($filter);
}
elseif (isset($attrConfig['regexp']))
$attribute->filterChain->append('#regexp')->setRegexp($attrConfig['regexp']);
if (isset($attrConfig['required']))
$attribute->required = $attrConfig['required'];
else
$attribute->required = ($attrName === 'id');
if (isset($attrConfig['postFilter']))
$this->appendFilter($attribute, $attrConfig['postFilter']);
if (isset($attrConfig['defaultValue']))
$attribute->defaultValue = $attrConfig['defaultValue'];
return $attribute;
}
protected function addScrapes(Tag $tag, array $scrapes)
{
$attributes = [];
$scrapeConfig = [];
foreach ($scrapes as $scrape)
{
$attrNames = [];
foreach ($scrape['extract'] as $extractRegexp)
{
$attributePreprocessor = new AttributePreprocessor($extractRegexp);
foreach ($attributePreprocessor->getAttributes() as $attrName => $attrRegexp)
{
$attrNames[] = $attrName;
$attributes[$attrName]['regexp'] = $attrRegexp;
}
}
$attrNames = \array_unique($attrNames);
\sort($attrNames);
$entry = [$scrape['match'], $scrape['extract'], $attrNames];
if (isset($scrape['url']))
$entry[] = $scrape['url'];
$scrapeConfig[] = $entry;
}
$tag->filterChain->insert(1, __NAMESPACE__ . '\\Parser::scrape')
->addParameterByName('scrapeConfig')
->addParameterByName('cacheDir')
->setVar('scrapeConfig', $scrapeConfig)
->setJS('returnTrue');
return $attributes;
}
protected function appendFilter(Attribute $attribute, $filter)
{
if (!\in_array($filter, $this->allowedFilters, \true))
throw new RuntimeException("Filter '" . $filter . "' is not allowed");
$attribute->filterChain->append($this->configurator->attributeFilters[$filter]);
}
protected function getSchemes()
{
$schemes = [];
foreach ($this->collection as $site)
if (isset($site['scheme']))
foreach ((array) $site['scheme'] as $scheme)
$schemes[] = $scheme;
return $schemes;
}
protected function normalizeId($siteId)
{
$siteId = \strtolower($siteId);
if (!\preg_match('(^[a-z0-9]+$)', $siteId))
throw new InvalidArgumentException('Invalid site ID');
return $siteId;
}
}

View File

@ -0,0 +1,135 @@
<?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\Plugins\MediaEmbed\Configurator\Collections;
class CachedDefinitionCollection extends SiteDefinitionCollection
{
protected $items = [
'abcnews'=>['example'=>'http://abcnews.go.com/WNN/video/dog-goes-wild-when-owner-leaves-22936610','extract'=>['!abcnews\\.go\\.com/(?:video/embed\\?id=|[^/]+/video/[^/]+-)(?\'id\'\\d+)!'],'homepage'=>'http://abcnews.go.com/','host'=>'abcnews.go.com','iframe'=>['src'=>'//abcnews.go.com/video/embed?id={@id}'],'name'=>'ABC News','scrape'=>[],'tags'=>['news']],
'amazon'=>['example'=>['http://www.amazon.ca/gp/product/B00GQT1LNO/','http://www.amazon.co.jp/gp/product/B003AKZ6I8/','https://www.amazon.co.uk/dp/B00EO4NN5C/','http://www.amazon.com/dp/B002MUC0ZY','http://www.amazon.com/The-BeerBelly-200-001-80-Ounce-Belly/dp/B001RB2CXY/','https://www.amazon.com/gp/product/B00ST0KGCU/','http://www.amazon.de/Netgear-WN3100RP-100PES-Repeater-integrierte-Steckdose/dp/B00ET2LTE6/','https://www.amazon.es/Microsoft-Sculpt-Ergonomic-Desktop-L5V-00011/dp/B00FO10ZK0/','http://www.amazon.fr/Vans-Authentic-Baskets-mixte-adulte/dp/B005NIKPAY/','http://www.amazon.in/Vans-Unisex-Authentic-Midnight-Sneakers/dp/B01I3LNWQG/','https://www.amazon.it/Super-Maxi-Pot-de-Nutella/dp/B0090GJ8VM/','https://www.amazon.com/dp/B0018CDWLS/'],'extract'=>['#/(?:dp|gp/product)/(?\'id\'[A-Z0-9]+)#','#amazon\\.(?:co\\.)?(?\'tld\'ca|de|es|fr|in|it|jp|uk)#'],'homepage'=>'http://affiliate-program.amazon.com/','host'=>['amazon.ca','amazon.co.uk','amazon.co.jp','amazon.com','amazon.de','amazon.es','amazon.fr','amazon.in','amazon.it'],'iframe'=>['height'=>240,'src'=>'//<xsl:choose><xsl:when test="@tld=\'es\'or@tld=\'it\'">rcm-eu.amazon-adsystem.com/e/cm?lt1=_blank&amp;bc1=FFFFFF&amp;bg1=FFFFFF&amp;fc1=000000&amp;lc1=0000FF&amp;p=8&amp;l=as1&amp;f=ifr&amp;asins=<xsl:value-of select="@id"/>&amp;o=<xsl:choose><xsl:when test="@tld=\'es\'">30</xsl:when><xsl:otherwise>29</xsl:otherwise></xsl:choose>&amp;t=<xsl:choose><xsl:when test="@tld=\'es\'and$AMAZON_ASSOCIATE_TAG_ES"><xsl:value-of select="$AMAZON_ASSOCIATE_TAG_ES"/></xsl:when><xsl:when test="@tld=\'it\'and$AMAZON_ASSOCIATE_TAG_IT"><xsl:value-of select="$AMAZON_ASSOCIATE_TAG_IT"/></xsl:when><xsl:otherwise>_</xsl:otherwise></xsl:choose></xsl:when><xsl:otherwise>ws-<xsl:choose><xsl:when test="@tld=\'in\'">in</xsl:when><xsl:when test="@tld=\'jp\'">fe</xsl:when><xsl:when test="@tld and contains(\'desfrituk\',@tld)">eu</xsl:when><xsl:otherwise>na</xsl:otherwise></xsl:choose>.amazon-adsystem.com/widgets/q?ServiceVersion=20070822&amp;OneJS=1&amp;Operation=GetAdHtml&amp;MarketPlace=<xsl:choose><xsl:when test="@tld"><xsl:value-of select="translate(@tld,\'acdefijknprstu\',\'ACDEFIJBNPRSTG\')"/></xsl:when><xsl:otherwise>US</xsl:otherwise></xsl:choose>&amp;ad_type=product_link&amp;tracking_id=<xsl:choose><xsl:when test="@tld=\'ca\'"><xsl:value-of select="$AMAZON_ASSOCIATE_TAG_CA"/></xsl:when><xsl:when test="@tld=\'de\'"><xsl:value-of select="$AMAZON_ASSOCIATE_TAG_DE"/></xsl:when><xsl:when test="@tld=\'fr\'"><xsl:value-of select="$AMAZON_ASSOCIATE_TAG_FR"/></xsl:when><xsl:when test="@tld=\'in\'"><xsl:value-of select="$AMAZON_ASSOCIATE_TAG_IN"/></xsl:when><xsl:when test="@tld=\'jp\'"><xsl:value-of select="$AMAZON_ASSOCIATE_TAG_JP"/></xsl:when><xsl:when test="@tld=\'uk\'"><xsl:value-of select="$AMAZON_ASSOCIATE_TAG_UK"/></xsl:when><xsl:when test="$AMAZON_ASSOCIATE_TAG"><xsl:value-of select="$AMAZON_ASSOCIATE_TAG"/></xsl:when><xsl:otherwise>-20</xsl:otherwise></xsl:choose>&amp;marketplace=amazon&amp;region=<xsl:choose><xsl:when test="@tld"><xsl:value-of select="translate(@tld,\'acdefijknprstu\',\'ACDEFIJBNPRSTG\')"/></xsl:when><xsl:otherwise>US</xsl:otherwise></xsl:choose>&amp;asins=<xsl:value-of select="@id"/>&amp;show_border=true&amp;link_opens_in_new_window=true</xsl:otherwise></xsl:choose>','width'=>120],'name'=>'Amazon Product','parameters'=>['AMAZON_ASSOCIATE_TAG'=>['title'=>'Amazon Associate tag (.com)'],'AMAZON_ASSOCIATE_TAG_CA'=>['title'=>'Amazon Associate tag (.ca)'],'AMAZON_ASSOCIATE_TAG_DE'=>['title'=>'Amazon Associate tag (.de)'],'AMAZON_ASSOCIATE_TAG_ES'=>['title'=>'Amazon Associate tag (.es)'],'AMAZON_ASSOCIATE_TAG_FR'=>['title'=>'Amazon Associate tag (.fr)'],'AMAZON_ASSOCIATE_TAG_IN'=>['title'=>'Amazon Associate tag (.in)'],'AMAZON_ASSOCIATE_TAG_IT'=>['title'=>'Amazon Associate tag (.it)'],'AMAZON_ASSOCIATE_TAG_JP'=>['title'=>'Amazon Associate tag (.co.jp)'],'AMAZON_ASSOCIATE_TAG_UK'=>['title'=>'Amazon Associate tag (.co.uk)']],'scrape'=>[]],
'audioboom'=>['example'=>['http://audioboo.fm/boos/2439994-deadline-day-update','http://audioboom.com/posts/2493448-robert-patrick'],'extract'=>['!audioboo(?:\\.f|m\\.co)m/(?:boo|post)s/(?\'id\'\\d+)!'],'homepage'=>'https://audioboom.com/','host'=>['audioboo.fm','audioboom.com'],'iframe'=>['height'=>150,'max-width'=>700,'src'=>'//audioboom.com/posts/{@id}/embed/v3','width'=>'100%'],'name'=>'audioBoom','scrape'=>[],'tags'=>['podcasts']],
'audiomack'=>['choose'=>['otherwise'=>['iframe'=>['height'=>252,'max-width'=>900,'src'=>'https://www.audiomack.com/embed/song/{@id}','width'=>'100%']],'when'=>['iframe'=>['height'=>400,'max-width'=>900,'src'=>'https://www.audiomack.com/embed/album/{@id}','width'=>'100%'],'test'=>'@mode=\'album\'']],'example'=>['http://www.audiomack.com/song/your-music-fix/jammin-kungs-remix-1','http://www.audiomack.com/album/chance-the-rapper/acid-rap'],'extract'=>['!audiomack\\.com/(?\'mode\'album|song)/(?\'id\'[-\\w]+/[-\\w]+)!'],'homepage'=>'http://www.audiomack.com/','host'=>'audiomack.com','name'=>'Audiomack','scrape'=>[],'tags'=>['music']],
'bandcamp'=>['example'=>['http://proleter.bandcamp.com/album/curses-from-past-times-ep','http://proleter.bandcamp.com/track/downtown-irony','http://therunons.bandcamp.com/track/still-feel'],'extract'=>[],'homepage'=>'http://bandcamp.com/','host'=>'bandcamp.com','iframe'=>['height'=>400,'src'=>'//bandcamp.com/EmbeddedPlayer/size=large/minimal=true/<xsl:choose><xsl:when test="@album_id">album=<xsl:value-of select="@album_id"/><xsl:if test="@track_num">/t=<xsl:value-of select="@track_num"/></xsl:if></xsl:when><xsl:otherwise>track=<xsl:value-of select="@track_id"/></xsl:otherwise></xsl:choose>','width'=>400],'name'=>'Bandcamp','scrape'=>[['extract'=>['!/album=(?\'album_id\'\\d+)!'],'match'=>['!bandcamp\\.com/album/.!']],['extract'=>['!"album_id":(?\'album_id\'\\d+)!','!"track_num":(?\'track_num\'\\d+)!','!/track=(?\'track_id\'\\d+)!'],'match'=>['!bandcamp\\.com/track/.!']]],'tags'=>['music']],
'bbcnews'=>['attributes'=>['id'=>['postFilter'=>'stripslashes']],'example'=>'http://www.bbc.com/news/science-environment-37854744','extract'=>[],'homepage'=>'http://www.bbc.com/news/video_and_audio/','host'=>'bbc.com','iframe'=>['src'=>'//www.bbc.com<xsl:choose><xsl:when test="starts-with(@playlist,\'/news/\')and contains(@playlist,\'A\')"><xsl:value-of select="substring-before(@playlist,\'A\')"/></xsl:when><xsl:otherwise>/news/<xsl:value-of select="@id"/></xsl:otherwise></xsl:choose>/embed'],'name'=>'BBC News','scrape'=>[['extract'=>['!bbc\\.com\\\\/news\\\\/(?\'id\'[-\\\\\\w/]+)\\\\/embed!'],'match'=>['!bbc\\.com/news/\\w!']]],'tags'=>['news']],
'blab'=>['example'=>'https://blab.im/05b6ce88279f40798069bb6227a04fce','extract'=>['#blab\\.im/(?!about$|live$|replay$|scheduled$|search\\?)(?\'id\'[-\\w]+)#'],'homepage'=>'https://blab.im/','host'=>'blab.im','iframe'=>['height'=>400,'src'=>'https://blab.im/{@id}','width'=>400],'name'=>'Blab','scrape'=>[],'tags'=>['social']],
'bleacherreport'=>['example'=>'http://bleacherreport.com/articles/2418813-steph-curry-salsas-after-teammate-leandro-barbosa-converts-difficult-layup','extract'=>[],'homepage'=>'http://bleacherreport.com/','host'=>'bleacherreport.com','iframe'=>['src'=>'//bleacherreport.com/video_embed?id={@id}'],'name'=>'Bleacher Report videos','scrape'=>[['extract'=>['!id="video-(?\'id\'[-\\w]+)!'],'match'=>['!/articles/.!']]],'tags'=>['sports']],
'break'=>['example'=>'http://www.break.com/video/video-game-playing-frog-wants-more-2278131','extract'=>['!break\\.com/video/.*-(?\'id\'\\d+)$!'],'homepage'=>'http://www.break.com/','host'=>'break.com','iframe'=>['height'=>280,'src'=>'//break.com/embed/{@id}','width'=>464],'name'=>'Break','scrape'=>[],'tags'=>['entertainment']],
'brightcove'=>['example'=>['http://link.brightcove.com/services/player/bcpid34762914001?bctid=66379363001','http://link.brightcove.com/services/player/bcpid3936710530001?bckey=AQ~~,AAAA3LlbZiE~,0uzoN5xJpHsvpxPw-K2_CalW5-PE-Ti2&bctid=4669818674001','http://bcove.me/rpuseykd'],'extract'=>[],'homepage'=>'https://www.brightcove.com/','host'=>['bcove.me','link.brightcove.com'],'iframe'=>['src'=>'https://link.brightcove.com/services/player/bcpid{@bcpid}?bckey={@bckey}&bctid={@bctid}&secureConnections=true&secureHTMLConnections=true&autoStart=false&height=100%25&width=100%25'],'name'=>'Brightcove','scrape'=>[['extract'=>['!meta name="twitter:player" content=".*?bcpid(?\'bcpid\'\\d+).*?bckey=(?\'bckey\'[-,~\\w]+).*?bctid=(?\'bctid\'\\d+)!'],'match'=>['!bcove\\.me/.!','!link\\.brightcove\\.com/services/player/!']]],'tags'=>['videos']],
'cbsnews'=>['attributes'=>['id'=>['required'=>\false]],'choose'=>['otherwise'=>['flash'=>['flashvars'=>'si=254&contentValue={@id}','padding-height'=>40,'src'=>'//i.i.cbsi.com/cnwk.1d/av/video/cbsnews/atlantis2/cbsnews_player_embed.swf']],'when'=>['flash'=>['flashvars'=>'pType=embed&si=254&pid={@pid}','padding-height'=>38,'src'=>'//www.cbsnews.com/common/video/cbsnews_player.swf'],'test'=>'@pid']],'example'=>['http://www.cbsnews.com/video/watch/?id=50156501n','http://www.cbsnews.com/videos/is-the-us-stock-market-rigged'],'extract'=>['#cbsnews\\.com/video/watch/\\?id=(?\'id\'\\d+)#'],'homepage'=>'http://www.cbsnews.com/video/','host'=>'cbsnews.com','name'=>'CBS News Video','scrape'=>[['extract'=>['#"pid":"(?\'pid\'\\w+)"#'],'match'=>['#cbsnews\\.com/videos/(?!watch/)#']]],'tags'=>['news']],
'cnbc'=>['example'=>'http://video.cnbc.com/gallery/?video=3000269279','extract'=>['!cnbc\\.com/gallery/\\?video=(?\'id\'\\d+)!'],'flash'=>['height'=>380,'src'=>'//plus.cnbc.com/rssvideosearch/action/player/id/{@id}/code/cnbcplayershare','width'=>400],'homepage'=>'http://www.cnbc.com/','host'=>'video.cnbc.com','name'=>'CNBC','scrape'=>[],'tags'=>['news']],
'cnn'=>['example'=>['http://edition.cnn.com/videos/tv/2015/06/09/airplane-yoga-rachel-crane-ts-orig.cnn','http://us.cnn.com/video/data/2.0/video/bestoftv/2013/10/23/vo-nr-prince-george-christening-arrival.cnn.html'],'extract'=>['!cnn.com/videos/(?\'id\'.*\\.cnn)!','!cnn\\.com/video/data/2\\.0/video/(?\'id\'.*\\.cnn)!'],'homepage'=>'http://edition.cnn.com/video/','host'=>'cnn.com','iframe'=>['src'=>'//edition.cnn.com/video/api/embed.html#/video/{@id}'],'name'=>'CNN','scrape'=>[],'tags'=>['news']],
'cnnmoney'=>['example'=>'http://money.cnn.com/video/technology/2014/05/20/t-twitch-vp-on-future.cnnmoney/','extract'=>['!money\\.cnn\\.com/video/(?\'id\'.*\\.cnnmoney)!'],'homepage'=>'http://money.cnn.com/video/','host'=>'money.cnn.com','iframe'=>['height'=>360,'src'=>'//money.cnn.com/.element/ssi/video/7.0/players/embed.player.html?videoid=video/{@id}&width=560&height=360','width'=>560],'name'=>'CNNMoney','scrape'=>[],'tags'=>['news']],
'collegehumor'=>['example'=>'http://www.collegehumor.com/video/1181601/more-than-friends','extract'=>['!collegehumor\\.com/(?:video|embed)/(?\'id\'\\d+)!'],'homepage'=>'http://www.collegehumor.com/','host'=>'collegehumor.com','iframe'=>['height'=>369,'src'=>'//www.collegehumor.com/e/{@id}','width'=>600],'name'=>'CollegeHumor','scrape'=>[],'tags'=>['entertainment']],
'comedycentral'=>['example'=>['http://www.cc.com/video-clips/uu5qz4/key-and-peele-dueling-hats','http://www.comedycentral.com/video-clips/uu5qz4/key-and-peele-dueling-hats','http://tosh.cc.com/video-clips/aet4lh/rc-car-crash'],'extract'=>[],'homepage'=>'http://www.comedycentral.com/funny-videos','host'=>['cc.com','comedycentral.com'],'iframe'=>['src'=>'//media.mtvnservices.com/embed/{@id}'],'name'=>'Comedy Central','scrape'=>[['extract'=>['!(?\'id\'mgid:arc:(?:episode|video):[.\\w]+:[-\\w]+)!'],'match'=>['!c(?:c|omedycentral)\\.com/(?:full-episode|video-clip)s/!']]],'tags'=>['entertainment']],
'coub'=>['example'=>'http://coub.com/view/6veusoty','extract'=>['!coub\\.com/view/(?\'id\'\\w+)!'],'homepage'=>'http://coub.com/','host'=>'coub.com','iframe'=>['src'=>'//coub.com/embed/{@id}'],'name'=>'Coub','scrape'=>[],'tags'=>['videos']],
'dailymotion'=>['example'=>['http://www.dailymotion.com/video/x222z1','http://www.dailymotion.com/user/Dailymotion/2#video=x222z1','http://games.dailymotion.com/live/x15gjhi'],'extract'=>['!dailymotion\\.com/(?:live/|swf/|user/[^#]+#video=|(?:related/\\d+/)?video/)(?\'id\'[A-Za-z0-9]+)!'],'homepage'=>'http://www.dailymotion.com/','host'=>'dailymotion.com','iframe'=>['src'=>'//www.dailymotion.com/embed/video/{@id}'],'name'=>'Dailymotion','oembed'=>['url'=>'http://www.dailymotion.com/services/oembed'],'scrape'=>[],'source'=>'http://www.dailymotion.com/doc/api/player.html','tags'=>['videos']],
'democracynow'=>['example'=>['http://www.democracynow.org/2014/7/2/dn_at_almedalen_week_at_swedens','http://www.democracynow.org/blog/2015/3/13/part_2_bruce_schneier_on_the','http://www.democracynow.org/shows/2006/2/20','http://www.democracynow.org/2015/5/21/headlines','http://m.democracynow.org/stories/15236'],'extract'=>['!democracynow.org/(?:embed/)?(?\'id\'(?:\\w+/)?\\d+/\\d+/\\d+(?:/\\w+)?)!'],'homepage'=>'http://www.democracynow.org/','host'=>'democracynow.org','iframe'=>['src'=>'//www.democracynow.org/embed/<xsl:choose><xsl:when test="contains(@id,\'/headlines\')">headlines/<xsl:value-of select="substring-before(@id,\'/headlines\')"/></xsl:when><xsl:when test="starts-with(@id,\'2\')">story/<xsl:value-of select="@id"/></xsl:when><xsl:when test="starts-with(@id,\'shows/\')">show/<xsl:value-of select="substring-after(@id,\'/\')"/></xsl:when><xsl:otherwise><xsl:value-of select="@id"/></xsl:otherwise></xsl:choose>'],'name'=>'Democracy Now!','scrape'=>[['extract'=>['!democracynow\\.org/(?\'id\'(?:\\w+/)?\\d+/\\d+/\\d+(?:/\\w+)?)\' rel=\'canonical!'],'match'=>['!m\\.democracynow\\.org/stories/\\d!']]]],
'dumpert'=>['example'=>'http://www.dumpert.nl/mediabase/6622577/4652b140/r_mi_gaillard_doet_halloween_prank.html','extract'=>['!dumpert\\.nl/mediabase/(?\'id\'\\d+[/_]\\w+)!'],'homepage'=>'http://www.dumpert.nl/','host'=>'dumpert.nl','iframe'=>['src'=>'//www.dumpert.nl/embed/{translate(@id,\'_\',\'/\')}/'],'name'=>'dumpert','scrape'=>[],'tags'=>['.nl','entertainment']],
'eighttracks'=>['example'=>['http://8tracks.com/lovinq/headphones-in-world-out','http://8tracks.com/lovinq/4982023'],'extract'=>['!8tracks\\.com/[-\\w]+/(?\'id\'\\d+)(?=#|$)!'],'homepage'=>'http://8tracks.com/','host'=>'8tracks.com','iframe'=>['height'=>400,'src'=>'//8tracks.com/mixes/{@id}/player_v3_universal','width'=>400],'name'=>'8tracks','scrape'=>[['extract'=>['!eighttracks://mix/(?\'id\'\\d+)!'],'match'=>['!8tracks\\.com/[-\\w]+/[-\\w]+!']]],'tags'=>['music']],
'espn'=>['example'=>['http://www.espn.com/video/clip?id=17474659','http://www.espn.com/espnw/video/13887284/kyrgios-angry-code-violation-almost-hitting-ref','http://broadband.espn.go.com/video/clip?id=17481969'],'extract'=>['#video/(?:clip(?:\\?id=|/_/id/))?(?\'id\'\\d+)#'],'homepage'=>'http://www.espn.com/','host'=>['espn.com','espn.go.com'],'iframe'=>['src'=>'//www.espn.com/core/video/iframe?id={@id}'],'name'=>'ESPN','scrape'=>[],'tags'=>['sports']],
'facebook'=>['example'=>['https://www.facebook.com/FacebookDevelopers/posts/10151471074398553','https://www.facebook.com/video/video.php?v=10150451523596807','https://www.facebook.com/photo.php?fbid=10152476416772631','https://www.facebook.com/ign/videos/10153762113196633/','https://www.facebook.com/southamptonfc/videos/vb.220396037973624/1357764664236750/'],'extract'=>['@/(?!(?:apps|developers|graph)\\.)[-\\w.]*facebook\\.com/(?:[/\\w]+/permalink|(?!pages/|groups/).*?)(?:/|fbid=|\\?v=)(?\'id\'\\d+)(?=$|[/?&#])@','#/(?\'type\'video)s?/#'],'homepage'=>'http://www.facebook.com/','host'=>'facebook.com','iframe'=>['data-s9e-livepreview-ignore-attrs'=>'style','onload'=>'var a=Math.random();window.addEventListener(\'message\',function(b){{if(b.data.id==a)style.height=b.data.height+\'px\'}});contentWindow.postMessage(\'s9e:\'+a,\'https://s9e.github.io\')','src'=>'https://s9e.github.io/iframe/facebook.min.html#{@type}{@id}'],'name'=>'Facebook','scrape'=>[],'tags'=>['social']],
'flickr'=>['example'=>'https://www.flickr.com/photos/8757881@N04/2971804544/lightbox/','extract'=>['!flickr\\.com/photos/[^/]+/(?\'id\'\\d+)!'],'homepage'=>'https://www.flickr.com/','host'=>'flickr.com','iframe'=>['height'=>500,'src'=>'https://www.flickr.com/photos/_/{@id}/player/','width'=>500],'name'=>'Flickr','scrape'=>[],'tags'=>['images']],
'foratv'=>['example'=>'http://fora.tv/2014/09/30/How_Google_Works','extract'=>[],'homepage'=>'http://fora.tv/','host'=>'fora.tv','iframe'=>['src'=>'//library.fora.tv/embed?id={@id}&type=c'],'name'=>'FORA.tv','scrape'=>[['extract'=>['!embed\\?id=(?\'id\'\\d+)!'],'match'=>['!fora\\.tv/\\d+/\\d+/\\d+/.!']]]],
'foxnews'=>['example'=>'http://video.foxnews.com/v/3592758613001/reddit-helps-fund-homemade-hot-sauce-venture/','extract'=>['!video\\.foxnews\\.com/v/(?\'id\'\\d+)!'],'homepage'=>'http://www.foxnews.com/','host'=>'video.foxnews.com','iframe'=>['src'=>'//video.foxnews.com/v/video-embed.html?video_id={@id}'],'name'=>'Fox News','scrape'=>[],'tags'=>['news']],
'funnyordie'=>['example'=>'http://www.funnyordie.com/videos/bf313bd8b4/murdock-with-keith-david','extract'=>['!funnyordie\\.com/videos/(?\'id\'[0-9a-f]+)!'],'homepage'=>'http://www.funnyordie.com/','host'=>'funnyordie.com','iframe'=>['src'=>'//www.funnyordie.com/embed/{@id}'],'name'=>'Funny or Die','scrape'=>[],'source'=>'http://support.funnyordie.com/discussions/problems/918-embed-user-videos-widget','tags'=>['entertainment']],
'gamespot'=>['example'=>['http://www.gamespot.com/destiny/videos/destiny-the-moon-trailer-6415176/','http://www.gamespot.com/events/game-crib-tsm-snapdragon/gamecrib-extras-cooking-with-dan-dinh-6412922/','http://www.gamespot.com/videos/beat-the-pros-pax-prime-2013/2300-6414307/'],'extract'=>['!gamespot\\.com.*?/(?:events|videos)/.*?-(?\'id\'\\d+)/(?:[#?].*)?$!'],'homepage'=>'http://www.gamespot.com/','host'=>'gamespot.com','iframe'=>['height'=>400,'src'=>'//www.gamespot.com/videos/embed/{@id}/','width'=>640],'name'=>'Gamespot','scrape'=>[],'tags'=>['gaming']],
'gametrailers'=>['example'=>'http://www.gametrailers.com/videos/view/pop-fiction/102300-Metal-Gear-Solid-3-Still-in-a-Dream','extract'=>[],'homepage'=>'http://www.gametrailers.com/','host'=>'gametrailers.com','iframe'=>['src'=>'//<xsl:choose><xsl:when test="starts-with(@id,\'mgid:\')">media.mtvnservices.com/embed/<xsl:value-of select="@id"/></xsl:when><xsl:otherwise>embed.gametrailers.com/embed/<xsl:value-of select="@id"/>?embed=1&amp;suppressBumper=1</xsl:otherwise></xsl:choose>'],'name'=>'GameTrailers','scrape'=>[['extract'=>['!embed/(?\'id\'\\d+)!'],'match'=>['!gametrailers\\.com/(?:full-episode|review|video)s/!']]],'tags'=>['gaming']],
'getty'=>['attributes'=>['height'=>['defaultValue'=>360],'width'=>['defaultValue'=>640]],'example'=>['http://gty.im/3232182','http://www.gettyimages.com/detail/3232182','http://www.gettyimages.com/detail/news-photo/the-beatles-travel-by-coach-to-the-west-country-for-some-news-photo/3232182','http://www.gettyimages.co.uk/detail/3232182'],'extract'=>['!gty\\.im/(?\'id\'\\d+)!','!gettyimages\\.[.\\w]+/detail(?=/).*?/(?\'id\'\\d+)!','!#[-\\w]*picture-id(?\'id\'\\d+)$!'],'homepage'=>'http://www.gettyimages.com/','host'=>['gettyimages.be','gettyimages.cn','gettyimages.co.jp','gettyimages.co.uk','gettyimages.com','gettyimages.com.au','gettyimages.de','gettyimages.dk','gettyimages.es','gettyimages.fr','gettyimages.ie','gettyimages.it','gettyimages.nl','gettyimages.pt','gettyimages.se','gty.im'],'iframe'=>['height'=>'{@height}','padding-height'=>49,'src'=>'//embed.gettyimages.com/embed/{@id}?et={@et}&sig={@sig}','width'=>'{@width}'],'name'=>'Getty Images','scrape'=>[['extract'=>['!"height":[ "]*(?\'height\'\\d+)!','!"width":[ "]*(?\'width\'\\d+)!','!\\bid[=:][\'"]?(?\'et\'[-=\\w]+)!','!\\bsig[=:][\'"]?(?\'sig\'[-=\\w]+)!'],'match'=>['//'],'url'=>'http://embed.gettyimages.com/preview/{@id}']],'tags'=>['images']],
'gfycat'=>['attributes'=>['height'=>['defaultValue'=>360],'width'=>['defaultValue'=>640]],'example'=>['http://gfycat.com/SereneIllfatedCapybara','http://giant.gfycat.com/SereneIllfatedCapybara.gif'],'extract'=>['!gfycat\\.com/(?:gifs/detail/)?(?\'id\'\\w+)!'],'homepage'=>'http://gfycat.com/','host'=>'gfycat.com','iframe'=>['height'=>'{@height}','src'=>'//gfycat.com/iframe/{@id}','width'=>'{@width}'],'name'=>'Gfycat','scrape'=>[['extract'=>['!<meta name="twitter:player:height" content="(?\'height\'\\d+)!','!video:height" content="(?\'height\'\\d+)!','!<meta name="twitter:player:width" content="(?\'width\'\\d+)!','!video:width" content="(?\'width\'\\d+)!'],'match'=>['//'],'url'=>'http://gfycat.com/iframe/{@id}']],'tags'=>['images']],
'gifs'=>['attributes'=>['height'=>['defaultValue'=>360],'width'=>['defaultValue'=>640]],'example'=>['https://gifs.com/gif/zm4DLy','https://j.gifs.com/Y6YZoO.gif'],'extract'=>['!gifs\\.com/(?:gif/)?(?\'id\'\\w+)!'],'homepage'=>'https://gifs.com/','host'=>'gifs.com','iframe'=>['height'=>'{@height}','src'=>'//gifs.com/embed/{@id}','width'=>'{@width}'],'name'=>'Gifs.com','scrape'=>[['extract'=>['!meta property="og:image:width" content="(?\'width\'\\d+)!','!meta property="og:image:height" content="(?\'height\'\\d+)!'],'match'=>['//'],'url'=>'https://gifs.com/gif/{@id}']],'tags'=>['images']],
'gist'=>['example'=>['https://gist.github.com/s9e/0ee8433f5a9a779d08ef','https://gist.github.com/6806305','https://gist.github.com/s9e/6806305/ad88d904b082c8211afa040162402015aacb8599'],'extract'=>['!gist\\.github\\.com/(?\'id\'(?:\\w+/)?[\\da-f]+(?:/[\\da-f]+)?)!'],'homepage'=>'https://gist.github.com/','host'=>'github.com','iframe'=>['data-s9e-livepreview-ignore-attrs'=>'style','height'=>180,'onload'=>'var a=Math.random();window.addEventListener(\'message\',function(b){{if(b.data.id==a)style.height=b.data.height+\'px\'}});contentWindow.postMessage(\'s9e:\'+a,\'https://s9e.github.io\')','scrolling'=>'','src'=>'https://s9e.github.io/iframe/gist.min.html#{@id}','width'=>'100%'],'name'=>'GitHub Gist (via custom iframe)','scrape'=>[]],
'globalnews'=>['example'=>'http://globalnews.ca/video/1647385/mark-channels-his-70s-look/','extract'=>['!globalnews\\.ca/video/(?\'id\'\\d+)!'],'homepage'=>'http://globalnews.ca/','host'=>'globalnews.ca','iframe'=>['height'=>377,'src'=>'//globalnews.ca/video/embed/{@id}/','width'=>560],'name'=>'Global News','scrape'=>[],'tags'=>['.ca','news']],
'gofundme'=>['example'=>'http://www.gofundme.com/2p37ao','extract'=>['@gofundme\\.com/(?\'id\'\\w+)(?![^#?])@'],'flash'=>['flashvars'=>'page={@id}','height'=>338,'src'=>'//funds.gofundme.com/Widgetflex.swf','width'=>258],'homepage'=>'http://www.gofundme.com/','host'=>'gofundme.com','name'=>'GoFundMe','scrape'=>[],'tags'=>['fundraising']],
'googledrive'=>['example'=>'https://drive.google.com/file/d/0B_4NRUjxLBejNjVmeG5MUzA3Q3M/view?usp=sharing','extract'=>['!drive\\.google\\.com/.*?(?:file/d/|id=)(?\'id\'[-\\w]+)!'],'homepage'=>'https://drive.google.com','host'=>'drive.google.com','iframe'=>['height'=>480,'src'=>'//drive.google.com/file/d/{@id}/preview','width'=>640],'name'=>'Google Drive','scrape'=>[],'tags'=>['documents','images','videos']],
'googleplus'=>['attributes'=>['name'=>['postFilter'=>'urldecode']],'example'=>['https://plus.google.com/+TonyHawk/posts/C5TMsDZJWBd','https://plus.google.com/106189723444098348646/posts/V8AojCoTzxV'],'extract'=>['!//plus\\.google\\.com/(?:u/\\d+/)?(?:\\+(?\'name\'[^/]+)|(?\'oid\'\\d+))/posts/(?\'pid\'\\w+)!'],'homepage'=>'https://plus.google.com/','host'=>'plus.google.com','iframe'=>['data-s9e-livepreview-ignore-attrs'=>'style','height'=>240,'onload'=>'var a=Math.random();window.addEventListener(\'message\',function(b){{if(b.data.id==a)style.height=b.data.height+\'px\'}});contentWindow.postMessage(\'s9e:\'+a,\'https://s9e.github.io\')','src'=>'https://s9e.github.io/iframe/googleplus.min.html#<xsl:choose><xsl:when test="@oid"><xsl:value-of select="@oid"/></xsl:when><xsl:otherwise>+<xsl:value-of select="@name"/></xsl:otherwise></xsl:choose>/posts/<xsl:value-of select="@pid"/>','width'=>450],'name'=>'Google+','scrape'=>[],'source'=>'https://developers.google.com/+/web/embedded-post/','tags'=>['social']],
'googlesheets'=>['example'=>'https://docs.google.com/spreadsheets/d/1f988o68HDvk335xXllJD16vxLBuRcmm3vg6U9lVaYpA','extract'=>['@docs\\.google\\.com/spreadsheet(?:/ccc\\?key=|s/d/)(?!e/)(?\'id\'[-\\w]+)[^#]*(?:#gid=(?\'gid\'\\d+))?@'],'homepage'=>'http://www.google.com/sheets/about/','host'=>'docs.google.com','iframe'=>['height'=>500,'src'=>'https://docs.google.com/spreadsheets/d/{@id}/pubhtml?widget=true&headers=false#gid={@gid}','style'=>['resize'=>'vertical'],'width'=>'100%'],'name'=>'Google Sheets','scrape'=>[],'tags'=>['documents']],
'healthguru'=>['example'=>'http://mental.healthguru.com/video/internet-addiction-signs-you-need-to-shut-down','extract'=>[],'homepage'=>'http://www.healthguru.com/','host'=>'healthguru.com','iframe'=>['src'=>'//www.healthguru.com/embed/{@id}'],'name'=>'Healthguru','scrape'=>[['extract'=>['!healthguru\\.com/embed/(?\'id\'\\w+)!'],'match'=>['!healthguru\\.com/(?:content/)?video/.!']]],'tags'=>['health']],
'hudl'=>['example'=>['http://www.hudl.com/athlete/2067184/highlights/163744377','http://www.hudl.com/v/CVmja','http://www.hudl.com/video/3/323679/57719969842eb243e47883f8'],'extract'=>['!hudl\\.com/athlete/(?\'athlete\'\\d+)/highlights/(?\'highlight\'[\\da-f]+)!','!hudl\\.com/video/\\d+/(?\'athlete\'\\d+)/(?\'highlight\'[\\da-f]+)!'],'homepage'=>'http://www.hudl.com/','host'=>'hudl.com','iframe'=>['src'=>'//www.hudl.com/embed/athlete/{@athlete}/highlights/{@highlight}'],'name'=>'Hudl','scrape'=>[['extract'=>['!hudl\\.com/video/\\d+/(?\'athlete\'\\d+)/(?\'highlight\'[\\da-f]+)!'],'match'=>['!hudl\\.com/v/!']]],'tags'=>['sports']],
'hulu'=>['example'=>'http://www.hulu.com/watch/484180','extract'=>[],'homepage'=>'http://www.hulu.com/','host'=>'hulu.com','iframe'=>['src'=>'https://secure.hulu.com/embed/{@id}'],'name'=>'Hulu','scrape'=>[['extract'=>['!eid=(?\'id\'[-\\w]+)!'],'match'=>['!hulu\\.com/watch/!']]]],
'humortvnl'=>['example'=>'http://humortv.vara.nl/pa.346135.denzel-washington-bij-graham-norton.html','extract'=>['!humortv\\.vara\\.nl/\\w+\\.(?\'id\'[-.\\w]+)\\.html!'],'homepage'=>'http://humortv.vara.nl/pg.2.pg-home.html','host'=>'humortv.vara.nl','iframe'=>['src'=>'//humortv.vara.nl/embed.{@id}.html'],'name'=>'HumorTV','scrape'=>[],'tags'=>['.nl','entertainment']],
'ign'=>['example'=>'http://www.ign.com/videos/2013/07/12/pokemon-x-version-pokemon-y-version-battle-trailer','extract'=>['!(?\'id\'https?://.*?ign\\.com/videos/.+)!i'],'homepage'=>'http://www.ign.com/videos/','host'=>'ign.com','iframe'=>['height'=>263,'src'=>'//widgets.ign.com/video/embed/content.html?url={@id}','width'=>468],'name'=>'IGN','scrape'=>[],'tags'=>['gaming']],
'imdb'=>['example'=>['http://www.imdb.com/video/imdb/vi2482677785/','http://www.imdb.com/title/tt2294629/videoplayer/vi2482677785'],'extract'=>['!imdb\\.com/[/\\w]+/vi(?\'id\'\\d+)!'],'homepage'=>'http://www.imdb.com/','host'=>'imdb.com','iframe'=>['src'=>'//www.imdb.com/video/imdb/vi{@id}/imdb/embed?autoplay=false&width=640'],'name'=>'IMDb','scrape'=>[],'tags'=>['movies']],
'imgur'=>['attributes'=>['type'=>['type'=>'alnum']],'example'=>['http://imgur.com/AsQ0K3P','http://imgur.com/a/9UGCL','http://imgur.com/gallery/9UGCL','http://i.imgur.com/u7Yo0Vy.gifv','http://i.imgur.com/UO1UrIx.mp4','https://imgur.com/t/current_events/0I30l'],'extract'=>['@imgur\\.com/(?!r/|user/)(?:gallery/|t/[^/]+/)?(?\'id\'(?:a/)?\\w+)(?!\\w|\\.(?:pn|jp)g)@'],'homepage'=>'http://imgur.com/','host'=>'imgur.com','iframe'=>['data-s9e-livepreview-ignore-attrs'=>'style','height'=>450,'onload'=>'var b=Math.random();window.addEventListener(\'message\',function(a){{a.data.id==b&&(style.height=a.data.height+\'px\',style.width=a.data.width+\'px\')}});contentWindow.postMessage(\'s9e:\'+b,\'https://s9e.github.io\')','src'=>'https://s9e.github.io/iframe/imgur.min.html#<xsl:if test="@type=\'album\'and not(starts-with(@id,\'a/\'))">a/</xsl:if><xsl:value-of select="@id"/>','width'=>568],'name'=>'Imgur','scrape'=>[['extract'=>['!image\\s*:\\s*.*?"is_(?\'type\'album)":true!','!<div id="(?\'type\'album)-!','!class="(?\'type\'album)-image!'],'match'=>['@imgur\\.com/(?:gallery/|t/[^/]+/)\\w@']]],'tags'=>['images']],
'indiegogo'=>['example'=>'http://www.indiegogo.com/projects/gameheart-redesigned','extract'=>['!indiegogo\\.com/projects/(?\'id\'[-\\w]+)!'],'homepage'=>'http://www.indiegogo.com/','host'=>'indiegogo.com','iframe'=>['height'=>445,'src'=>'//www.indiegogo.com/project/{@id}/embedded','width'=>222],'name'=>'Indiegogo','scrape'=>[],'tags'=>['fundraising']],
'instagram'=>['example'=>'http://instagram.com/p/gbGaIXBQbn/','extract'=>['!instagram\\.com/p/(?\'id\'[-\\w]+)!'],'homepage'=>'http://instagram.com/','host'=>'instagram.com','iframe'=>['data-s9e-livepreview-ignore-attrs'=>'style','height'=>640,'onload'=>'var a=Math.random();window.addEventListener(\'message\',function(b){{if(b.data.id==a)style.height=b.data.height+\'px\'}});contentWindow.postMessage(\'s9e:\'+a,\'https://s9e.github.io\')','src'=>'https://s9e.github.io/iframe/instagram.min.html#{@id}','width'=>640],'name'=>'Instagram','scrape'=>[],'source'=>'http://help.instagram.com/513918941996087','tags'=>['social']],
'internetarchive'=>['attributes'=>['height'=>['defaultValue'=>360],'width'=>['defaultValue'=>640]],'example'=>['https://archive.org/details/BillGate99','https://archive.org/details/DFTS2014-05-30'],'extract'=>[],'homepage'=>'https://archive.org/','host'=>'archive.org','iframe'=>['height'=>'{@height}','src'=>'https://archive.org/embed/{@id}','width'=>'{@width}'],'name'=>'Internet Archive','scrape'=>[['extract'=>['!meta property="twitter:player" content="https://archive.org/embed/(?\'id\'[^/"]+)!','!meta property="og:video:width" content="(?\'width\'\\d+)!','!meta property="og:video:height" content="(?\'height\'\\d+)!'],'match'=>['!archive\\.org/details/!']]]],
'izlesene'=>['example'=>'http://www.izlesene.com/video/lily-allen-url-badman/7600704','extract'=>['!izlesene\\.com/video/[-\\w]+/(?\'id\'\\d+)!'],'homepage'=>'http://www.izlesene.com/','host'=>'izlesene.com','iframe'=>['src'=>'//www.izlesene.com/embedplayer/{@id}'],'name'=>'İzlesene','scrape'=>[],'tags'=>['.tr']],
'jwplatform'=>['example'=>['http://content.jwplatform.com/players/X6tRZpKj-7Y21S9TB.html','http://content.jwplatform.com/previews/YfTSAegE-L0l35Tsd'],'extract'=>['!jwplatform\\.com/\\w+/(?\'id\'[-\\w]+)!'],'homepage'=>'https://www.jwplayer.com/products/jwplatform/','host'=>'jwplatform.com','iframe'=>['src'=>'//content.jwplatform.com/players/{@id}.html'],'name'=>'JW Platform','scrape'=>[],'tags'=>['videos']],
'khl'=>['example'=>['http://video.khl.ru/events/233677','http://video.khl.ru/quotes/251237'],'extract'=>[],'homepage'=>'http://www.khl.ru/','host'=>'video.khl.ru','iframe'=>['src'=>'//video.khl.ru/iframe/feed/start/{@id}?type_id=18&width=560&height=315'],'name'=>'Kontinental Hockey League (КХЛ)','scrape'=>[['extract'=>['!/feed/start/(?\'id\'[/\\w]+)!'],'match'=>['!video\\.khl\\.ru/(?:event|quote)s/\\d!']]],'tags'=>['.ru','sports']],
'kickstarter'=>['choose'=>['otherwise'=>['iframe'=>['height'=>420,'src'=>'//www.kickstarter.com/projects/{@id}/widget/card.html','width'=>220]],'when'=>['iframe'=>['height'=>360,'src'=>'//www.kickstarter.com/projects/{@id}/widget/video.html','width'=>480],'test'=>'@video']],'example'=>['http://www.kickstarter.com/projects/1869987317/wish-i-was-here-1','http://www.kickstarter.com/projects/1869987317/wish-i-was-here-1/widget/card.html','http://www.kickstarter.com/projects/1869987317/wish-i-was-here-1/widget/video.html'],'extract'=>['!kickstarter\\.com/projects/(?\'id\'[^/]+/[^/?]+)(?:/widget/(?:(?\'card\'card)|(?\'video\'video)))?!'],'homepage'=>'http://www.kickstarter.com/','host'=>'kickstarter.com','name'=>'Kickstarter','scrape'=>[],'tags'=>['fundraising']],
'kissvideo'=>['example'=>'http://www.kissvideo.click/alton-towers-smiler-rollercoaster-crash_7789d8de8.html','extract'=>['!kissvideo\\.click/[^_]*_(?\'id\'[0-9a-f]+)!'],'homepage'=>'http://www.kissvideo.click/index.html','host'=>'kissvideo.click','iframe'=>['src'=>'//www.kissvideo.click/embed.php?vid={@id}'],'name'=>'Kiss Video','scrape'=>[],'tags'=>['videos']],
'libsyn'=>['example'=>'http://bunkerbuddies.libsyn.com/interstellar-w-brandie-posey','extract'=>[],'homepage'=>'http://www.libsyn.com/','host'=>'libsyn.com','iframe'=>['height'=>45,'max-width'=>900,'src'=>'//html5-player.libsyn.com/embed/episode/id/{@id}/height/45/width/900/theme/standard/direction/no/autoplay/no/autonext/no/thumbnail/no/preload/no/no_addthis/no/','width'=>'100%'],'name'=>'Libsyn','scrape'=>[['extract'=>['!embed/episode/id/(?\'id\'\\d+)!'],'match'=>['@(?!\\.mp3)....$@']]],'tags'=>['podcasts']],
'livecap'=>['example'=>['https://www.livecap.tv/s/esl_sc2/uZoEz6RR1eA','https://www.livecap.tv/t/riotgames/uLxUzBTBs7u'],'extract'=>['!livecap.tv/[st]/(?\'channel\'\\w+)/(?\'id\'\\w+)!'],'homepage'=>'https://www.livecap.tv/','host'=>'livecap.tv','iframe'=>['src'=>'https://www.livecap.tv/s/embed/{@channel}/{@id}'],'name'=>'LiveCap','scrape'=>[],'tags'=>['gaming']],
'liveleak'=>['example'=>'http://www.liveleak.com/view?i=3dd_1366238099','extract'=>['!liveleak\\.com/view\\?i=(?\'id\'[a-f_0-9]+)!'],'homepage'=>'http://www.liveleak.com/','host'=>'liveleak.com','iframe'=>['src'=>'//www.liveleak.com/ll_embed?i={@id}'],'name'=>'LiveLeak','scrape'=>[],'tags'=>['videos']],
'livestream'=>['example'=>['http://new.livestream.com/jbtvlive/musicmarathon','http://livestream.com/ccscsl/USChessChampionships/videos/83267610','http://livestre.am/58XNV'],'extract'=>['!livestream\\.com/accounts/(?\'account_id\'\\d+)/events/(?\'event_id\'\\d+)!','!/videos/(?\'video_id\'\\d+)!','!original\\.livestream\\.com/(?\'channel\'\\w+)/video\\?clipId=(?\'clip_id\'[-\\w]+)!'],'homepage'=>'http://new.livestream.com/','host'=>['livestre.am','livestream.com'],'iframe'=>['src'=>'//<xsl:choose><xsl:when test="@clip_id">cdn.livestream.com/embed/<xsl:value-of select="@channel"/>?layout=4&amp;autoplay=false&amp;clip=<xsl:value-of select="@clip_id"/></xsl:when><xsl:otherwise>livestream.com/accounts/<xsl:value-of select="@account_id"/>/events/<xsl:value-of select="@event_id"/><xsl:if test="@video_id">/videos/<xsl:value-of select="@video_id"/></xsl:if>/player?autoPlay=false</xsl:otherwise></xsl:choose>'],'name'=>'Livestream','scrape'=>[['extract'=>['!accounts/(?\'account_id\'\\d+)/events/(?\'event_id\'\\d+)!'],'match'=>['//']],['extract'=>['!//original\\.livestream\\.com/(?\'channel\'\\w+)/video/(?\'clip_id\'[-\\w]+)!'],'match'=>['!livestre.am!']]],'tags'=>['livestreaming','videos']],
'mailru'=>['example'=>['https://my.mail.ru/corp/auto/video/testdrive/34.html','https://my.mail.ru/mail/alenka1957/video/1/7.html'],'extract'=>[],'homepage'=>'http://my.mail.ru/','host'=>'my.mail.ru','iframe'=>['src'=>'https://my.mail.ru/video/embed/{@id}'],'name'=>'Mail.Ru','scrape'=>[['extract'=>['!"itemId": ?"?(?\'id\'\\d+)!'],'match'=>['!my\\.mail\\.ru/\\w+/\\w+/video/\\w+/\\d!']]],'tags'=>['.ru']],
'medium'=>['example'=>'https://medium.com/@donnydonny/team-internet-is-about-to-win-net-neutrality-and-they-didnt-need-googles-help-e7e2cf9b8a95','extract'=>['!medium\\.com/[^/]*/(?:[-\\w]+-)?(?\'id\'[\\da-f]+)!'],'homepage'=>'https://medium.com/','host'=>'medium.com','iframe'=>['data-s9e-livepreview-ignore-attrs'=>'style','height'=>400,'onload'=>'window.addEventListener(\'message\',function(a){{a=a.data.split(\'::\');\'m\'===a[0]&&0<src.indexOf(a[1])&&a[2]&&(style.height=a[2]+\'px\')}})','src'=>'https://api.medium.com/embed?type=story&path=%2F%2F{@id}&id={translate(@id,\'abcdef\',\'111111\')}','style'=>['border'=>'1px solid','border-color'=>'#eee #ddd #bbb','border-radius'=>'5px','box-shadow'=>'rgba(0,0,0,.15) 0 1px 3px'],'width'=>400],'name'=>'Medium','scrape'=>[],'tags'=>['blogging']],
'metacafe'=>['example'=>'http://www.metacafe.com/watch/10785282/chocolate_treasure_chest_epic_meal_time/','extract'=>['!metacafe\\.com/watch/(?\'id\'\\d+)!'],'homepage'=>'http://www.metacafe.com/','host'=>'metacafe.com','iframe'=>['src'=>'//www.metacafe.com/embed/{@id}/'],'name'=>'Metacafe','scrape'=>[],'tags'=>['videos']],
'mixcloud'=>['example'=>'http://www.mixcloud.com/OneTakeTapes/timsch-one-take-tapes-2/','extract'=>['@mixcloud\\.com/(?!categories|tag)(?\'id\'[-\\w]+/[^/&]+)/@'],'homepage'=>'http://www.mixcloud.com/','host'=>'mixcloud.com','iframe'=>['height'=>400,'src'=>'//www.mixcloud.com/widget/iframe/?feed=http%3A%2F%2Fwww.mixcloud.com%2F{@id}%2F&embed_type=widget_standard','width'=>400],'name'=>'Mixcloud','scrape'=>[],'tags'=>['music']],
'mlb'=>['example'=>['http://m.mlb.com/video/v1205791883','http://m.mlb.com/video/topic/39049312/v1211631483/cubs-celebrate-world-series-with-historic-parade'],'extract'=>['#mlb\\.com/[\\w/]*/v(?\'id\'\\d+)#','#mlb\\.com/r/video\\?content_id=(?\'id\'\\d+)#'],'homepage'=>'http://mlb.com/video/','host'=>'mlb.com','iframe'=>['src'=>'//m.mlb.com/shared/video/embed/embed.html?content_id={@id}&width=640&height=360'],'name'=>'MLB','scrape'=>[],'tags'=>['sports']],
'mrctv'=>['example'=>'http://dev.mrctv.org/videos/cnn-frets-about-tobacco-companies-color-coding-tricks','extract'=>[],'homepage'=>'http://www.mrctv.org/','host'=>'mrctv.org','iframe'=>['src'=>'https://www.mrctv.org/embed/{@id}'],'name'=>'MRCTV','scrape'=>[['extract'=>['!mrctv\\.org/embed/(?\'id\'\\d+)!'],'match'=>['!mrctv\\.org/videos/.!']]]],
'msnbc'=>['example'=>['http://www.msnbc.com/ronan-farrow-daily/watch/thats-no-moon--300512323725','http://on.msnbc.com/1qkH62o'],'extract'=>[],'homepage'=>'http://www.msnbc.com/watch','host'=>'msnbc.com','iframe'=>['height'=>440,'src'=>'//player.theplatform.com/p/2E2eJC/EmbeddedOffSite?guid={@id}','width'=>635],'name'=>'MSNBC','scrape'=>[['extract'=>['@property="nv:videoId" content="(?\'id\'\\w+)@','@guid"?[=:]"?(?\'id\'\\w+)@'],'match'=>['@msnbc\\.com/[-\\w]+/watch/@','@on\\.msnbc\\.com/.@']]],'tags'=>['news']],
'natgeochannel'=>['example'=>['http://channel.nationalgeographic.com/channel/brain-games/videos/jason-silva-on-intuition/','http://channel.nationalgeographic.com/wild/urban-jungle/videos/leopard-in-the-city/'],'extract'=>['@channel\\.nationalgeographic\\.com/(?\'id\'[-/\\w]+/videos/[-\\w]+)@'],'homepage'=>'http://channel.nationalgeographic.com/','host'=>'channel.nationalgeographic.com','iframe'=>['src'=>'//channel.nationalgeographic.com/{@id}/embed/'],'name'=>'National Geographic Channel','scrape'=>[]],
'natgeovideo'=>['example'=>['http://video.nationalgeographic.com/tv/changing-earth','http://video.nationalgeographic.com/video/weirdest-superb-lyrebird'],'extract'=>[],'homepage'=>'http://video.nationalgeographic.com/','host'=>'video.nationalgeographic.com','iframe'=>['src'=>'//player.d.nationalgeographic.com/players/ngsvideo/share/?guid={@id}'],'name'=>'National Geographic Video','scrape'=>[['extract'=>['@guid="(?\'id\'[-\\w]+)"@'],'match'=>['@video\\.nationalgeographic\\.com/(?:tv|video)/\\w@']]],'tags'=>['documentaries']],
'nbcnews'=>['example'=>'http://www.nbcnews.com/video/bob-dylan-awarded-nobel-prize-for-literature-785193027834','extract'=>['!nbcnews\\.com/(?:widget/video-embed/|video/[-\\w]+?-)(?\'id\'\\d+)!'],'homepage'=>'http://www.nbcnews.com/video/','host'=>'nbcnews.com','iframe'=>['src'=>'//www.nbcnews.com/widget/video-embed/{@id}'],'name'=>'NBC News','scrape'=>[],'tags'=>['news']],
'nbcsports'=>['example'=>'http://www.nbcsports.com/video/countdown-rio-olympics-what-makes-perfect-performance','extract'=>[],'homepage'=>'http://www.nbcsports.com/video','host'=>'nbcsports.com','iframe'=>['src'=>'//vplayer.nbcsports.com/p/BxmELC/nbcsports_embed/select/media/{@id}?parentUrl='],'name'=>'NBC Sports','scrape'=>[['extract'=>['!select/media/(?\'id\'\\w+)!'],'match'=>['!nbcsports\\.com/video/.!']]],'tags'=>['sports']],
'nhl'=>['example'=>'https://www.nhl.com/video/recap-min-2-ott-1-fot/t-277753022/c-46330703','extract'=>['#nhl\\.com/(?:\\w+/)?video(?:/(?![ct]-)[-\\w]+)?(?:/t-(?\'t\'\\d+))?(?:/c-(?\'c\'\\d+))?#'],'homepage'=>'https://www.nhl.com/video','host'=>'nhl.com','iframe'=>['src'=>'https://www.nhl.com/video/embed<xsl:if test="@t">/t-<xsl:value-of select="@t"/></xsl:if><xsl:if test="@c">/c-<xsl:value-of select="@c"/></xsl:if>?autostart=false'],'name'=>'NHL Videos and Highlights','scrape'=>[],'tags'=>['sports']],
'npr'=>['example'=>['http://www.npr.org/blogs/goatsandsoda/2015/02/11/385396431/the-50-most-effective-ways-to-transform-the-developing-world','http://n.pr/1Qky1m5'],'extract'=>[],'homepage'=>'http://www.npr.org/','host'=>['npr.org','n.pr'],'iframe'=>['height'=>228,'max-width'=>800,'src'=>'//www.npr.org/player/embed/{@i}/{@m}','width'=>'100%'],'name'=>'NPR','scrape'=>[['extract'=>['!player/embed/(?\'i\'\\d+)/(?\'m\'\\d+)!'],'match'=>['!npr\\.org/[/\\w]+/\\d+!','!n\\.pr/\\w!']]],'tags'=>['podcasts']],
'nytimes'=>['example'=>['http://www.nytimes.com/video/magazine/100000003166834/small-plates.html','http://www.nytimes.com/video/technology/personaltech/100000002907606/soylent-taste-test.html','http://www.nytimes.com/video/2012/12/17/business/100000001950744/how-wal-mart-conquered-teotihuacan.html','http://movies.nytimes.com/movie/131154/Crooklyn/trailers'],'extract'=>['!nytimes\\.com/video/[a-z]+/(?:[a-z]+/)?(?\'id\'\\d+)!','!nytimes\\.com/video/\\d+/\\d+/\\d+/[a-z]+/(?\'id\'\\d+)!'],'homepage'=>'http://www.nytimes.com/video/','host'=>'nytimes.com','iframe'=>['height'=>400,'src'=>'//graphics8.nytimes.com/video/players/offsite/index.html?videoId={@id}','width'=>585],'name'=>'The New York Times Video','scrape'=>[['extract'=>['!/video/movies/(?\'id\'\\d+)!'],'match'=>['!nytimes\\.com/movie(?:s/movie)?/(?\'playlist\'\\d+)/[-\\w]+/trailers!'],'url'=>'http://www.nytimes.com/svc/video/api/playlist/{@playlist}?externalId=true']],'tags'=>['movies','news']],
'oddshot'=>['example'=>'https://oddshot.tv/s/-MrDaG','extract'=>['!oddshot.tv/s/(?\'id\'[-\\w]+)!'],'homepage'=>'http://oddshot.tv/','host'=>'oddshot.tv','iframe'=>['src'=>'https://oddshot.tv/s/{@id}/embed'],'name'=>'Oddshot','scrape'=>[],'tags'=>['gaming']],
'orfium'=>['example'=>['https://www.orfium.com/album/24371/everybody-loves-kanye-totom/','https://www.orfium.com/live-set/614763/foof-no-lights-5-foof/','https://www.orfium.com/playlist/511651/electronic-live-sessions-creamtronic/','https://www.orfium.com/track/625367/the-ambience-of-the-goss-vistas/'],'extract'=>['@album/(?\'album_id\'\\d+)@','@playlist/(?\'playlist_id\'\\d+)@','@live-set/(?\'set_id\'\\d+)@','@track/(?\'track_id\'\\d+)@'],'homepage'=>'https://www.orfium.com/','host'=>'orfium.com','iframe'=>['height'=>'<xsl:choose><xsl:when test="@album_id">550</xsl:when><xsl:otherwise>275</xsl:otherwise></xsl:choose>','max-width'=>900,'src'=>'https://www.orfium.com/embedded/<xsl:choose><xsl:when test="@album_id">album/<xsl:value-of select="@album_id"/></xsl:when><xsl:when test="@playlist_id">playlist/<xsl:value-of select="@playlist_id"/></xsl:when><xsl:when test="@set_id">live-set/<xsl:value-of select="@set_id"/></xsl:when><xsl:otherwise>track/<xsl:value-of select="@track_id"/></xsl:otherwise></xsl:choose>','width'=>'100%'],'name'=>'Orfium','scrape'=>[],'tags'=>['music']],
'pastebin'=>['example'=>'http://pastebin.com/9jEf44nc','extract'=>['@pastebin\\.com/(?!u/)(?:\\w+(?:\\.php\\?i=|/))?(?\'id\'\\w+)@'],'homepage'=>'http://pastebin.com/','host'=>'pastebin.com','iframe'=>['height'=>300,'scrolling'=>'','src'=>'//pastebin.com/embed_iframe.php?i={@id}','style'=>['resize'=>'vertical'],'width'=>'100%'],'name'=>'Pastebin','scrape'=>[]],
'pinterest'=>['attributes'=>['id'=>['regexp'=>'@^(?:\\d+|[-\\w]+/[-\\w]+)$@']],'example'=>['https://www.pinterest.com/pin/99360735500167749/','https://www.pinterest.com/pinterest/official-news/'],'extract'=>['@pinterest.com/pin/(?\'id\'\\d+)@','@pinterest.com/(?!_/|discover/|explore/|news_hub/|pin/|search/)(?\'id\'[-\\w]+/[-\\w]+)@'],'homepage'=>'https://www.pinterest.com/','host'=>'pinterest.com','iframe'=>['data-s9e-livepreview-ignore-attrs'=>'style','height'=>360,'onload'=>'var a=Math.random();window.addEventListener(\'message\',function(b){{if(b.data.id==a)style.height=b.data.height+\'px\'}});contentWindow.postMessage(\'s9e:\'+a,\'https://s9e.github.io\')','src'=>'https://s9e.github.io/iframe/pinterest.min.html#{@id}','width'=>'<xsl:choose><xsl:when test="contains(@id,\'/\')">730</xsl:when><xsl:otherwise>345</xsl:otherwise></xsl:choose>'],'name'=>'Pinterest','scrape'=>[],'source'=>'https://developers.pinterest.com/tools/widget-builder/','tags'=>['social']],
'playstv'=>['example'=>['http://plays.tv/s/Kt4onQhyyVyz','http://plays.tv/video/565683db95f139f47e/full-length-version-radeon-software-crimson-edition-is-amds-revolutionary-new-graphics-software-that'],'extract'=>['!plays\\.tv/video/(?\'id\'\\w+)!'],'homepage'=>'http://plays.tv/','host'=>'plays.tv','iframe'=>['src'=>'//plays.tv/embeds/{@id}'],'name'=>'Plays.tv','scrape'=>[['extract'=>['!plays\\.tv/video/(?\'id\'\\w+)!'],'match'=>['!plays\\.tv/s/!']]],'tags'=>['gaming']],
'podbean'=>['example'=>['http://dialhforheroclix.podbean.com/e/dial-h-for-heroclix-episode-46-all-ya-need-is-love/','http://www.podbean.com/media/share/pb-qtwub-4ee10c'],'extract'=>['!podbean\\.com/media/(?:player/|share/pb-)(?\'id\'[-\\w]+)!'],'homepage'=>'http://www.podbean.com/','host'=>'podbean.com','iframe'=>['height'=>100,'max-width'=>900,'src'=>'//www.podbean.com/media/player/{@id}','width'=>'100%'],'name'=>'Podbean','scrape'=>[['extract'=>['!podbean\\.com/media/player/(?\'id\'[-\\w]+)!'],'match'=>['!podbean\\.com/(?:media/shar)?e/!']]],'tags'=>['podcasts']],
'prezi'=>['example'=>'http://prezi.com/5ye8po_hmikp/10-most-common-rookie-presentation-mistakes/','extract'=>['#//prezi\\.com/(?!(?:a(?:bout|mbassadors)|c(?:o(?:llaborate|mmunity|ntact)|reate)|exp(?:erts|lore)|ip(?:ad|hone)|jobs|l(?:ear|ogi)n|m(?:ac|obility)|pr(?:es(?:s|ent)|icing)|recommend|support|user|windows|your)/)(?\'id\'\\w+)/#'],'homepage'=>'http://prezi.com/','host'=>'prezi.com','iframe'=>['height'=>400,'src'=>'//prezi.com/embed/{@id}/','width'=>550],'name'=>'Prezi','scrape'=>[],'tags'=>['presentations']],
'reddit'=>['example'=>['http://www.reddit.com/r/pics/comments/304rms/cats_reaction_to_seeing_the_ceiling_fan_move_for/','http://www.reddit.com/r/pics/comments/304rms/cats_reaction_to_seeing_the_ceiling_fan_move_for/cpp2kkl'],'extract'=>['!(?\'path\'/r/\\w+/comments/\\w+/(?:\\w+/\\w+)?)!'],'homepage'=>'http://www.reddit.com/','host'=>'reddit.com','iframe'=>['data-s9e-livepreview-ignore-attrs'=>'style','height'=>165,'onload'=>'var a=Math.random();window.addEventListener(\'message\',function(b){{if(b.data.id==a)style.height=b.data.height+\'px\'}});contentWindow.postMessage(\'s9e:\'+a,\'https://s9e.github.io\')','src'=>'https://s9e.github.io/iframe/reddit.min.html#{@path}','width'=>800],'name'=>'Reddit threads and comments','scrape'=>[],'source'=>'https://www.reddit.com/wiki/embeds','tags'=>['social']],
'rutube'=>['example'=>['http://rutube.ru/video/b920dc58f1397f1761a226baae4d2f3b/','http://rutube.ru/tracks/4118278.html?v=8b490a46447720d4ad74616f5de2affd'],'extract'=>['!rutube\\.ru/tracks/(?\'id\'\\d+)!'],'homepage'=>'http://rutube.ru/','host'=>'rutube.ru','iframe'=>['height'=>405,'src'=>'//rutube.ru/play/embed/{@id}','width'=>720],'name'=>'Rutube','scrape'=>[['extract'=>['!rutube\\.ru/play/embed/(?\'id\'\\d+)!'],'match'=>['!rutube\\.ru/video/[0-9a-f]{32}!']]],'tags'=>['.ru']],
'scribd'=>['example'=>'http://www.scribd.com/doc/237147661/Calculus-2-Test-1-Review?in_collection=5291376','extract'=>['!scribd\\.com/(?:mobile/)?doc(?:ument)?/(?\'id\'\\d+)!'],'homepage'=>'http://www.scribd.com/','host'=>'scribd.com','iframe'=>['height'=>500,'src'=>'https://www.scribd.com/embeds/{@id}/content?view_mode=scroll&show_recommendations=false','style'=>['resize'=>'vertical'],'width'=>'100%'],'name'=>'Scribd','scrape'=>[],'tags'=>['documents','presentations']],
'slideshare'=>['example'=>'http://www.slideshare.net/Slideshare/how-23431564','extract'=>['!slideshare\\.net/[^/]+/[-\\w]+-(?\'id\'\\d{6,})$!'],'homepage'=>'http://www.slideshare.net/','host'=>'slideshare.net','iframe'=>['height'=>356,'src'=>'//www.slideshare.net/slideshow/embed_code/{@id}','width'=>427],'name'=>'SlideShare','scrape'=>[['extract'=>['!"presentationId":(?\'id\'\\d+)!'],'match'=>['!slideshare\\.net/[^/]+/\\w!']]],'source'=>'http://help.slideshare.com/forums/67665-Embedding-Sharing','tags'=>['presentations']],
'soundcloud'=>['example'=>['http://api.soundcloud.com/tracks/98282116','https://soundcloud.com/andrewbird/three-white-horses','https://soundcloud.com/tenaciousd/sets/rize-of-the-fenix/'],'extract'=>['@https?://(?:api\\.)?soundcloud\\.com/(?!pages/)(?\'id\'[-/\\w]+/[-/\\w]+|^[^/]+/[^/]+$)@i','@api\\.soundcloud\\.com/playlists/(?\'playlist_id\'\\d+)@','@api\\.soundcloud\\.com/tracks/(?\'track_id\'\\d+)(?:\\?secret_token=(?\'secret_token\'[-\\w]+))?@','@soundcloud\\.com/(?!playlists|tracks)[-\\w]+/[-\\w]+/(?=s-)(?\'secret_token\'[-\\w]+)@'],'homepage'=>'https://soundcloud.com/','host'=>'soundcloud.com','iframe'=>['height'=>'<xsl:choose><xsl:when test="@playlist_id or contains(@id,\'/sets/\')">450</xsl:when><xsl:otherwise>166</xsl:otherwise></xsl:choose>','max-width'=>900,'src'=>'https://w.soundcloud.com/player/?url=<xsl:choose><xsl:when test="@playlist_id">https%3A//api.soundcloud.com/playlists/<xsl:value-of select="@playlist_id"/></xsl:when><xsl:when test="@track_id">https%3A//api.soundcloud.com/tracks/<xsl:value-of select="@track_id"/>&amp;secret_token=<xsl:value-of select="@secret_token"/></xsl:when><xsl:otherwise><xsl:if test="not(contains(@id,\'://\'))">https%3A//soundcloud.com/</xsl:if><xsl:value-of select="@id"/></xsl:otherwise></xsl:choose>','width'=>'100%'],'name'=>'SoundCloud','scrape'=>[['extract'=>['@soundcloud:tracks:(?\'track_id\'\\d+)@'],'match'=>['@soundcloud\\.com/(?!playlists/\\d|tracks/\\d)[-\\w]+/[-\\w]@']],['extract'=>['@soundcloud://playlists:(?\'playlist_id\'\\d+)@'],'match'=>['@soundcloud\\.com/\\w+/sets/@']]],'source'=>'https://soundcloud.com/pages/widgets','tags'=>['music']],
'sportsnet'=>['example'=>'http://www.sportsnet.ca/soccer/west-ham-2-hull-2/','extract'=>[],'homepage'=>'http://www.sportsnet.ca/','host'=>'sportsnet.ca','iframe'=>['src'=>'https://images.rogersdigitalmedia.com/video_service.php?videoId={@id}&playerKey=AQ~~,AAAAAGWRwLc~,cRCmKE8Utf7OFWP38XQcokFZ80fR-u_y&autoStart=false&width=100%25&height=100%25'],'name'=>'Sportsnet','scrape'=>[['extract'=>['/vid(?:eoId)?=(?\'id\'\\d+)/','/param name="@videoPlayer" value="(?\'id\'\\d+)"/'],'match'=>['//']]],'tags'=>['.ca','sports']],
'spotify'=>['example'=>['spotify:track:5JunxkcjfCYcY7xJ29tLai','spotify:trackset:PREFEREDTITLE:5Z7ygHQo02SUrFmcgpwsKW,1x6ACsKV4UdWS2FMuPFUiT,4bi73jCM02fMpkI11Lqmfe','http://open.spotify.com/user/ozmoetr/playlist/4yRrCWNhWOqWZx5lmFqZvt','https://play.spotify.com/album/5OSzFvFAYuRh93WDNCTLEz'],'extract'=>['!(?\'uri\'spotify:(?:album|artist|user|track(?:set)?):[-,:\\w]+)!','!(?:open|play)\\.spotify\\.com/(?\'path\'(?:album|artist|track|user)/[-/\\w]+)!'],'homepage'=>'https://www.spotify.com/','host'=>['open.spotify.com','play.spotify.com'],'iframe'=>['height'=>480,'src'=>'https://embed.spotify.com/?view=coverart&amp;uri=<xsl:choose><xsl:when test="@uri"><xsl:value-of select="@uri"/></xsl:when><xsl:otherwise>spotify:<xsl:value-of select="translate(@path,\'/\',\':\')"/></xsl:otherwise></xsl:choose>','width'=>400],'name'=>'Spotify','scheme'=>'spotify','scrape'=>[],'source'=>['https://developer.spotify.com/technologies/widgets/spotify-play-button/','http://news.spotify.com/2008/01/14/linking-to-spotify/'],'tags'=>['music']],
'steamstore'=>['example'=>'http://store.steampowered.com/app/250520/','extract'=>['!store.steampowered.com/app/(?\'id\'\\d+)!'],'homepage'=>'http://store.steampowered.com/','host'=>'store.steampowered.com','iframe'=>['height'=>190,'max-width'=>900,'src'=>'//store.steampowered.com/widget/{@id}','width'=>'100%'],'name'=>'Steam store','scrape'=>[],'tags'=>['gaming']],
'stitcher'=>['example'=>'http://www.stitcher.com/podcast/twit/tech-news-today/e/twitter-shares-fall-18-percent-after-earnings-leak-on-twitter-37808629','extract'=>[],'homepage'=>'http://www.stitcher.com/','host'=>'stitcher.com','iframe'=>['height'=>150,'max-width'=>900,'src'=>'//app.stitcher.com/splayer/f/{@fid}/{@eid}','width'=>'100%'],'name'=>'Stitcher','scrape'=>[['extract'=>['!data-eid="(?\'eid\'\\d+)!','!data-fid="(?\'fid\'\\d+)!'],'match'=>['!/podcast/!']]],'tags'=>['podcasts']],
'strawpoll'=>['example'=>'http://strawpoll.me/738091','extract'=>['!strawpoll\\.me/(?\'id\'\\d+)!'],'homepage'=>'http://strawpoll.me/','host'=>'strawpoll.me','iframe'=>['scrolling'=>'','src'=>'//www.strawpoll.me/embed_1/{@id}'],'name'=>'Straw Poll','scrape'=>[]],
'streamable'=>['example'=>'http://streamable.com/e4d','extract'=>['!streamable\\.com/(?\'id\'\\w+)!'],'homepage'=>'http://streamable.com/','host'=>'streamable.com','iframe'=>['src'=>'//streamable.com/e/{@id}'],'name'=>'Streamable','scrape'=>[],'tags'=>['videos']],
'teamcoco'=>['example'=>['http://teamcoco.com/video/serious-jibber-jabber-a-scott-berg-full-episode','http://teamcoco.com/video/73784/historian-a-scott-berg-serious-jibber-jabber-with-conan-obrien'],'extract'=>['!teamcoco\\.com/video/(?\'id\'\\d+)!'],'homepage'=>'http://teamcoco.com/','host'=>'teamcoco.com','iframe'=>['height'=>415,'src'=>'//teamcoco.com/embed/v/{@id}','width'=>640],'name'=>'Team Coco','scrape'=>[['extract'=>['!"id":(?\'id\'\\d+)!'],'match'=>['!teamcoco\\.com/video/.!']]],'tags'=>['entertainment']],
'ted'=>['example'=>['http://www.ted.com/talks/eli_pariser_beware_online_filter_bubbles.html','http://embed.ted.com/playlists/26/our_digital_lives.html'],'extract'=>['#ted\\.com/(?\'id\'(?:talk|playlist)s/[-\\w]+(?:\\.html)?)(?![-\\w]|/transcript)#i'],'homepage'=>'http://www.ted.com/','host'=>'ted.com','iframe'=>['src'=>'//embed.ted.com/<xsl:value-of select="@id"/><xsl:if test="not(contains(@id,\'.html\'))">.html</xsl:if>'],'name'=>'TED Talks','scrape'=>[],'source'=>'http://blog.ted.com/2011/04/01/now-you-can-embed-tedtalks-with-subtitles-enabled/','tags'=>['presentations']],
'theatlantic'=>['example'=>'http://www.theatlantic.com/video/index/358928/computer-vision-syndrome-and-you/','extract'=>['!theatlantic\\.com/video/index/(?\'id\'\\d+)!'],'homepage'=>'http://www.theatlantic.com/video/','host'=>'theatlantic.com','iframe'=>['src'=>'//www.theatlantic.com/video/iframe/{@id}/'],'name'=>'The Atlantic Video','scrape'=>[],'tags'=>['news']],
'theguardian'=>['example'=>'http://www.theguardian.com/world/video/2016/apr/07/tokyos-hedgehog-cafe-encourages-you-to-embrace-prickly-pets-video','extract'=>['!theguardian\\.com/(?\'id\'\\w+/video/[-/\\w]+)!'],'homepage'=>'http://www.theguardian.com/video','host'=>'theguardian.com','iframe'=>['src'=>'//embed.theguardian.com/embed/video/{@id}'],'name'=>'The Guardian','scrape'=>[],'tags'=>['news']],
'theonion'=>['example'=>['http://www.theonion.com/video/nation-successfully-completes-mothers-day-by-918-a,35998/','http://www.theonion.com/video/the-onion-reviews-avengers-age-of-ultron-38524'],'extract'=>['!theonion\\.com/video/[-\\w]+[-,](?\'id\'\\d+)!'],'homepage'=>'http://www.theonion.com/video/','host'=>'theonion.com','iframe'=>['src'=>'//www.theonion.com/video_embed/?id={@id}'],'name'=>'The Onion','scrape'=>[],'tags'=>['entertainment']],
'tinypic'=>['example'=>['http://tinypic.com/player.php?v=29x86j9&s=8','http://tinypic.com/r/29x86j9/8'],'extract'=>['!tinypic\\.com/player\\.php\\?v=(?\'id\'\\w+)&s=(?\'s\'\\d+)!','!tinypic\\.com/r/(?\'id\'\\w+)/(?\'s\'\\d+)!'],'flash'=>['padding-height'=>30,'src'=>'//tinypic.com/player.swf?file={@id}&s={@s}'],'homepage'=>'http://tinypic.com/','host'=>'tinypic.com','name'=>'TinyPic videos','scrape'=>[['extract'=>['!file=(?\'id\'\\w+)&amp;s=(?\'s\'\\d+)!'],'match'=>['!tinypic\\.com/(?:m|usermedia)/!']]],'tags'=>['images']],
'tmz'=>['example'=>'http://www.tmz.com/videos/0_2pr9x3rb/','extract'=>['@tmz\\.com/videos/(?\'id\'\\w+)@'],'homepage'=>'http://www.tmz.com/videos','host'=>'tmz.com','iframe'=>['src'=>'//www.kaltura.com/index.php/kwidget/cache_st/133592691/wid/_591531/partner_id/591531/uiconf_id/9071262/entry_id/{@id}'],'name'=>'TMZ','scrape'=>[],'tags'=>['gossip']],
'traileraddict'=>['example'=>'http://www.traileraddict.com/the-amazing-spider-man-2/super-bowl-tv-spot','extract'=>[],'homepage'=>'http://www.traileraddict.com/','host'=>'traileraddict.com','iframe'=>['src'=>'//v.traileraddict.com/{@id}'],'name'=>'Trailer Addict','scrape'=>[['extract'=>['@v\\.traileraddict\\.com/(?\'id\'\\d+)@'],'match'=>['@traileraddict\\.com/(?!tags/)[^/]+/.@']]],'tags'=>['movies']],
'tumblr'=>['example'=>'http://mrbenvey.tumblr.com/post/104191225637','extract'=>['!(?\'name\'[-\\w]+)\\.tumblr\\.com/post/(?\'id\'\\d+)!'],'homepage'=>'https://www.tumblr.com/','host'=>'tumblr.com','iframe'=>['data-s9e-livepreview-ignore-attrs'=>'style','height'=>180,'onload'=>'var a=Math.random();window.addEventListener(\'message\',function(b){{if(b.data.id==a)style.height=b.data.height+\'px\'}});contentWindow.postMessage(\'s9e:\'+a,\'https://s9e.github.io\')','src'=>'https://s9e.github.io/iframe/tumblr.min.html#{@key}/{@id}','width'=>520],'name'=>'Tumblr','scrape'=>[['extract'=>['!did=\\\\u0022(?\'did\'[-\\w]+)!','!embed\\\\/post\\\\/(?\'key\'[-\\w]+)!'],'match'=>['!\\w\\.tumblr\\.com/post/\\d!'],'url'=>'http://www.tumblr.com/oembed/1.0?url=http://{@name}.tumblr.com/post/{@id}']],'tags'=>['social']],
'twitch'=>['example'=>['http://www.twitch.tv/twitch','http://www.twitch.tv/twitch/v/29415830?t=17m17s','https://clips.twitch.tv/twitch/HorribleWoodpeckerHassanChop'],'extract'=>['#twitch\\.tv/(?:videos|\\w+/v)/(?\'video_id\'\\d+)?#','#www\\.twitch\\.tv/(?!videos/)(?\'channel\'\\w+)#','#t=(?\'t\'(?:(?:\\d+h)?\\d+m)?\\d+s)#','#clips\\.twitch\\.tv/(?:(?\'channel\'\\w+)/)?(?\'clip_id\'\\w+)#'],'homepage'=>'http://www.twitch.tv/','host'=>'twitch.tv','iframe'=>['src'=>'//<xsl:choose><xsl:when test="@clip_id">clips.twitch.tv/embed?autoplay=false&amp;clip=<xsl:if test="@channel"><xsl:value-of select="@channel"/>/</xsl:if><xsl:value-of select="@clip_id"/></xsl:when><xsl:otherwise>player.twitch.tv/?autoplay=false&amp;<xsl:choose><xsl:when test="@video_id">video=v<xsl:value-of select="@video_id"/></xsl:when><xsl:otherwise>channel=<xsl:value-of select="@channel"/></xsl:otherwise></xsl:choose><xsl:if test="@t">&amp;time=<xsl:value-of select="@t"/></xsl:if></xsl:otherwise></xsl:choose>'],'name'=>'Twitch','scrape'=>[],'source'=>'https://github.com/justintv/Twitch-API/blob/master/embed-video.md','tags'=>['gaming','livestreaming']],
'twitter'=>['example'=>['https://twitter.com/IJasonAlexander/statuses/526635414338023424','https://mobile.twitter.com/DerekTVShow/status/463372588690202624','https://twitter.com/#!/IJasonAlexander/status/526635414338023424'],'extract'=>['@twitter\\.com/(?:#!/)?\\w+/status(?:es)?/(?\'id\'\\d+)@'],'homepage'=>'https://twitter.com/','host'=>'twitter.com','iframe'=>['data-s9e-livepreview-ignore-attrs'=>'style','height'=>186,'onload'=>'var a=Math.random();window.addEventListener(\'message\',function(b){{if(b.data.id==a)style.height=b.data.height+\'px\'}});contentWindow.postMessage(\'s9e:\'+a,\'https://s9e.github.io\')','src'=>'https://s9e.github.io/iframe/twitter.min.html#{@id}','style'=>['background'=>'url(https://abs.twimg.com/favicons/favicon.ico) no-repeat 50% 50%'],'width'=>500],'name'=>'Twitter','scrape'=>[],'tags'=>['social']],
'ustream'=>['choose'=>['otherwise'=>['iframe'=>['src'=>'//www.ustream.tv/embed/{@cid}?html5ui']],'when'=>['iframe'=>['src'=>'//www.ustream.tv/embed/recorded/{@vid}?html5ui'],'test'=>'@vid']],'example'=>['http://www.ustream.tv/channel/ps4-ustream-gameplay','http://www.ustream.tv/baja1000tv','http://www.ustream.tv/recorded/40688256'],'extract'=>['!ustream\\.tv/recorded/(?\'vid\'\\d+)!'],'homepage'=>'http://www.ustream.tv/','host'=>'ustream.tv','name'=>'Ustream','scrape'=>[['extract'=>['!embed/(?\'cid\'\\d+)!'],'match'=>['#ustream\\.tv/(?!explore/|platform/|recorded/|search\\?|upcoming$|user/)(?:channel/)?[-\\w]+#']]],'tags'=>['gaming']],
'vbox7'=>['example'=>'http://vbox7.com/play:3975300ec6','extract'=>['!vbox7\\.com/play:(?\'id\'[\\da-f]+)!'],'homepage'=>'http://vbox7.com/','host'=>'vbox7.com','iframe'=>['src'=>'//vbox7.com/emb/external.php?vid={@id}'],'name'=>'VBOX7','scrape'=>[],'tags'=>['.bg']],
'veoh'=>['example'=>'http://www.veoh.com/watch/v6335577TeB8kyNR','extract'=>['!veoh\\.com/(?:m/watch\\.php\\?v=|watch/)v(?\'id\'\\w+)!'],'flash'=>['padding-height'=>40,'src'=>'//www.veoh.com/swf/webplayer/WebPlayer.swf?version=AFrontend.5.7.0.1509&permalinkId=v{@id}&player=videodetailsembedded&videoAutoPlay=0&id=anonymous'],'homepage'=>'http://www.veoh.com/','host'=>'veoh.com','name'=>'Veoh','scrape'=>[],'tags'=>['videos']],
'vevo'=>['example'=>['http://www.vevo.com/watch/USUV71400682','http://www.vevo.com/watch/eminem/the-monster-explicit/USUV71302925'],'extract'=>['!vevo\\.com/watch/([-/\\w]+/)?(?\'id\'[A-Z0-9]+)!'],'homepage'=>'http://vevo.com/','host'=>'vevo.com','iframe'=>['height'=>324,'src'=>'//cache.vevo.com/m/html/embed.html?video={@id}','width'=>575],'name'=>'VEVO','scrape'=>[],'tags'=>['music']],
'viagame'=>['example'=>'http://www.viagame.com/channels/hearthstone-championship/405177','extract'=>['!viagame\\.com/channels/[^/]+/(?\'id\'\\d+)!'],'homepage'=>'http://www.viagame.com/','host'=>'viagame.com','iframe'=>['height'=>392,'src'=>'//www.viagame.com/embed/{@id}','width'=>640],'name'=>'Viagame','scrape'=>[],'tags'=>['gaming']],
'videodetective'=>['example'=>'http://www.videodetective.com/movies/zootopia/658596','extract'=>['!videodetective\\.com/\\w+/[-\\w]+/(?:trailer/P0*)?(?\'id\'\\d+)!'],'homepage'=>'http://www.videodetective.com/','host'=>'videodetective.com','iframe'=>['src'=>'//www.videodetective.com/embed/video/?options=false&autostart=false&playlist=none&publishedid={@id}'],'name'=>'Video Detective','scrape'=>[]],
'videomega'=>['example'=>'http://videomega.tv/?ref=aPRKXgQdaD','extract'=>['!videomega\\.tv/\\?ref=(?\'id\'\\w+)!'],'homepage'=>'http://videomega.tv/','host'=>'videomega.tv','iframe'=>['src'=>'//videomega.tv/iframe.php?ref={@id}'],'name'=>'Videomega','scrape'=>[],'tags'=>['videos']],
'vidme'=>['example'=>'https://vid.me/6AZf1','extract'=>[],'homepage'=>'https://vid.me','host'=>'vid.me','iframe'=>['src'=>'https://vid.me/e/{@id}'],'name'=>'vidme','scrape'=>[['extract'=>['#vid\\.me/e/(?\'id\'\\w+)#','#oembed.*?url=https://vid\\.me/(?!e/)(?\'id\'\\w+)#'],'match'=>['//']]],'tags'=>['videos']],
'vimeo'=>['example'=>['http://vimeo.com/67207222','http://vimeo.com/channels/staffpicks/67207222'],'extract'=>['!vimeo\\.com/(?:channels/[^/]+/|video/)?(?\'id\'\\d+)!'],'homepage'=>'http://vimeo.com/','host'=>'vimeo.com','iframe'=>['src'=>'//player.vimeo.com/video/{@id}'],'name'=>'Vimeo','scrape'=>[],'source'=>'http://developer.vimeo.com/player/embedding','tags'=>['videos']],
'vine'=>['example'=>'https://vine.co/v/bYwPIluIipH','extract'=>['!vine\\.co/v/(?\'id\'[^/]+)!'],'homepage'=>'https://vine.co/','host'=>'vine.co','iframe'=>['height'=>480,'src'=>'https://vine.co/v/{@id}/embed/simple?audio=1','width'=>480],'name'=>'Vine','scrape'=>[],'tags'=>['social','videos']],
'vk'=>['example'=>['http://vkontakte.ru/video-7016284_163645555','http://vk.com/video226156999_168963041','http://vk.com/newmusicvideos?z=video-13895667_161988074','http://vk.com/video_ext.php?oid=121599878&id=165723901&hash=e06b0878046e1d32'],'extract'=>['!vk(?:\\.com|ontakte\\.ru)/(?:[\\w.]+\\?z=)?video(?\'oid\'-?\\d+)_(?\'vid\'\\d+)!','!vk(?:\\.com|ontakte\\.ru)/video_ext\\.php\\?oid=(?\'oid\'-?\\d+)&id=(?\'vid\'\\d+)&hash=(?\'hash\'[0-9a-f]+)!'],'homepage'=>'https://vk.com/','host'=>['vk.com','vkontakte.ru'],'iframe'=>['height'=>360,'src'=>'//vk.com/video_ext.php?oid={@oid}&id={@vid}&hash={@hash}&hd=1','width'=>607],'name'=>'VK','scrape'=>[['extract'=>['!embed_hash=(?\'hash\'[0-9a-f]+)!'],'match'=>['!vk.*?video-?\\d+_\\d+!'],'url'=>'http://vk.com/video{@oid}_{@vid}']],'tags'=>['.ru']],
'vocaroo'=>['example'=>'http://vocaroo.com/i/s0dRy3rZ47bf','extract'=>['!vocaroo\\.com/i/(?\'id\'\\w+)!'],'flash'=>['height'=>44,'src'=>'//vocaroo.com/player.swf?playMediaID={@id}&autoplay=0','width'=>148],'homepage'=>'http://vocaroo.com/','host'=>'vocaroo.com','name'=>'Vocaroo','scrape'=>[]],
'vox'=>['example'=>'http://www.vox.com/2015/7/21/9005857/ant-man-marvel-apology-review#ooid=ltbzJkdTpKpE-O6hOfD3YJew3t3MppXb','extract'=>['!vox.com/.*#ooid=(?\'id\'[-\\w]+)!'],'homepage'=>'http://www.vox.com/','host'=>'vox.com','iframe'=>['src'=>'//player.ooyala.com/iframe.html#pbid=a637d53c5c0a43c7bf4e342886b9d8b0&ec={@id}'],'name'=>'Vox','scrape'=>[]],
'washingtonpost'=>['example'=>['https://www.washingtonpost.com/video/c/video/df229384-9216-11e6-bc00-1a9756d4111b','https://www.washingtonpost.com/video/world/aurora-display-lights-up-the-night-sky-over-finland/2016/10/14/df229384-9216-11e6-bc00-1a9756d4111b_video.html'],'extract'=>['#washingtonpost\\.com/video/c/\\w+/(?\'id\'[-0-9a-f]+)#','#washingtonpost\\.com/video/[-/\\w]+/(?\'id\'[-0-9a-f]+)_video\\.html#'],'homepage'=>'https://www.washingtonpost.com/video/','host'=>'washingtonpost.com','iframe'=>['src'=>'//www.washingtonpost.com/video/c/embed/{@id}'],'name'=>'Washington Post Video','scrape'=>[],'tags'=>['news']],
'wshh'=>['example'=>['http://www.worldstarhiphop.com/videos/video.php?v=wshhZ8F22UtJ8sLHdja0','http://m.worldstarhiphop.com/video.php?v=wshh2SXFFe7W14DqQx61','http://www.worldstarhiphop.com/featured/71630'],'extract'=>['!worldstarhiphop\\.com/featured/(?\'id\'\\d+)!'],'homepage'=>'http://www.worldstarhiphop.com/','host'=>'worldstarhiphop.com','iframe'=>['src'=>'//www.worldstarhiphop.com/embed/{@id}'],'name'=>'WorldStarHipHop','scrape'=>[['extract'=>['!v: ?"?(?\'id\'\\d+)!'],'match'=>['!worldstarhiphop\\.com/(?:\\w+/)?video\\.php\\?v=\\w+!']]],'tags'=>['videos']],
'wsj'=>['example'=>['http://www.wsj.com/video/nba-players-primp-with-pedicures/9E476D54-6A60-4F3F-ABC1-411014552DE6.html','http://live.wsj.com/#!09FB2B3B-583E-4284-99D8-FEF6C23BE4E2','http://live.wsj.com/video/seahawks-qb-russell-wilson-on-super-bowl-win/9B3DF790-9D20-442C-B564-51524B06FD26.html'],'extract'=>['@wsj\\.com/[^#]*#!(?\'id\'[-0-9A-F]{36})@','@wsj\\.com/video/[^/]+/(?\'id\'[-0-9A-F]{36})@'],'homepage'=>'http://www.wsj.com/video/','host'=>'wsj.com','iframe'=>['height'=>288,'src'=>'//video-api.wsj.com/api-video/player/iframe.html?guid={@id}','width'=>512],'name'=>'The Wall Street Journal Online','scrape'=>[['extract'=>['@guid=(?\'id\'[-0-9A-F]{36})@'],'match'=>['@on\\.wsj\\.com/\\w@']]],'tags'=>['news']],
'xboxclips'=>['example'=>'http://xboxclips.com/dizturbd/e3a2d685-3e9f-454f-89bf-54ddea8f29b3','extract'=>['@xboxclips\\.com/(?\'user\'[^/]+)/(?!screenshots/)(?\'id\'[-0-9a-f]+)@'],'homepage'=>'http://xboxclips.com/','host'=>'xboxclips.com','iframe'=>['src'=>'//xboxclips.com/{@user}/{@id}/embed'],'name'=>'XboxClips','scrape'=>[],'tags'=>['gaming']],
'xboxdvr'=>['example'=>'http://xboxdvr.com/gamer/LOXITANE/video/12463958','extract'=>['!xboxdvr\\.com/gamer/(?\'user\'[^/]+)/video/(?\'id\'\\d+)!'],'homepage'=>'http://xboxdvr.com/','host'=>'xboxdvr.com','iframe'=>['src'=>'//xboxdvr.com/gamer/{@user}/video/{@id}/embed'],'name'=>'Xbox DVR','scrape'=>[],'tags'=>['gaming']],
'yahooscreen'=>['example'=>['https://screen.yahoo.com/mr-short-term-memory-000000263.html','https://screen.yahoo.com/dana-carvey-snl-skits/church-chat-satan-000000502.html'],'extract'=>['!screen\\.yahoo\\.com/(?:[-\\w]+/)?(?\'id\'[-\\w]+)\\.html!'],'homepage'=>'https://screen.yahoo.com/','host'=>'screen.yahoo.com','iframe'=>['src'=>'https://screen.yahoo.com/{@id}.html?format=embed'],'name'=>'Yahoo! Screen','scrape'=>[],'tags'=>['movies']],
'youku'=>['example'=>'http://v.youku.com/v_show/id_XNzQwNjcxNDM2.html','extract'=>['!youku\\.com/v(?:_show|ideo)/id_(?\'id\'\\w+)!'],'homepage'=>'http://www.youku.com/','host'=>'youku.com','iframe'=>['padding-height'=>40,'src'=>'https://players.youku.com/embed/{@id}'],'name'=>'Youku','scrape'=>[],'tags'=>['.cn']],
'youtube'=>['attributes'=>['t'=>['type'=>'timestamp']],'example'=>['http://www.youtube.com/watch?v=-cEzsCAzTak','http://youtu.be/-cEzsCAzTak','http://www.youtube.com/watch?feature=player_detailpage&v=jofNR_WkoCE#t=40','http://www.youtube.com/watch?v=pC35x6iIPmo&list=PLOU2XLYxmsIIxJrlMIY5vYXAFcO5g83gA'],'extract'=>['!youtube\\.com/(?:watch.*?v=|v/|attribution_link.*?v%3D)(?\'id\'[-\\w]+)!','!youtu\\.be/(?\'id\'[-\\w]+)!','@[#&?]t=(?\'t\'\\d[\\dhms]*)@','!&list=(?\'list\'[-\\w]+)!'],'homepage'=>'http://www.youtube.com/','host'=>['youtube.com','youtu.be'],'iframe'=>['src'=>'https://www.youtube.com/embed/<xsl:value-of select="@id"/><xsl:if test="@list">?list=<xsl:value-of select="@list"/></xsl:if><xsl:if test="@t"><xsl:choose><xsl:when test="@list">&amp;</xsl:when><xsl:otherwise>?</xsl:otherwise></xsl:choose>start=<xsl:value-of select="@t"/></xsl:if>','style'=>['background'=>'url(https://i.ytimg.com/vi/{@id}/hqdefault.jpg) 50% 50% / cover']],'name'=>'YouTube','scrape'=>[['extract'=>['!/vi/(?\'id\'[-\\w]+)!'],'match'=>['!/shared\\?ci=!']]],'source'=>'http://support.google.com/youtube/bin/answer.py?hl=en&answer=171780','tags'=>['livestreaming','videos']],
'zippyshare'=>['example'=>'http://www17.zippyshare.com/v/EtPLaXGE/file.html','extract'=>[],'flash'=>['flashvars'=>'file={@file}&server={@server}&autostart=false','height'=>80,'max-width'=>900,'src'=>'//api.zippyshare.com/api/player.swf','width'=>'100%'],'homepage'=>'http://www.zippyshare.com/','host'=>'zippyshare.com','name'=>'Zippyshare audio files','scrape'=>[['extract'=>['!file=(?\'file\'\\w+)&amp;server=(?\'server\'\\d+)!'],'match'=>['!/v/!']]],'tags'=>['file sharing']]
];
}

View File

@ -0,0 +1,28 @@
<?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\Plugins\MediaEmbed\Configurator\Collections;
use ArrayObject;
use s9e\TextFormatter\Configurator\ConfigProvider;
use s9e\TextFormatter\Configurator\JavaScript\Dictionary;
class SiteCollection extends ArrayObject implements ConfigProvider
{
public function asConfig()
{
$map = [];
foreach ($this as $siteId => $siteConfig)
{
if (isset($siteConfig['host']))
foreach ((array) $siteConfig['host'] as $host)
$map[$host] = $siteId;
if (isset($siteConfig['scheme']))
foreach ((array) $siteConfig['scheme'] as $scheme)
$map[$scheme . ':'] = $siteId;
}
return new Dictionary($map);
}
}

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\Plugins\MediaEmbed\Configurator\Collections;
use InvalidArgumentException;
use RuntimeException;
use s9e\TextFormatter\Configurator\Collections\NormalizedCollection;
class SiteDefinitionCollection extends NormalizedCollection
{
protected $onDuplicateAction = 'replace';
protected function getAlreadyExistsException($key)
{
return new RuntimeException("Media site '" . $key . "' already exists");
}
protected function getNotExistException($key)
{
return new RuntimeException("Media site '" . $key . "' does not exist");
}
public function normalizeKey($siteId)
{
$siteId = \strtolower($siteId);
if (!\preg_match('(^[a-z0-9]+$)', $siteId))
throw new InvalidArgumentException('Invalid site ID');
return $siteId;
}
public function normalizeValue($siteConfig)
{
if (!\is_array($siteConfig))
throw new InvalidArgumentException('Invalid site definition type');
$siteConfig += ['extract' => [], 'scrape' => []];
$siteConfig['extract'] = $this->normalizeRegexp($siteConfig['extract']);
$siteConfig['scrape'] = $this->normalizeScrape($siteConfig['scrape']);
return $siteConfig;
}
protected function normalizeRegexp($value)
{
return (array) $value;
}
protected function normalizeScrape($value)
{
if (!empty($value) && !isset($value[0]))
$value = [$value];
foreach ($value as &$scrape)
{
$scrape += ['extract' => [], 'match' => '//'];
$scrape['extract'] = $this->normalizeRegexp($scrape['extract']);
$scrape['match'] = $this->normalizeRegexp($scrape['match']);
}
unset($scrape);
return $value;
}
}

View File

@ -0,0 +1,85 @@
<?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\Plugins\MediaEmbed\Configurator\Collections;
use DOMDocument;
use DOMElement;
use InvalidArgumentException;
class XmlFileDefinitionCollection extends SiteDefinitionCollection
{
protected $configTypes = [
['(^defaultValue$)', '(^[1-9][0-9]*$)D', 'castToInt'],
['(height$|width$)', '(^[1-9][0-9]*$)D', 'castToInt'],
['(^required$)', '(^(?:true|false)$)iD', 'castToBool']
];
public function __construct($path)
{
if (!\file_exists($path) || !\is_dir($path))
throw new InvalidArgumentException('Invalid site directory');
foreach (\glob($path . '/*.xml') as $filepath)
{
$siteId = \basename($filepath, '.xml');
$this->add($siteId, $this->getConfigFromXmlFile($filepath));
}
}
protected function castConfigValue($name, $value)
{
foreach ($this->configTypes as $_d1e08ffa)
{
list($nameRegexp, $valueRegexp, $methodName) = $_d1e08ffa;
if (\preg_match($nameRegexp, $name) && \preg_match($valueRegexp, $value))
return $this->$methodName($value);
}
return $value;
}
protected function castToBool($value)
{
return (\strtolower($value) === 'true');
}
protected function castToInt($value)
{
return (int) $value;
}
protected function convertValueTypes(array $config)
{
foreach ($config as $k => $v)
if (\is_array($v))
$config[$k] = $this->convertValueTypes($v);
else
$config[$k] = $this->castConfigValue($k, $v);
return $config;
}
protected function flattenConfig(array $config)
{
foreach ($config as $k => $v)
if (\is_array($v) && \count($v) === 1)
$config[$k] = \end($v);
return $config;
}
protected function getConfigFromXmlFile($filepath)
{
$dom = new DOMDocument;
$dom->load($filepath, \LIBXML_NOCDATA);
return $this->getElementConfig($dom->documentElement);
}
protected function getElementConfig(DOMElement $element)
{
$config = [];
foreach ($element->attributes as $attribute)
$config[$attribute->name][] = $attribute->value;
foreach ($element->childNodes as $childNode)
if ($childNode instanceof DOMElement)
$config[$childNode->nodeName][] = $this->getValueFromElement($childNode);
return $this->flattenConfig($this->convertValueTypes($config));
}
protected function getValueFromElement(DOMElement $element)
{
return (!$element->attributes->length && $element->childNodes->length === 1 && $element->firstChild->nodeType === \XML_TEXT_NODE)
? $element->nodeValue
: $this->getElementConfig($element);
}
}

View File

@ -0,0 +1,43 @@
<?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\Plugins\MediaEmbed\Configurator;
use DOMXPath;
use s9e\TextFormatter\Configurator\Helpers\TemplateHelper;
use s9e\TextFormatter\Plugins\MediaEmbed\Configurator\TemplateGenerators\Choose;
use s9e\TextFormatter\Plugins\MediaEmbed\Configurator\TemplateGenerators\Flash;
use s9e\TextFormatter\Plugins\MediaEmbed\Configurator\TemplateGenerators\Iframe;
class TemplateBuilder
{
protected $templateGenerators = [];
public function __construct()
{
$this->templateGenerators['choose'] = new Choose($this);
$this->templateGenerators['flash'] = new Flash;
$this->templateGenerators['iframe'] = new Iframe;
}
public function build($siteId, array $siteConfig)
{
return $this->addSiteId($siteId, $this->getTemplate($siteConfig));
}
public function getTemplate(array $config)
{
foreach ($this->templateGenerators as $type => $generator)
if (isset($config[$type]))
return $generator->getTemplate($config[$type]);
return '';
}
protected function addSiteId($siteId, $template)
{
$dom = TemplateHelper::loadTemplate($template);
$xpath = new DOMXPath($dom);
$query = '//*[namespace-uri() != "' . TemplateHelper::XMLNS_XSL . '"][not(ancestor::*[namespace-uri() != "' . TemplateHelper::XMLNS_XSL . '"])]';
foreach ($xpath->query($query) as $element)
$element->setAttribute('data-s9e-mediaembed', $siteId);
return TemplateHelper::saveTemplate($dom);
}
}

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\Plugins\MediaEmbed\Configurator;
use s9e\TextFormatter\Configurator\Helpers\AVTHelper;
abstract class TemplateGenerator
{
protected $attributes;
protected $defaultAttributes = [
'height' => 360,
'padding-height' => 0,
'style' => [],
'width' => 640
];
abstract protected function getContentTemplate();
public function getTemplate(array $attributes)
{
$this->attributes = $attributes + $this->defaultAttributes;
return ($this->needsWrapper()) ? $this->getWrappedTemplate() : $this->getUnwrappedTemplate();
}
protected function expr($expr)
{
$expr = \trim($expr, '{}');
return (\preg_match('(^[@$]?[-\\w]+$)D', $expr)) ? $expr : "($expr)";
}
protected function generateAttributes(array $attributes)
{
if (isset($attributes['style']) && \is_array($attributes['style']))
$attributes['style'] = $this->generateStyle($attributes['style']);
\ksort($attributes);
$xsl = '';
foreach ($attributes as $attrName => $attrValue)
{
$innerXML = (\strpos($attrValue, '<xsl:') !== \false) ? $attrValue : AVTHelper::toXSL($attrValue);
$xsl .= '<xsl:attribute name="' . \htmlspecialchars($attrName, \ENT_QUOTES, 'UTF-8') . '">' . $innerXML . '</xsl:attribute>';
}
return $xsl;
}
protected function generateStyle(array $properties)
{
\ksort($properties);
$style = '';
foreach ($properties as $name => $value)
$style .= $name . ':' . $value . ';';
return \trim($style, ';');
}
protected function getResponsivePadding()
{
$height = $this->expr($this->attributes['height']);
$paddingHeight = $this->expr($this->attributes['padding-height']);
$width = $this->expr($this->attributes['width']);
$css = 'padding-bottom:<xsl:value-of select="100*(' . $height . '+' . $paddingHeight . ')div' . $width . '"/>%';
if (!empty($this->attributes['padding-height']))
$css .= ';padding-bottom:calc(<xsl:value-of select="100*' . $height . ' div' . $width . '"/>% + ' . $paddingHeight . 'px)';
if (\strpos($width, '@') !== \false)
$css = '<xsl:if test="@width&gt;0">' . $css . '</xsl:if>';
return $css;
}
protected function getUnwrappedTemplate()
{
$this->attributes['style']['width'] = '100%';
$this->attributes['style']['height'] = $this->attributes['height'] . 'px';
$this->attributes['style']['max-width'] = '100%';
if (isset($this->attributes['max-width']))
$this->attributes['style']['max-width'] = $this->attributes['max-width'] . 'px';
elseif ($this->attributes['width'] !== '100%')
{
$property = ($this->hasDynamicWidth()) ? 'width' : 'max-width';
$this->attributes['style'][$property] = $this->attributes['width'] . 'px';
}
if ($this->attributes['style']['width'] === $this->attributes['style']['max-width'])
unset($this->attributes['style']['max-width']);
return $this->getContentTemplate();
}
protected function getWrappedTemplate()
{
$this->attributes['style']['width'] = '100%';
$this->attributes['style']['height'] = '100%';
$this->attributes['style']['position'] = 'absolute';
$this->attributes['style']['left'] = '0';
$outerStyle = 'display:inline-block;width:100%;max-width:' . $this->attributes['width'] . 'px';
$innerStyle = 'display:block;overflow:hidden;position:relative;' . $this->getResponsivePadding();
$template = '<span>' . $this->generateAttributes(['style' => $outerStyle]);
$template .= '<span>' . $this->generateAttributes(['style' => $innerStyle]);
$template .= $this->getContentTemplate();
$template .= '</span></span>';
return $template;
}
protected function hasDynamicHeight()
{
return (isset($this->attributes['onload']) && \strpos($this->attributes['onload'], '.height') !== \false);
}
protected function hasDynamicWidth()
{
return (isset($this->attributes['onload']) && \strpos($this->attributes['onload'], '.width') !== \false);
}
protected function mergeAttributes(array $defaultAttributes, array $newAttributes)
{
$attributes = \array_merge($defaultAttributes, $newAttributes);
if (isset($defaultAttributes['style'], $newAttributes['style']))
$attributes['style'] += $defaultAttributes['style'];
return $attributes;
}
protected function needsWrapper()
{
return ($this->attributes['width'] !== '100%' && !$this->hasDynamicHeight());
}
}

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\Plugins\MediaEmbed\Configurator\TemplateGenerators;
use s9e\TextFormatter\Plugins\MediaEmbed\Configurator\TemplateBuilder;
use s9e\TextFormatter\Plugins\MediaEmbed\Configurator\TemplateGenerator;
class Choose extends TemplateGenerator
{
protected $templateBuilder;
public function __construct(TemplateBuilder $templateBuilder)
{
$this->templateBuilder = $templateBuilder;
}
protected function needsWrapper()
{
return \false;
}
protected function getContentTemplate()
{
$branches = (isset($this->attributes['when'][0])) ? $this->attributes['when'] : [$this->attributes['when']];
$template = '<xsl:choose>';
foreach ($branches as $when)
$template .= '<xsl:when test="' . \htmlspecialchars($when['test'], \ENT_COMPAT, 'UTF-8') . '">' . $this->templateBuilder->getTemplate($when) . '</xsl:when>';
$template .= '<xsl:otherwise>' . $this->templateBuilder->getTemplate($this->attributes['otherwise']) . '</xsl:otherwise></xsl:choose>';
return $template;
}
}

View File

@ -0,0 +1,34 @@
<?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\Plugins\MediaEmbed\Configurator\TemplateGenerators;
use s9e\TextFormatter\Plugins\MediaEmbed\Configurator\TemplateGenerator;
class Flash extends TemplateGenerator
{
protected function getContentTemplate()
{
$attributes = [
'data' => $this->attributes['src'],
'style' => $this->attributes['style'],
'type' => 'application/x-shockwave-flash',
'typemustmatch' => ''
];
$flashVarsParam = '';
if (isset($this->attributes['flashvars']))
$flashVarsParam = $this->generateParamElement('flashvars', $this->attributes['flashvars']);
$template = '<object>'
. $this->generateAttributes($attributes)
. $this->generateParamElement('allowfullscreen', 'true')
. $flashVarsParam
. '</object>';
return $template;
}
protected function generateParamElement($paramName, $paramValue)
{
return '<param name="' . \htmlspecialchars($paramName) . '">' . $this->generateAttributes(['value' => $paramValue]) . '</param>';
}
}

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\Plugins\MediaEmbed\Configurator\TemplateGenerators;
use s9e\TextFormatter\Plugins\MediaEmbed\Configurator\TemplateGenerator;
class Iframe extends TemplateGenerator
{
protected $defaultIframeAttributes = [
'allowfullscreen' => '',
'scrolling' => 'no',
'style' => ['border' => '0']
];
protected $iframeAttributes = ['data-s9e-livepreview-ignore-attrs', 'data-s9e-livepreview-postprocess', 'onload', 'scrolling', 'src', 'style'];
protected function getContentTemplate()
{
$attributes = $this->mergeAttributes($this->defaultIframeAttributes, $this->getFilteredAttributes());
return '<iframe>' . $this->generateAttributes($attributes) . '</iframe>';
}
protected function getFilteredAttributes()
{
return \array_intersect_key($this->attributes, \array_flip($this->iframeAttributes));
}
}

View File

@ -0,0 +1,10 @@
matches.forEach(function(m)
{
var url = m[0][0],
pos = m[0][1],
len = url.length,
// Give that tag priority over other tags such as Autolink's
tag = addSelfClosingTag(config.tagName, pos, len, -10);
tag.setAttribute('url', url);
});

View File

@ -0,0 +1,178 @@
<?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\Plugins\MediaEmbed;
use s9e\TextFormatter\Parser as TagStack;
use s9e\TextFormatter\Parser\Tag;
use s9e\TextFormatter\Plugins\ParserBase;
use s9e\TextFormatter\Utils\Http;
class Parser extends ParserBase
{
protected static $client;
public function parse($text, array $matches)
{
foreach ($matches as $m)
{
$url = $m[0][0];
$pos = $m[0][1];
$len = \strlen($url);
$tag = $this->parser->addSelfClosingTag($this->config['tagName'], $pos, $len, -10);
$tag->setAttribute('url', $url);
}
}
public static function filterTag(Tag $tag, TagStack $tagStack, array $sites)
{
if ($tag->hasAttribute('site'))
self::addTagFromMediaId($tag, $tagStack, $sites);
elseif ($tag->hasAttribute('url'))
self::addTagFromMediaUrl($tag, $tagStack, $sites);
return \false;
}
public static function hasNonDefaultAttribute(Tag $tag)
{
foreach ($tag->getAttributes() as $attrName => $void)
if ($attrName !== 'url')
return \true;
return \false;
}
public static function scrape(Tag $tag, array $scrapeConfig, $cacheDir = \null)
{
if ($tag->hasAttribute('url'))
{
$url = $tag->getAttribute('url');
if (\preg_match('#^https?://[^<>"\'\\s]+$#Di', $url))
{
$url = \strtolower(\substr($url, 0, 5)) . \substr($url, 5);
foreach ($scrapeConfig as $scrape)
self::scrapeEntry($url, $tag, $scrape, $cacheDir);
}
}
return \true;
}
protected static function addSiteTag(Tag $tag, TagStack $tagStack, $siteId)
{
$endTag = $tag->getEndTag();
if ($endTag)
{
$startPos = $tag->getPos();
$startLen = $tag->getLen();
$endPos = $endTag->getPos();
$endLen = $endTag->getLen();
}
else
{
$startPos = $tag->getPos();
$startLen = 0;
$endPos = $tag->getPos() + $tag->getLen();
$endLen = 0;
}
$tagStack->addTagPair(\strtoupper($siteId), $startPos, $startLen, $endPos, $endLen, $tag->getSortPriority())->setAttributes($tag->getAttributes());
}
protected static function addTagFromMediaId(Tag $tag, TagStack $tagStack, array $sites)
{
$siteId = \strtolower($tag->getAttribute('site'));
if (\in_array($siteId, $sites, \true))
self::addSiteTag($tag, $tagStack, $siteId);
}
protected static function addTagFromMediaUrl(Tag $tag, TagStack $tagStack, array $sites)
{
$p = \parse_url($tag->getAttribute('url'));
if (isset($p['scheme']) && isset($sites[$p['scheme'] . ':']))
$siteId = $sites[$p['scheme'] . ':'];
elseif (isset($p['host']))
$siteId = self::findSiteIdByHost($p['host'], $sites);
if (!empty($siteId))
self::addSiteTag($tag, $tagStack, $siteId);
}
protected static function findSiteIdByHost($host, array $sites)
{
do
{
if (isset($sites[$host]))
return $sites[$host];
$pos = \strpos($host, '.');
if ($pos === \false)
break;
$host = \substr($host, 1 + $pos);
}
while ($host > '');
return \false;
}
protected static function getHttpClient()
{
if (!isset(self::$client))
self::$client = Http::getClient();
self::$client->timeout = 10;
return self::$client;
}
protected static function replaceTokens($url, array $vars)
{
return \preg_replace_callback(
'#\\{@(\\w+)\\}#',
function ($m) use ($vars)
{
return (isset($vars[$m[1]])) ? $vars[$m[1]] : '';
},
$url
);
}
protected static function scrapeEntry($url, Tag $tag, array $scrape, $cacheDir)
{
list($matchRegexps, $extractRegexps, $attrNames) = $scrape;
if (!self::tagIsMissingAnyAttribute($tag, $attrNames))
return;
$vars = [];
$matched = \false;
foreach ((array) $matchRegexps as $matchRegexp)
if (\preg_match($matchRegexp, $url, $m))
{
$vars += $m;
$matched = \true;
}
if (!$matched)
return;
$vars += $tag->getAttributes();
$scrapeUrl = (isset($scrape[3])) ? self::replaceTokens($scrape[3], $vars) : $url;
self::scrapeUrl($scrapeUrl, $tag, (array) $extractRegexps, $cacheDir);
}
protected static function scrapeUrl($url, Tag $tag, array $regexps, $cacheDir)
{
$content = self::wget($url, $cacheDir);
foreach ($regexps as $regexp)
if (\preg_match($regexp, $content, $m))
foreach ($m as $k => $v)
if (!\is_numeric($k) && !$tag->hasAttribute($k))
$tag->setAttribute($k, $v);
}
protected static function tagIsMissingAnyAttribute(Tag $tag, array $attrNames)
{
foreach ($attrNames as $attrName)
if (!$tag->hasAttribute($attrName))
return \true;
return \false;
}
protected static function wget($url, $cacheDir = \null)
{
$prefix = '';
$url = \preg_replace('(#.*)s', '', $url);
if (isset($cacheDir) && \file_exists($cacheDir))
{
$cacheFile = $cacheDir . '/http.' . \crc32($url);
if (\extension_loaded('zlib'))
{
$prefix = 'compress.zlib://';
$cacheFile .= '.gz';
}
if (\file_exists($cacheFile))
return \file_get_contents($prefix . $cacheFile);
}
$content = @self::getHttpClient()->get($url, ['User-Agent: PHP (not Mozilla)']);
if (isset($cacheFile) && !empty($content))
\file_put_contents($prefix . $cacheFile, $content);
return $content;
}
}

View File

@ -0,0 +1,18 @@
/**
* Test whether a given tag has at least one non-default attribute
*
* @param {!Tag} tag The original tag
* @return {!boolean} Whether the tag contains an attribute not named "url"
*/
function (tag)
{
for (var attrName in tag.getAttributes())
{
if (attrName !== 'url')
{
return true;
}
}
return false;
}

View File

@ -0,0 +1,145 @@
/**
* @param {!Tag} tag The original tag
* @param {!Object} sites Map of [host => siteId]
* @return {!boolean} Always false
*/
function (tag, sites)
{
function in_array(needle, haystack)
{
var k;
for (k in haystack)
{
if (haystack[k] === needle)
{
return true;
}
}
return false;
}
/**
* Filter a MEDIA tag
*
* This will always invalidate the original tag, and possibly replace it with the tag that
* corresponds to the media site
*
* @param {!Tag} tag The original tag
* @param {!Object} sites Map of [host => siteId]
* @return {!boolean} Always false
*/
function filterTag(tag, sites)
{
if (tag.hasAttribute('site'))
{
addTagFromMediaId(tag, sites);
}
else if (tag.hasAttribute('url'))
{
addTagFromMediaUrl(tag, sites);
}
return false;
}
/**
* Add a site tag
*
* @param {!Tag} tag The original tag
* @param {!string} siteId Site ID
*/
function addSiteTag(tag, siteId)
{
var endTag = tag.getEndTag();
if (endTag)
{
var startPos = tag.getPos(),
startLen = tag.getLen(),
endPos = endTag.getPos(),
endLen = endTag.getLen();
}
else
{
var startPos = tag.getPos(),
startLen = 0,
endPos = tag.getPos() + tag.getLen(),
endLen = 0;
}
// Create a new tag and copy this tag's attributes and priority
addTagPair(siteId.toUpperCase(), startPos, startLen, endPos, endLen, tag.getSortPriority()).setAttributes(tag.getAttributes());
}
/**
* Add a media site tag based on the attributes of a MEDIA tag
*
* @param {!Tag} tag The original tag
* @param {!Object} sites Map of [host => siteId]
*/
function addTagFromMediaId(tag, sites)
{
var siteId = tag.getAttribute('site').toLowerCase();
if (in_array(siteId, sites))
{
addSiteTag(tag, siteId);
}
}
/**
* Add a media site tag based on the url attribute of a MEDIA tag
*
* @param {!Tag} tag The original tag
* @param {!Object} sites Map of [host => siteId]
*/
function addTagFromMediaUrl(tag, sites)
{
// Capture the scheme and (if applicable) host of the URL
var p = /^(?:([^:]+):)?(?:\/\/([^\/]+))?/.exec(tag.getAttribute('url')), siteId;
if (p[1] && sites[p[1] + ':'])
{
siteId = sites[p[1] + ':'];
}
else if (p[2])
{
siteId = findSiteIdByHost(p[2], sites);
}
if (siteId)
{
addSiteTag(tag, siteId);
}
}
/**
* Match a given host to a site ID
*
* @param {!string} host Host
* @param {!Object} sites Map of [host => siteId]
* @return {!string|!boolean} Site ID or FALSE
*/
function findSiteIdByHost(host, sites)
{
// Start with the full host then pop domain labels off the start until we get a match
do
{
if (sites[host])
{
return sites[host];
}
var pos = host.indexOf('.');
if (pos < 0)
{
break;
}
host = host.substr(1 + pos);
}
while (host > '');
return false;
}
return filterTag(tag, sites);
}

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\Plugins;
use s9e\TextFormatter\Parser;
abstract class ParserBase
{
protected $config;
protected $parser;
final public function __construct(Parser $parser, array $config)
{
$this->parser = $parser;
$this->config = $config;
$this->setUp();
}
protected function setUp()
{
}
abstract public function parse($text, array $matches);
}

View File

@ -0,0 +1,55 @@
<?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\Plugins\PipeTables;
use s9e\TextFormatter\Configurator\Items\AttributeFilters\ChoiceFilter;
use s9e\TextFormatter\Plugins\ConfiguratorBase;
class Configurator extends ConfiguratorBase
{
protected $quickMatch = '|';
protected function setUp()
{
$tags = [
'TABLE' => ['template' => '<table><xsl:apply-templates/></table>'],
'TBODY' => ['template' => '<tbody><xsl:apply-templates/></tbody>'],
'TD' => $this->generateCellTagConfig('td'),
'TH' => $this->generateCellTagConfig('th'),
'THEAD' => ['template' => '<thead><xsl:apply-templates/></thead>'],
'TR' => ['template' => '<tr><xsl:apply-templates/></tr>']
];
foreach ($tags as $tagName => $tagConfig)
if (!isset($this->configurator->tags[$tagName]))
$this->configurator->tags->add($tagName, $tagConfig);
}
protected function generateCellTagConfig($elName)
{
$alignFilter = new ChoiceFilter(['left', 'center', 'right', 'justify'], \true);
return [
'attributes' => [
'align' => [
'filterChain' => ['strtolower', $alignFilter],
'required' => \false
]
],
'rules' => ['createParagraphs' => \false],
'template' =>
'<' . $elName . '>
<xsl:if test="@align">
<xsl:attribute name="style">text-align:<xsl:value-of select="@align"/></xsl:attribute>
</xsl:if>
<xsl:apply-templates/>
</' . $elName . '>'
];
}
public function asConfig()
{
return [
'overwriteEscapes' => isset($this->configurator->Escaper),
'overwriteMarkdown' => isset($this->configurator->Litedown)
];
}
}

View File

@ -0,0 +1,369 @@
var pos, table = null, tableTag, tables, text;
if (config.overwriteMarkdown)
{
overwriteMarkdown();
}
if (config.overwriteEscapes)
{
overwriteEscapes();
}
captureTables();
processTables();
/**
* Add current line to a table
*
* @param {!string} line Line of text
*/
function addLine(line)
{
var ignoreLen = 0;
if (!table)
{
table = { rows: [] };
// Make the table start at the first non-space character
ignoreLen = /^ */.exec(line)[0].length;
line = line.substr(ignoreLen);
}
// Overwrite the outermost pipes
line = line.replace(/^( *)\|/, '$1 ').replace(/\|( *)$/, ' $1');
table.rows.push({ line: line, pos: pos + ignoreLen });
}
/**
* Process current table's body
*/
function addTableBody()
{
var i = 1,
cnt = table.rows.length;
while (++i < cnt)
{
addTableRow('TD', table.rows[i]);
}
createBodyTags(table.rows[2].pos, pos);
}
/**
* Add a cell's tags for current table at current position
*
* @param {!string} tagName Either TD or TH
* @param {!string} align Either "left", "center", "right" or ""
*/
function addTableCell(tagName, align, text)
{
var startPos = pos,
endPos = startPos + text.length,
ignoreLen;
pos = endPos;
var m = /^( *).*?( *)$/.exec(text);
if (m[1])
{
ignoreLen = m[1].length;
createIgnoreTag(startPos, ignoreLen);
startPos += ignoreLen;
}
if (m[2])
{
ignoreLen = m[2].length;
createIgnoreTag(endPos - ignoreLen, ignoreLen);
endPos -= ignoreLen;
}
createCellTags(tagName, startPos, endPos, align);
}
/**
* Process current table's head
*/
function addTableHead()
{
addTableRow('TH', table.rows[0]);
createHeadTags(table.rows[0].pos, pos);
}
/**
* Process given table row
*
* @param {!string} tagName Either TD or TH
* @param {!Object} row
*/
function addTableRow(tagName, row)
{
pos = row.pos;
row.line.split('|').forEach(function(str, i)
{
if (i > 0)
{
createIgnoreTag(pos, 1);
++pos;
}
var align = (!table.cols[i]) ? '' : table.cols[i];
addTableCell(tagName, align, str);
});
createRowTags(row.pos, pos);
}
/**
* Capture all pipe tables in current text
*/
function captureTables()
{
table = null;
tables = [];
pos = 0;
text.split("\n").forEach(function(line)
{
if (line.indexOf('|') < 0)
{
endTable();
}
else
{
addLine(line);
}
pos += 1 + line.length;
});
endTable();
}
/**
* Create a pair of TBODY tags for given text span
*
* @param {!number} startPos
* @param {!number} endPos
*/
function createBodyTags(startPos, endPos)
{
addTagPair('TBODY', startPos, 0, endPos, 0, -103);
}
/**
* Create a pair of TD or TH tags for given text span
*
* @param {!string} tagName Either TD or TH
* @param {!number} startPos
* @param {!number} endPos
* @param {!string} align Either "left", "center", "right" or ""
*/
function createCellTags(tagName, startPos, endPos, align)
{
var tag;
if (startPos === endPos)
{
tag = addSelfClosingTag(tagName, startPos, 0, -101);
}
else
{
tag = addTagPair(tagName, startPos, 0, endPos, 0, -101);
}
if (align)
{
tag.setAttribute('align', align);
}
}
/**
* Create a pair of THEAD tags for given text span
*
* @param {!number} startPos
* @param {!number} endPos
*/
function createHeadTags(startPos, endPos)
{
addTagPair('THEAD', startPos, 0, endPos, 0, -103);
}
/**
* Create an ignore tag for given text span
*
* @param {!number} pos
* @param {!number} len
*/
function createIgnoreTag(pos, len)
{
tableTag.cascadeInvalidationTo(addIgnoreTag(pos, len, 1000));
}
/**
* Create a pair of TR tags for given text span
*
* @param {!number} startPos
* @param {!number} endPos
*/
function createRowTags(startPos, endPos)
{
addTagPair('TR', startPos, 0, endPos, 0, -102);
}
/**
* Create an ignore tag for given separator row
*
* @param {!Object} row
*/
function createSeparatorTag(row)
{
createIgnoreTag(row.pos - 1, 1 + row.line.length);
}
/**
* Create a pair of TABLE tags for given text span
*
* @param {!number} startPos
* @param {!number} endPos
*/
function createTableTags(startPos, endPos)
{
tableTag = addTagPair('TABLE', startPos, 0, endPos, 0, -104);
}
/**
* End current buffered table
*/
function endTable()
{
if (hasValidTable())
{
table.cols = parseColumnAlignments(table.rows[1].line);
tables.push(table);
}
table = null;
}
/**
* Test whether a valid table is currently buffered
*
* @return {!boolean}
*/
function hasValidTable()
{
return (table && table.rows.length > 2 && isValidSeparator(table.rows[1].line));
}
/**
* Test whether given line is a valid separator
*
* @param {!string} line
* @return {!boolean}
*/
function isValidSeparator(line)
{
return /^ *:?-+:?(?:(?:\+| *\| *):?-+:?)+ */.test(line);
}
/**
* Overwrite right angle brackets in given match
*
* @param {!string} str
* @return {!string}
*/
function overwriteBlockquoteCallback(str)
{
return str.replace(/>/g, ' ');
}
/**
* Overwrite escape sequences in current text
*/
function overwriteEscapes()
{
if (text.indexOf('\\|') > -1)
{
text = text.replace(/\\[\\|]/g, '..');
}
}
/**
* Overwrite backticks in given match
*
* @param {!string} str
* @return string
*/
function overwriteInlineCodeCallback(str)
{
return str.replace(/\|/g, '.');
}
/**
* Overwrite Markdown-style markup in current text
*/
function overwriteMarkdown()
{
// Overwrite inline code spans
if (text.indexOf('`') > -1)
{
text = text.replace(/`[^`]*`/g, overwriteInlineCodeCallback);
}
// Overwrite blockquotes
if (text.indexOf('>') > -1)
{
text = text.replace(/^(?:> ?)+/gm, overwriteBlockquoteCallback);
}
}
/**
* Parse and return column alignments in given separator line
*
* @param {!string} line
* @return {!Array<!string>}
*/
function parseColumnAlignments(line)
{
// Use a bitfield to represent the colons' presence and map it to the CSS value
var align = [
'',
'right',
'left',
'center'
],
cols = [],
regexp = /(:?)-+(:?)/g,
m;
while (m = regexp.exec(line))
{
var key = (m[1] ? 2 : 0) + (m[2] ? 1 : 0);
cols.push(align[key]);
}
return cols;
}
/**
* Process current table declaration
*/
function processCurrentTable()
{
var firstRow = table.rows[0],
lastRow = table.rows[table.rows.length - 1];
createTableTags(firstRow.pos, lastRow.pos + lastRow.line.length);
addTableHead();
createSeparatorTag(table.rows[1]);
addTableBody();
}
/**
* Process all the captured tables
*/
function processTables()
{
var i = -1, cnt = tables.length;
while (++i < cnt)
{
table = tables[i];
processCurrentTable();
}
}

View File

@ -0,0 +1,210 @@
<?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\Plugins\PipeTables;
use s9e\TextFormatter\Plugins\ParserBase;
class Parser extends ParserBase
{
protected $pos;
protected $table;
protected $tableTag;
protected $tables;
protected $text;
public function parse($text, array $matches)
{
$this->text = $text;
if ($this->config['overwriteMarkdown'])
$this->overwriteMarkdown();
if ($this->config['overwriteEscapes'])
$this->overwriteEscapes();
$this->captureTables();
$this->processTables();
unset($this->tables);
unset($this->text);
}
protected function addLine($line)
{
$ignoreLen = 0;
if (!isset($this->table))
{
$this->table = [];
\preg_match('/^ */', $line, $m);
$ignoreLen = \strlen($m[0]);
$line = \substr($line, $ignoreLen);
}
$line = \preg_replace('/^( *)\\|/', '$1 ', $line);
$line = \preg_replace('/\\|( *)$/', ' $1', $line);
$this->table['rows'][] = ['line' => $line, 'pos' => $this->pos + $ignoreLen];
}
protected function addTableBody()
{
$i = 1;
$cnt = \count($this->table['rows']);
while (++$i < $cnt)
$this->addTableRow('TD', $this->table['rows'][$i]);
$this->createBodyTags($this->table['rows'][2]['pos'], $this->pos);
}
protected function addTableCell($tagName, $align, $text)
{
$startPos = $this->pos;
$endPos = $startPos + \strlen($text);
$this->pos = $endPos;
\preg_match('/^( *).*?( *)$/', $text, $m);
if ($m[1])
{
$ignoreLen = \strlen($m[1]);
$this->createIgnoreTag($startPos, $ignoreLen);
$startPos += $ignoreLen;
}
if ($m[2])
{
$ignoreLen = \strlen($m[2]);
$this->createIgnoreTag($endPos - $ignoreLen, $ignoreLen);
$endPos -= $ignoreLen;
}
$this->createCellTags($tagName, $startPos, $endPos, $align);
}
protected function addTableHead()
{
$this->addTableRow('TH', $this->table['rows'][0]);
$this->createHeadTags($this->table['rows'][0]['pos'], $this->pos);
}
protected function addTableRow($tagName, $row)
{
$this->pos = $row['pos'];
foreach (\explode('|', $row['line']) as $i => $str)
{
if ($i > 0)
{
$this->createIgnoreTag($this->pos, 1);
++$this->pos;
}
$align = (empty($this->table['cols'][$i])) ? '' : $this->table['cols'][$i];
$this->addTableCell($tagName, $align, $str);
}
$this->createRowTags($row['pos'], $this->pos);
}
protected function captureTables()
{
unset($this->table);
$this->tables = [];
$this->pos = 0;
foreach (\explode("\n", $this->text) as $line)
{
if (\strpos($line, '|') === \false)
$this->endTable();
else
$this->addLine($line);
$this->pos += 1 + \strlen($line);
}
$this->endTable();
}
protected function createBodyTags($startPos, $endPos)
{
$this->parser->addTagPair('TBODY', $startPos, 0, $endPos, 0, -103);
}
protected function createCellTags($tagName, $startPos, $endPos, $align)
{
if ($startPos === $endPos)
$tag = $this->parser->addSelfClosingTag($tagName, $startPos, 0, -101);
else
$tag = $this->parser->addTagPair($tagName, $startPos, 0, $endPos, 0, -101);
if ($align)
$tag->setAttribute('align', $align);
}
protected function createHeadTags($startPos, $endPos)
{
$this->parser->addTagPair('THEAD', $startPos, 0, $endPos, 0, -103);
}
protected function createIgnoreTag($pos, $len)
{
$this->tableTag->cascadeInvalidationTo($this->parser->addIgnoreTag($pos, $len, 1000));
}
protected function createRowTags($startPos, $endPos)
{
$this->parser->addTagPair('TR', $startPos, 0, $endPos, 0, -102);
}
protected function createSeparatorTag(array $row)
{
$this->createIgnoreTag($row['pos'] - 1, 1 + \strlen($row['line']));
}
protected function createTableTags($startPos, $endPos)
{
$this->tableTag = $this->parser->addTagPair('TABLE', $startPos, 0, $endPos, 0, -104);
}
protected function endTable()
{
if ($this->hasValidTable())
{
$this->table['cols'] = $this->parseColumnAlignments($this->table['rows'][1]['line']);
$this->tables[] = $this->table;
}
unset($this->table);
}
protected function hasValidTable()
{
return (isset($this->table) && \count($this->table['rows']) > 2 && $this->isValidSeparator($this->table['rows'][1]['line']));
}
protected function isValidSeparator($line)
{
return (bool) \preg_match('/^ *:?-+:?(?:(?:\\+| *\\| *):?-+:?)+ *$/', $line);
}
protected function overwriteBlockquoteCallback(array $m)
{
return \strtr($m[0], '>', ' ');
}
protected function overwriteEscapes()
{
if (\strpos($this->text, '\\|') !== \false)
$this->text = \preg_replace('/\\\\[\\\\|]/', '..', $this->text);
}
protected function overwriteInlineCodeCallback(array $m)
{
return \strtr($m[0], '|', '.');
}
protected function overwriteMarkdown()
{
if (\strpos($this->text, '`') !== \false)
$this->text = \preg_replace_callback('/`[^`]*`/', [$this, 'overwriteInlineCodeCallback'], $this->text);
if (\strpos($this->text, '>') !== \false)
$this->text = \preg_replace_callback('/^(?:> ?)+/m', [$this, 'overwriteBlockquoteCallback'], $this->text);
}
protected function parseColumnAlignments($line)
{
$align = [
0b00 => '',
0b01 => 'right',
0b10 => 'left',
0b11 => 'center'
];
$cols = [];
\preg_match_all('/(:?)-+(:?)/', $line, $matches, \PREG_SET_ORDER);
foreach ($matches as $m)
{
$key = (!empty($m[1]) ? 2 : 0) + (!empty($m[2]) ? 1 : 0);
$cols[] = $align[$key];
}
return $cols;
}
protected function processCurrentTable()
{
$firstRow = $this->table['rows'][0];
$lastRow = \end($this->table['rows']);
$this->createTableTags($firstRow['pos'], $lastRow['pos'] + \strlen($lastRow['line']));
$this->addTableHead();
$this->createSeparatorTag($this->table['rows'][1]);
$this->addTableBody();
}
protected function processTables()
{
foreach ($this->tables as $table)
{
$this->table = $table;
$this->processCurrentTable();
}
}
}

View File

@ -0,0 +1,255 @@
<?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\Plugins\Preg;
use DOMAttr;
use DOMText;
use DOMXPath;
use Exception;
use InvalidArgumentException;
use s9e\TextFormatter\Configurator\Helpers\RegexpParser;
use s9e\TextFormatter\Configurator\Helpers\TemplateHelper;
use s9e\TextFormatter\Configurator\Items\Regexp;
use s9e\TextFormatter\Configurator\Items\Tag;
use s9e\TextFormatter\Configurator\JavaScript\RegexpConvertor;
use s9e\TextFormatter\Configurator\Validators\TagName;
use s9e\TextFormatter\Plugins\ConfiguratorBase;
class Configurator extends ConfiguratorBase
{
protected $captures;
protected $collection = [];
protected $delimiter;
protected $modifiers;
protected $references;
protected $referencesRegexp = '((?<!\\\\)(?:\\\\\\\\)*\\K(?:[$\\\\]\\d+|\\$\\{\\d+\\}))S';
public function asConfig()
{
if (!\count($this->collection))
return;
$pregs = [];
foreach ($this->collection as $_ca164be8)
{
list($tagName, $regexp, $passthroughIdx) = $_ca164be8;
$captures = RegexpParser::getCaptureNames($regexp);
$pregs[] = [$tagName, new Regexp($regexp, \true), $passthroughIdx, $captures];
}
return ['generics' => $pregs];
}
public function getJSHints()
{
$hasPassthrough = \false;
foreach ($this->collection as $_ca164be8)
{
list($tagName, $regexp, $passthroughIdx) = $_ca164be8;
if ($passthroughIdx)
{
$hasPassthrough = \true;
break;
}
}
return ['PREG_HAS_PASSTHROUGH' => $hasPassthrough];
}
public function match($regexp, $tagName)
{
$tagName = TagName::normalize($tagName);
$passthroughIdx = 0;
$this->parseRegexp($regexp);
foreach ($this->captures as $i => $capture)
{
if (!$this->isCatchAll($capture['expr']))
continue;
$passthroughIdx = $i;
}
$this->collection[] = [$tagName, $regexp, $passthroughIdx];
}
public function replace($regexp, $template, $tagName = \null)
{
if (!isset($tagName))
$tagName = 'PREG_' . \strtoupper(\dechex(\crc32($regexp)));
$this->parseRegexp($regexp);
$this->parseTemplate($template);
$passthroughIdx = $this->getPassthroughCapture();
if ($passthroughIdx)
$this->captures[$passthroughIdx]['passthrough'] = \true;
$regexp = $this->fixUnnamedCaptures($regexp);
$template = $this->convertTemplate($template, $passthroughIdx);
$this->collection[] = [$tagName, $regexp, $passthroughIdx];
return $this->createTag($tagName, $template);
}
protected function addAttribute(Tag $tag, $attrName)
{
$isUrl = \false;
$exprs = [];
foreach ($this->captures as $key => $capture)
{
if ($capture['name'] !== $attrName)
continue;
$exprs[] = $capture['expr'];
if (isset($this->references['asUrl'][$key]))
$isUrl = \true;
}
$exprs = \array_unique($exprs);
$regexp = $this->delimiter . '^';
$regexp .= (\count($exprs) === 1) ? $exprs[0] : '(?:' . \implode('|', $exprs) . ')';
$regexp .= '$' . $this->delimiter . 'D' . $this->modifiers;
$attribute = $tag->attributes->add($attrName);
$filter = $this->configurator->attributeFilters['#regexp'];
$filter->setRegexp($regexp);
$attribute->filterChain[] = $filter;
if ($isUrl)
{
$filter = $this->configurator->attributeFilters['#url'];
$attribute->filterChain[] = $filter;
}
}
protected function convertTemplate($template, $passthroughIdx)
{
$template = TemplateHelper::replaceTokens(
$template,
$this->referencesRegexp,
function ($m, $node) use ($passthroughIdx)
{
$key = (int) \trim($m[0], '\\${}');
if ($key === 0)
return ['expression', '.'];
if ($key === $passthroughIdx && $node instanceof DOMText)
return ['passthrough'];
if (isset($this->captures[$key]['name']))
return ['expression', '@' . $this->captures[$key]['name']];
return ['literal', ''];
}
);
$template = TemplateHelper::replaceTokens(
$template,
'(\\\\+[0-9${\\\\])',
function ($m)
{
return ['literal', \stripslashes($m[0])];
}
);
return $template;
}
protected function createTag($tagName, $template)
{
$tag = new Tag;
foreach ($this->captures as $key => $capture)
{
if (!isset($capture['name']))
continue;
$attrName = $capture['name'];
if (isset($tag->attributes[$attrName]))
continue;
$this->addAttribute($tag, $attrName);
}
$tag->template = $template;
$this->configurator->templateNormalizer->normalizeTag($tag);
$this->configurator->templateChecker->checkTag($tag);
return $this->configurator->tags->add($tagName, $tag);
}
protected function fixUnnamedCaptures($regexp)
{
$keys = [];
foreach ($this->references['anywhere'] as $key)
{
$capture = $this->captures[$key];
if (!$key || isset($capture['name']))
continue;
if (isset($this->references['asUrl'][$key]) || !isset($capture['passthrough']))
$keys[] = $key;
}
\rsort($keys);
foreach ($keys as $key)
{
$name = '_' . $key;
$pos = $this->captures[$key]['pos'];
$regexp = \substr_replace($regexp, "?'" . $name . "'", 2 + $pos, 0);
$this->captures[$key]['name'] = $name;
}
return $regexp;
}
protected function getPassthroughCapture()
{
$passthrough = 0;
foreach ($this->references['inText'] as $key)
{
if (!$this->isCatchAll($this->captures[$key]['expr']))
continue;
if ($passthrough)
{
$passthrough = 0;
break;
}
$passthrough = (int) $key;
}
return $passthrough;
}
protected function getRegexpInfo($regexp)
{
if (@\preg_match_all($regexp, '') === \false)
throw new InvalidArgumentException('Invalid regexp');
return RegexpParser::parse($regexp);
}
protected function isCatchAll($expr)
{
return (bool) \preg_match('(^\\.[*+]\\??$)D', $expr);
}
protected function parseRegexp($regexp)
{
$this->captures = [['name' => \null, 'expr' => \null]];
$regexpInfo = $this->getRegexpInfo($regexp);
$this->delimiter = $regexpInfo['delimiter'];
$this->modifiers = \str_replace('D', '', $regexpInfo['modifiers']);
foreach ($regexpInfo['tokens'] as $token)
{
if ($token['type'] !== 'capturingSubpatternStart')
continue;
$this->captures[] = [
'pos' => $token['pos'],
'name' => (isset($token['name'])) ? $token['name'] : \null,
'expr' => $token['content']
];
}
}
protected function parseTemplate($template)
{
$this->references = [
'anywhere' => [],
'asUrl' => [],
'inText' => []
];
\preg_match_all($this->referencesRegexp, $template, $matches);
foreach ($matches[0] as $match)
{
$key = \trim($match, '\\${}');
$this->references['anywhere'][$key] = $key;
}
$dom = TemplateHelper::loadTemplate($template);
$xpath = new DOMXPath($dom);
foreach ($xpath->query('//text()') as $node)
{
\preg_match_all($this->referencesRegexp, $node->textContent, $matches);
foreach ($matches[0] as $match)
{
$key = \trim($match, '\\${}');
$this->references['inText'][$key] = $key;
}
}
foreach (TemplateHelper::getURLNodes($dom) as $node)
if ($node instanceof DOMAttr
&& \preg_match('(^(?:[$\\\\]\\d+|\\$\\{\\d+\\}))', \trim($node->value), $m))
{
$key = \trim($m[0], '\\${}');
$this->references['asUrl'][$key] = $key;
}
$this->removeUnknownReferences();
}
protected function removeUnknownReferences()
{
foreach ($this->references as &$references)
$references = \array_intersect_key($references, $this->captures);
}
}

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