mirror of
https://gitlab.com/signalytic/client-external/streamline/streamline-emr.git
synced 2026-09-12 11:11:31 +00:00
Build modified streamline images
The official image from streamline does not currently work for the arm64 platform. As a temporary measure, the source code and docker build scripts have been lifted from the official images and are used to build locally. Some additional modifications are made to reduce overall image size, these are documented in docker/README.md
This commit is contained in:
+45
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser\Handler;
|
||||
|
||||
use Symfony\Component\CssSelector\Parser\Reader;
|
||||
use Symfony\Component\CssSelector\Parser\TokenStream;
|
||||
|
||||
/**
|
||||
* CSS selector comment handler.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class CommentHandler implements HandlerInterface
|
||||
{
|
||||
public function handle(Reader $reader, TokenStream $stream): bool
|
||||
{
|
||||
if ('/*' !== $reader->getSubstring(2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$offset = $reader->getOffset('*/');
|
||||
|
||||
if (false === $offset) {
|
||||
$reader->moveToEnd();
|
||||
} else {
|
||||
$reader->moveForward($offset + 2);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser\Handler;
|
||||
|
||||
use Symfony\Component\CssSelector\Parser\Reader;
|
||||
use Symfony\Component\CssSelector\Parser\TokenStream;
|
||||
|
||||
/**
|
||||
* CSS selector handler interface.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface HandlerInterface
|
||||
{
|
||||
public function handle(Reader $reader, TokenStream $stream): bool;
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser\Handler;
|
||||
|
||||
use Symfony\Component\CssSelector\Parser\Reader;
|
||||
use Symfony\Component\CssSelector\Parser\Token;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
|
||||
use Symfony\Component\CssSelector\Parser\TokenStream;
|
||||
|
||||
/**
|
||||
* CSS selector comment handler.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class HashHandler implements HandlerInterface
|
||||
{
|
||||
private TokenizerPatterns $patterns;
|
||||
private TokenizerEscaping $escaping;
|
||||
|
||||
public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping)
|
||||
{
|
||||
$this->patterns = $patterns;
|
||||
$this->escaping = $escaping;
|
||||
}
|
||||
|
||||
public function handle(Reader $reader, TokenStream $stream): bool
|
||||
{
|
||||
$match = $reader->findPattern($this->patterns->getHashPattern());
|
||||
|
||||
if (!$match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$value = $this->escaping->escapeUnicode($match[1]);
|
||||
$stream->push(new Token(Token::TYPE_HASH, $value, $reader->getPosition()));
|
||||
$reader->moveForward(\strlen($match[0]));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser\Handler;
|
||||
|
||||
use Symfony\Component\CssSelector\Parser\Reader;
|
||||
use Symfony\Component\CssSelector\Parser\Token;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
|
||||
use Symfony\Component\CssSelector\Parser\TokenStream;
|
||||
|
||||
/**
|
||||
* CSS selector comment handler.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class IdentifierHandler implements HandlerInterface
|
||||
{
|
||||
private TokenizerPatterns $patterns;
|
||||
private TokenizerEscaping $escaping;
|
||||
|
||||
public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping)
|
||||
{
|
||||
$this->patterns = $patterns;
|
||||
$this->escaping = $escaping;
|
||||
}
|
||||
|
||||
public function handle(Reader $reader, TokenStream $stream): bool
|
||||
{
|
||||
$match = $reader->findPattern($this->patterns->getIdentifierPattern());
|
||||
|
||||
if (!$match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$value = $this->escaping->escapeUnicode($match[0]);
|
||||
$stream->push(new Token(Token::TYPE_IDENTIFIER, $value, $reader->getPosition()));
|
||||
$reader->moveForward(\strlen($match[0]));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser\Handler;
|
||||
|
||||
use Symfony\Component\CssSelector\Parser\Reader;
|
||||
use Symfony\Component\CssSelector\Parser\Token;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
|
||||
use Symfony\Component\CssSelector\Parser\TokenStream;
|
||||
|
||||
/**
|
||||
* CSS selector comment handler.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class NumberHandler implements HandlerInterface
|
||||
{
|
||||
private TokenizerPatterns $patterns;
|
||||
|
||||
public function __construct(TokenizerPatterns $patterns)
|
||||
{
|
||||
$this->patterns = $patterns;
|
||||
}
|
||||
|
||||
public function handle(Reader $reader, TokenStream $stream): bool
|
||||
{
|
||||
$match = $reader->findPattern($this->patterns->getNumberPattern());
|
||||
|
||||
if (!$match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$stream->push(new Token(Token::TYPE_NUMBER, $match[0], $reader->getPosition()));
|
||||
$reader->moveForward(\strlen($match[0]));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser\Handler;
|
||||
|
||||
use Symfony\Component\CssSelector\Exception\InternalErrorException;
|
||||
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
|
||||
use Symfony\Component\CssSelector\Parser\Reader;
|
||||
use Symfony\Component\CssSelector\Parser\Token;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
|
||||
use Symfony\Component\CssSelector\Parser\TokenStream;
|
||||
|
||||
/**
|
||||
* CSS selector comment handler.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class StringHandler implements HandlerInterface
|
||||
{
|
||||
private TokenizerPatterns $patterns;
|
||||
private TokenizerEscaping $escaping;
|
||||
|
||||
public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping)
|
||||
{
|
||||
$this->patterns = $patterns;
|
||||
$this->escaping = $escaping;
|
||||
}
|
||||
|
||||
public function handle(Reader $reader, TokenStream $stream): bool
|
||||
{
|
||||
$quote = $reader->getSubstring(1);
|
||||
|
||||
if (!\in_array($quote, ["'", '"'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$reader->moveForward(1);
|
||||
$match = $reader->findPattern($this->patterns->getQuotedStringPattern($quote));
|
||||
|
||||
if (!$match) {
|
||||
throw new InternalErrorException(sprintf('Should have found at least an empty match at %d.', $reader->getPosition()));
|
||||
}
|
||||
|
||||
// check unclosed strings
|
||||
if (\strlen($match[0]) === $reader->getRemainingLength()) {
|
||||
throw SyntaxErrorException::unclosedString($reader->getPosition() - 1);
|
||||
}
|
||||
|
||||
// check quotes pairs validity
|
||||
if ($quote !== $reader->getSubstring(1, \strlen($match[0]))) {
|
||||
throw SyntaxErrorException::unclosedString($reader->getPosition() - 1);
|
||||
}
|
||||
|
||||
$string = $this->escaping->escapeUnicodeAndNewLine($match[0]);
|
||||
$stream->push(new Token(Token::TYPE_STRING, $string, $reader->getPosition()));
|
||||
$reader->moveForward(\strlen($match[0]) + 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser\Handler;
|
||||
|
||||
use Symfony\Component\CssSelector\Parser\Reader;
|
||||
use Symfony\Component\CssSelector\Parser\Token;
|
||||
use Symfony\Component\CssSelector\Parser\TokenStream;
|
||||
|
||||
/**
|
||||
* CSS selector whitespace handler.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class WhitespaceHandler implements HandlerInterface
|
||||
{
|
||||
public function handle(Reader $reader, TokenStream $stream): bool
|
||||
{
|
||||
$match = $reader->findPattern('~^[ \t\r\n\f]+~');
|
||||
|
||||
if (false === $match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$stream->push(new Token(Token::TYPE_WHITESPACE, $match[0], $reader->getPosition()));
|
||||
$reader->moveForward(\strlen($match[0]));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser;
|
||||
|
||||
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
|
||||
use Symfony\Component\CssSelector\Node;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\Tokenizer;
|
||||
|
||||
/**
|
||||
* CSS selector parser.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class Parser implements ParserInterface
|
||||
{
|
||||
private Tokenizer $tokenizer;
|
||||
|
||||
public function __construct(?Tokenizer $tokenizer = null)
|
||||
{
|
||||
$this->tokenizer = $tokenizer ?? new Tokenizer();
|
||||
}
|
||||
|
||||
public function parse(string $source): array
|
||||
{
|
||||
$reader = new Reader($source);
|
||||
$stream = $this->tokenizer->tokenize($reader);
|
||||
|
||||
return $this->parseSelectorList($stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the arguments for ":nth-child()" and friends.
|
||||
*
|
||||
* @param Token[] $tokens
|
||||
*
|
||||
* @throws SyntaxErrorException
|
||||
*/
|
||||
public static function parseSeries(array $tokens): array
|
||||
{
|
||||
foreach ($tokens as $token) {
|
||||
if ($token->isString()) {
|
||||
throw SyntaxErrorException::stringAsFunctionArgument();
|
||||
}
|
||||
}
|
||||
|
||||
$joined = trim(implode('', array_map(fn (Token $token) => $token->getValue(), $tokens)));
|
||||
|
||||
$int = function ($string) {
|
||||
if (!is_numeric($string)) {
|
||||
throw SyntaxErrorException::stringAsFunctionArgument();
|
||||
}
|
||||
|
||||
return (int) $string;
|
||||
};
|
||||
|
||||
switch (true) {
|
||||
case 'odd' === $joined:
|
||||
return [2, 1];
|
||||
case 'even' === $joined:
|
||||
return [2, 0];
|
||||
case 'n' === $joined:
|
||||
return [1, 0];
|
||||
case !str_contains($joined, 'n'):
|
||||
return [0, $int($joined)];
|
||||
}
|
||||
|
||||
$split = explode('n', $joined);
|
||||
$first = $split[0] ?? null;
|
||||
|
||||
return [
|
||||
$first ? ('-' === $first || '+' === $first ? $int($first.'1') : $int($first)) : 1,
|
||||
isset($split[1]) && $split[1] ? $int($split[1]) : 0,
|
||||
];
|
||||
}
|
||||
|
||||
private function parseSelectorList(TokenStream $stream): array
|
||||
{
|
||||
$stream->skipWhitespace();
|
||||
$selectors = [];
|
||||
|
||||
while (true) {
|
||||
$selectors[] = $this->parserSelectorNode($stream);
|
||||
|
||||
if ($stream->getPeek()->isDelimiter([','])) {
|
||||
$stream->getNext();
|
||||
$stream->skipWhitespace();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $selectors;
|
||||
}
|
||||
|
||||
private function parserSelectorNode(TokenStream $stream): Node\SelectorNode
|
||||
{
|
||||
[$result, $pseudoElement] = $this->parseSimpleSelector($stream);
|
||||
|
||||
while (true) {
|
||||
$stream->skipWhitespace();
|
||||
$peek = $stream->getPeek();
|
||||
|
||||
if ($peek->isFileEnd() || $peek->isDelimiter([','])) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (null !== $pseudoElement) {
|
||||
throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector');
|
||||
}
|
||||
|
||||
if ($peek->isDelimiter(['+', '>', '~'])) {
|
||||
$combinator = $stream->getNext()->getValue();
|
||||
$stream->skipWhitespace();
|
||||
} else {
|
||||
$combinator = ' ';
|
||||
}
|
||||
|
||||
[$nextSelector, $pseudoElement] = $this->parseSimpleSelector($stream);
|
||||
$result = new Node\CombinedSelectorNode($result, $combinator, $nextSelector);
|
||||
}
|
||||
|
||||
return new Node\SelectorNode($result, $pseudoElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses next simple node (hash, class, pseudo, negation).
|
||||
*
|
||||
* @throws SyntaxErrorException
|
||||
*/
|
||||
private function parseSimpleSelector(TokenStream $stream, bool $insideNegation = false): array
|
||||
{
|
||||
$stream->skipWhitespace();
|
||||
|
||||
$selectorStart = \count($stream->getUsed());
|
||||
$result = $this->parseElementNode($stream);
|
||||
$pseudoElement = null;
|
||||
|
||||
while (true) {
|
||||
$peek = $stream->getPeek();
|
||||
if ($peek->isWhitespace()
|
||||
|| $peek->isFileEnd()
|
||||
|| $peek->isDelimiter([',', '+', '>', '~'])
|
||||
|| ($insideNegation && $peek->isDelimiter([')']))
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (null !== $pseudoElement) {
|
||||
throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector');
|
||||
}
|
||||
|
||||
if ($peek->isHash()) {
|
||||
$result = new Node\HashNode($result, $stream->getNext()->getValue());
|
||||
} elseif ($peek->isDelimiter(['.'])) {
|
||||
$stream->getNext();
|
||||
$result = new Node\ClassNode($result, $stream->getNextIdentifier());
|
||||
} elseif ($peek->isDelimiter(['['])) {
|
||||
$stream->getNext();
|
||||
$result = $this->parseAttributeNode($result, $stream);
|
||||
} elseif ($peek->isDelimiter([':'])) {
|
||||
$stream->getNext();
|
||||
|
||||
if ($stream->getPeek()->isDelimiter([':'])) {
|
||||
$stream->getNext();
|
||||
$pseudoElement = $stream->getNextIdentifier();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$identifier = $stream->getNextIdentifier();
|
||||
if (\in_array(strtolower($identifier), ['first-line', 'first-letter', 'before', 'after'])) {
|
||||
// Special case: CSS 2.1 pseudo-elements can have a single ':'.
|
||||
// Any new pseudo-element must have two.
|
||||
$pseudoElement = $identifier;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$stream->getPeek()->isDelimiter(['('])) {
|
||||
$result = new Node\PseudoNode($result, $identifier);
|
||||
if ('Pseudo[Element[*]:scope]' === $result->__toString()) {
|
||||
$used = \count($stream->getUsed());
|
||||
if (!(2 === $used
|
||||
|| 3 === $used && $stream->getUsed()[0]->isWhiteSpace()
|
||||
|| $used >= 3 && $stream->getUsed()[$used - 3]->isDelimiter([','])
|
||||
|| $used >= 4
|
||||
&& $stream->getUsed()[$used - 3]->isWhiteSpace()
|
||||
&& $stream->getUsed()[$used - 4]->isDelimiter([','])
|
||||
)) {
|
||||
throw SyntaxErrorException::notAtTheStartOfASelector('scope');
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$stream->getNext();
|
||||
$stream->skipWhitespace();
|
||||
|
||||
if ('not' === strtolower($identifier)) {
|
||||
if ($insideNegation) {
|
||||
throw SyntaxErrorException::nestedNot();
|
||||
}
|
||||
|
||||
[$argument, $argumentPseudoElement] = $this->parseSimpleSelector($stream, true);
|
||||
$next = $stream->getNext();
|
||||
|
||||
if (null !== $argumentPseudoElement) {
|
||||
throw SyntaxErrorException::pseudoElementFound($argumentPseudoElement, 'inside ::not()');
|
||||
}
|
||||
|
||||
if (!$next->isDelimiter([')'])) {
|
||||
throw SyntaxErrorException::unexpectedToken('")"', $next);
|
||||
}
|
||||
|
||||
$result = new Node\NegationNode($result, $argument);
|
||||
} else {
|
||||
$arguments = [];
|
||||
$next = null;
|
||||
|
||||
while (true) {
|
||||
$stream->skipWhitespace();
|
||||
$next = $stream->getNext();
|
||||
|
||||
if ($next->isIdentifier()
|
||||
|| $next->isString()
|
||||
|| $next->isNumber()
|
||||
|| $next->isDelimiter(['+', '-'])
|
||||
) {
|
||||
$arguments[] = $next;
|
||||
} elseif ($next->isDelimiter([')'])) {
|
||||
break;
|
||||
} else {
|
||||
throw SyntaxErrorException::unexpectedToken('an argument', $next);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$arguments) {
|
||||
throw SyntaxErrorException::unexpectedToken('at least one argument', $next);
|
||||
}
|
||||
|
||||
$result = new Node\FunctionNode($result, $identifier, $arguments);
|
||||
}
|
||||
} else {
|
||||
throw SyntaxErrorException::unexpectedToken('selector', $peek);
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($stream->getUsed()) === $selectorStart) {
|
||||
throw SyntaxErrorException::unexpectedToken('selector', $stream->getPeek());
|
||||
}
|
||||
|
||||
return [$result, $pseudoElement];
|
||||
}
|
||||
|
||||
private function parseElementNode(TokenStream $stream): Node\ElementNode
|
||||
{
|
||||
$peek = $stream->getPeek();
|
||||
|
||||
if ($peek->isIdentifier() || $peek->isDelimiter(['*'])) {
|
||||
if ($peek->isIdentifier()) {
|
||||
$namespace = $stream->getNext()->getValue();
|
||||
} else {
|
||||
$stream->getNext();
|
||||
$namespace = null;
|
||||
}
|
||||
|
||||
if ($stream->getPeek()->isDelimiter(['|'])) {
|
||||
$stream->getNext();
|
||||
$element = $stream->getNextIdentifierOrStar();
|
||||
} else {
|
||||
$element = $namespace;
|
||||
$namespace = null;
|
||||
}
|
||||
} else {
|
||||
$element = $namespace = null;
|
||||
}
|
||||
|
||||
return new Node\ElementNode($namespace, $element);
|
||||
}
|
||||
|
||||
private function parseAttributeNode(Node\NodeInterface $selector, TokenStream $stream): Node\AttributeNode
|
||||
{
|
||||
$stream->skipWhitespace();
|
||||
$attribute = $stream->getNextIdentifierOrStar();
|
||||
|
||||
if (null === $attribute && !$stream->getPeek()->isDelimiter(['|'])) {
|
||||
throw SyntaxErrorException::unexpectedToken('"|"', $stream->getPeek());
|
||||
}
|
||||
|
||||
if ($stream->getPeek()->isDelimiter(['|'])) {
|
||||
$stream->getNext();
|
||||
|
||||
if ($stream->getPeek()->isDelimiter(['='])) {
|
||||
$namespace = null;
|
||||
$stream->getNext();
|
||||
$operator = '|=';
|
||||
} else {
|
||||
$namespace = $attribute;
|
||||
$attribute = $stream->getNextIdentifier();
|
||||
$operator = null;
|
||||
}
|
||||
} else {
|
||||
$namespace = $operator = null;
|
||||
}
|
||||
|
||||
if (null === $operator) {
|
||||
$stream->skipWhitespace();
|
||||
$next = $stream->getNext();
|
||||
|
||||
if ($next->isDelimiter([']'])) {
|
||||
return new Node\AttributeNode($selector, $namespace, $attribute, 'exists', null);
|
||||
} elseif ($next->isDelimiter(['='])) {
|
||||
$operator = '=';
|
||||
} elseif ($next->isDelimiter(['^', '$', '*', '~', '|', '!'])
|
||||
&& $stream->getPeek()->isDelimiter(['='])
|
||||
) {
|
||||
$operator = $next->getValue().'=';
|
||||
$stream->getNext();
|
||||
} else {
|
||||
throw SyntaxErrorException::unexpectedToken('operator', $next);
|
||||
}
|
||||
}
|
||||
|
||||
$stream->skipWhitespace();
|
||||
$value = $stream->getNext();
|
||||
|
||||
if ($value->isNumber()) {
|
||||
// if the value is a number, it's casted into a string
|
||||
$value = new Token(Token::TYPE_STRING, (string) $value->getValue(), $value->getPosition());
|
||||
}
|
||||
|
||||
if (!($value->isIdentifier() || $value->isString())) {
|
||||
throw SyntaxErrorException::unexpectedToken('string or identifier', $value);
|
||||
}
|
||||
|
||||
$stream->skipWhitespace();
|
||||
$next = $stream->getNext();
|
||||
|
||||
if (!$next->isDelimiter([']'])) {
|
||||
throw SyntaxErrorException::unexpectedToken('"]"', $next);
|
||||
}
|
||||
|
||||
return new Node\AttributeNode($selector, $namespace, $attribute, $operator, $value->getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser;
|
||||
|
||||
use Symfony\Component\CssSelector\Node\SelectorNode;
|
||||
|
||||
/**
|
||||
* CSS selector parser interface.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface ParserInterface
|
||||
{
|
||||
/**
|
||||
* Parses given selector source into an array of tokens.
|
||||
*
|
||||
* @return SelectorNode[]
|
||||
*/
|
||||
public function parse(string $source): array;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser;
|
||||
|
||||
/**
|
||||
* CSS selector reader.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class Reader
|
||||
{
|
||||
private string $source;
|
||||
private int $length;
|
||||
private int $position = 0;
|
||||
|
||||
public function __construct(string $source)
|
||||
{
|
||||
$this->source = $source;
|
||||
$this->length = \strlen($source);
|
||||
}
|
||||
|
||||
public function isEOF(): bool
|
||||
{
|
||||
return $this->position >= $this->length;
|
||||
}
|
||||
|
||||
public function getPosition(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function getRemainingLength(): int
|
||||
{
|
||||
return $this->length - $this->position;
|
||||
}
|
||||
|
||||
public function getSubstring(int $length, int $offset = 0): string
|
||||
{
|
||||
return substr($this->source, $this->position + $offset, $length);
|
||||
}
|
||||
|
||||
public function getOffset(string $string): int|false
|
||||
{
|
||||
$position = strpos($this->source, $string, $this->position);
|
||||
|
||||
return false === $position ? false : $position - $this->position;
|
||||
}
|
||||
|
||||
public function findPattern(string $pattern): array|false
|
||||
{
|
||||
$source = substr($this->source, $this->position);
|
||||
|
||||
if (preg_match($pattern, $source, $matches)) {
|
||||
return $matches;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function moveForward(int $length): void
|
||||
{
|
||||
$this->position += $length;
|
||||
}
|
||||
|
||||
public function moveToEnd(): void
|
||||
{
|
||||
$this->position = $this->length;
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser\Shortcut;
|
||||
|
||||
use Symfony\Component\CssSelector\Node\ClassNode;
|
||||
use Symfony\Component\CssSelector\Node\ElementNode;
|
||||
use Symfony\Component\CssSelector\Node\SelectorNode;
|
||||
use Symfony\Component\CssSelector\Parser\ParserInterface;
|
||||
|
||||
/**
|
||||
* CSS selector class parser shortcut.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ClassParser implements ParserInterface
|
||||
{
|
||||
public function parse(string $source): array
|
||||
{
|
||||
// Matches an optional namespace, optional element, and required class
|
||||
// $source = 'test|input.ab6bd_field';
|
||||
// $matches = array (size=4)
|
||||
// 0 => string 'test|input.ab6bd_field' (length=22)
|
||||
// 1 => string 'test' (length=4)
|
||||
// 2 => string 'input' (length=5)
|
||||
// 3 => string 'ab6bd_field' (length=11)
|
||||
if (preg_match('/^(?:([a-z]++)\|)?+([\w-]++|\*)?+\.([\w-]++)$/i', trim($source), $matches)) {
|
||||
return [
|
||||
new SelectorNode(new ClassNode(new ElementNode($matches[1] ?: null, $matches[2] ?: null), $matches[3])),
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser\Shortcut;
|
||||
|
||||
use Symfony\Component\CssSelector\Node\ElementNode;
|
||||
use Symfony\Component\CssSelector\Node\SelectorNode;
|
||||
use Symfony\Component\CssSelector\Parser\ParserInterface;
|
||||
|
||||
/**
|
||||
* CSS selector element parser shortcut.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ElementParser implements ParserInterface
|
||||
{
|
||||
public function parse(string $source): array
|
||||
{
|
||||
// Matches an optional namespace, required element or `*`
|
||||
// $source = 'testns|testel';
|
||||
// $matches = array (size=3)
|
||||
// 0 => string 'testns|testel' (length=13)
|
||||
// 1 => string 'testns' (length=6)
|
||||
// 2 => string 'testel' (length=6)
|
||||
if (preg_match('/^(?:([a-z]++)\|)?([\w-]++|\*)$/i', trim($source), $matches)) {
|
||||
return [new SelectorNode(new ElementNode($matches[1] ?: null, $matches[2]))];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser\Shortcut;
|
||||
|
||||
use Symfony\Component\CssSelector\Node\ElementNode;
|
||||
use Symfony\Component\CssSelector\Node\SelectorNode;
|
||||
use Symfony\Component\CssSelector\Parser\ParserInterface;
|
||||
|
||||
/**
|
||||
* CSS selector class parser shortcut.
|
||||
*
|
||||
* This shortcut ensure compatibility with previous version.
|
||||
* - The parser fails to parse an empty string.
|
||||
* - In the previous version, an empty string matches each tags.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class EmptyStringParser implements ParserInterface
|
||||
{
|
||||
public function parse(string $source): array
|
||||
{
|
||||
// Matches an empty string
|
||||
if ('' == $source) {
|
||||
return [new SelectorNode(new ElementNode(null, '*'))];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser\Shortcut;
|
||||
|
||||
use Symfony\Component\CssSelector\Node\ElementNode;
|
||||
use Symfony\Component\CssSelector\Node\HashNode;
|
||||
use Symfony\Component\CssSelector\Node\SelectorNode;
|
||||
use Symfony\Component\CssSelector\Parser\ParserInterface;
|
||||
|
||||
/**
|
||||
* CSS selector hash parser shortcut.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class HashParser implements ParserInterface
|
||||
{
|
||||
public function parse(string $source): array
|
||||
{
|
||||
// Matches an optional namespace, optional element, and required id
|
||||
// $source = 'test|input#ab6bd_field';
|
||||
// $matches = array (size=4)
|
||||
// 0 => string 'test|input#ab6bd_field' (length=22)
|
||||
// 1 => string 'test' (length=4)
|
||||
// 2 => string 'input' (length=5)
|
||||
// 3 => string 'ab6bd_field' (length=11)
|
||||
if (preg_match('/^(?:([a-z]++)\|)?+([\w-]++|\*)?+#([\w-]++)$/i', trim($source), $matches)) {
|
||||
return [
|
||||
new SelectorNode(new HashNode(new ElementNode($matches[1] ?: null, $matches[2] ?: null), $matches[3])),
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser;
|
||||
|
||||
/**
|
||||
* CSS selector token.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class Token
|
||||
{
|
||||
public const TYPE_FILE_END = 'eof';
|
||||
public const TYPE_DELIMITER = 'delimiter';
|
||||
public const TYPE_WHITESPACE = 'whitespace';
|
||||
public const TYPE_IDENTIFIER = 'identifier';
|
||||
public const TYPE_HASH = 'hash';
|
||||
public const TYPE_NUMBER = 'number';
|
||||
public const TYPE_STRING = 'string';
|
||||
|
||||
private ?string $type;
|
||||
private ?string $value;
|
||||
private ?int $position;
|
||||
|
||||
public function __construct(?string $type, ?string $value, ?int $position)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->value = $value;
|
||||
$this->position = $position;
|
||||
}
|
||||
|
||||
public function getType(): ?int
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getValue(): ?string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getPosition(): ?int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function isFileEnd(): bool
|
||||
{
|
||||
return self::TYPE_FILE_END === $this->type;
|
||||
}
|
||||
|
||||
public function isDelimiter(array $values = []): bool
|
||||
{
|
||||
if (self::TYPE_DELIMITER !== $this->type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$values) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return \in_array($this->value, $values);
|
||||
}
|
||||
|
||||
public function isWhitespace(): bool
|
||||
{
|
||||
return self::TYPE_WHITESPACE === $this->type;
|
||||
}
|
||||
|
||||
public function isIdentifier(): bool
|
||||
{
|
||||
return self::TYPE_IDENTIFIER === $this->type;
|
||||
}
|
||||
|
||||
public function isHash(): bool
|
||||
{
|
||||
return self::TYPE_HASH === $this->type;
|
||||
}
|
||||
|
||||
public function isNumber(): bool
|
||||
{
|
||||
return self::TYPE_NUMBER === $this->type;
|
||||
}
|
||||
|
||||
public function isString(): bool
|
||||
{
|
||||
return self::TYPE_STRING === $this->type;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
if ($this->value) {
|
||||
return sprintf('<%s "%s" at %s>', $this->type, $this->value, $this->position);
|
||||
}
|
||||
|
||||
return sprintf('<%s at %s>', $this->type, $this->position);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser;
|
||||
|
||||
use Symfony\Component\CssSelector\Exception\InternalErrorException;
|
||||
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
|
||||
|
||||
/**
|
||||
* CSS selector token stream.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class TokenStream
|
||||
{
|
||||
/**
|
||||
* @var Token[]
|
||||
*/
|
||||
private array $tokens = [];
|
||||
|
||||
/**
|
||||
* @var Token[]
|
||||
*/
|
||||
private array $used = [];
|
||||
|
||||
private int $cursor = 0;
|
||||
private ?Token $peeked;
|
||||
private bool $peeking = false;
|
||||
|
||||
/**
|
||||
* Pushes a token.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function push(Token $token): static
|
||||
{
|
||||
$this->tokens[] = $token;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Freezes stream.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function freeze(): static
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next token.
|
||||
*
|
||||
* @throws InternalErrorException If there is no more token
|
||||
*/
|
||||
public function getNext(): Token
|
||||
{
|
||||
if ($this->peeking) {
|
||||
$this->peeking = false;
|
||||
$this->used[] = $this->peeked;
|
||||
|
||||
return $this->peeked;
|
||||
}
|
||||
|
||||
if (!isset($this->tokens[$this->cursor])) {
|
||||
throw new InternalErrorException('Unexpected token stream end.');
|
||||
}
|
||||
|
||||
return $this->tokens[$this->cursor++];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns peeked token.
|
||||
*/
|
||||
public function getPeek(): Token
|
||||
{
|
||||
if (!$this->peeking) {
|
||||
$this->peeked = $this->getNext();
|
||||
$this->peeking = true;
|
||||
}
|
||||
|
||||
return $this->peeked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns used tokens.
|
||||
*
|
||||
* @return Token[]
|
||||
*/
|
||||
public function getUsed(): array
|
||||
{
|
||||
return $this->used;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next identifier token.
|
||||
*
|
||||
* @throws SyntaxErrorException If next token is not an identifier
|
||||
*/
|
||||
public function getNextIdentifier(): string
|
||||
{
|
||||
$next = $this->getNext();
|
||||
|
||||
if (!$next->isIdentifier()) {
|
||||
throw SyntaxErrorException::unexpectedToken('identifier', $next);
|
||||
}
|
||||
|
||||
return $next->getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next identifier or null if star delimiter token is found.
|
||||
*
|
||||
* @throws SyntaxErrorException If next token is not an identifier or a star delimiter
|
||||
*/
|
||||
public function getNextIdentifierOrStar(): ?string
|
||||
{
|
||||
$next = $this->getNext();
|
||||
|
||||
if ($next->isIdentifier()) {
|
||||
return $next->getValue();
|
||||
}
|
||||
|
||||
if ($next->isDelimiter(['*'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw SyntaxErrorException::unexpectedToken('identifier or "*"', $next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips next whitespace if any.
|
||||
*/
|
||||
public function skipWhitespace(): void
|
||||
{
|
||||
$peek = $this->getPeek();
|
||||
|
||||
if ($peek->isWhitespace()) {
|
||||
$this->getNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser\Tokenizer;
|
||||
|
||||
use Symfony\Component\CssSelector\Parser\Handler;
|
||||
use Symfony\Component\CssSelector\Parser\Reader;
|
||||
use Symfony\Component\CssSelector\Parser\Token;
|
||||
use Symfony\Component\CssSelector\Parser\TokenStream;
|
||||
|
||||
/**
|
||||
* CSS selector tokenizer.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class Tokenizer
|
||||
{
|
||||
/**
|
||||
* @var Handler\HandlerInterface[]
|
||||
*/
|
||||
private array $handlers;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$patterns = new TokenizerPatterns();
|
||||
$escaping = new TokenizerEscaping($patterns);
|
||||
|
||||
$this->handlers = [
|
||||
new Handler\WhitespaceHandler(),
|
||||
new Handler\IdentifierHandler($patterns, $escaping),
|
||||
new Handler\HashHandler($patterns, $escaping),
|
||||
new Handler\StringHandler($patterns, $escaping),
|
||||
new Handler\NumberHandler($patterns),
|
||||
new Handler\CommentHandler(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenize selector source code.
|
||||
*/
|
||||
public function tokenize(Reader $reader): TokenStream
|
||||
{
|
||||
$stream = new TokenStream();
|
||||
|
||||
while (!$reader->isEOF()) {
|
||||
foreach ($this->handlers as $handler) {
|
||||
if ($handler->handle($reader, $stream)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
$stream->push(new Token(Token::TYPE_DELIMITER, $reader->getSubstring(1), $reader->getPosition()));
|
||||
$reader->moveForward(1);
|
||||
}
|
||||
|
||||
return $stream
|
||||
->push(new Token(Token::TYPE_FILE_END, null, $reader->getPosition()))
|
||||
->freeze();
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser\Tokenizer;
|
||||
|
||||
/**
|
||||
* CSS selector tokenizer escaping applier.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class TokenizerEscaping
|
||||
{
|
||||
private TokenizerPatterns $patterns;
|
||||
|
||||
public function __construct(TokenizerPatterns $patterns)
|
||||
{
|
||||
$this->patterns = $patterns;
|
||||
}
|
||||
|
||||
public function escapeUnicode(string $value): string
|
||||
{
|
||||
$value = $this->replaceUnicodeSequences($value);
|
||||
|
||||
return preg_replace($this->patterns->getSimpleEscapePattern(), '$1', $value);
|
||||
}
|
||||
|
||||
public function escapeUnicodeAndNewLine(string $value): string
|
||||
{
|
||||
$value = preg_replace($this->patterns->getNewLineEscapePattern(), '', $value);
|
||||
|
||||
return $this->escapeUnicode($value);
|
||||
}
|
||||
|
||||
private function replaceUnicodeSequences(string $value): string
|
||||
{
|
||||
return preg_replace_callback($this->patterns->getUnicodeEscapePattern(), function ($match) {
|
||||
$c = hexdec($match[1]);
|
||||
|
||||
if (0x80 > $c %= 0x200000) {
|
||||
return \chr($c);
|
||||
}
|
||||
if (0x800 > $c) {
|
||||
return \chr(0xC0 | $c >> 6).\chr(0x80 | $c & 0x3F);
|
||||
}
|
||||
if (0x10000 > $c) {
|
||||
return \chr(0xE0 | $c >> 12).\chr(0x80 | $c >> 6 & 0x3F).\chr(0x80 | $c & 0x3F);
|
||||
}
|
||||
|
||||
return '';
|
||||
}, $value);
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser\Tokenizer;
|
||||
|
||||
/**
|
||||
* CSS selector tokenizer patterns builder.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class TokenizerPatterns
|
||||
{
|
||||
private string $unicodeEscapePattern;
|
||||
private string $simpleEscapePattern;
|
||||
private string $newLineEscapePattern;
|
||||
private string $escapePattern;
|
||||
private string $stringEscapePattern;
|
||||
private string $nonAsciiPattern;
|
||||
private string $nmCharPattern;
|
||||
private string $nmStartPattern;
|
||||
private string $identifierPattern;
|
||||
private string $hashPattern;
|
||||
private string $numberPattern;
|
||||
private string $quotedStringPattern;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->unicodeEscapePattern = '\\\\([0-9a-f]{1,6})(?:\r\n|[ \n\r\t\f])?';
|
||||
$this->simpleEscapePattern = '\\\\(.)';
|
||||
$this->newLineEscapePattern = '\\\\(?:\n|\r\n|\r|\f)';
|
||||
$this->escapePattern = $this->unicodeEscapePattern.'|\\\\[^\n\r\f0-9a-f]';
|
||||
$this->stringEscapePattern = $this->newLineEscapePattern.'|'.$this->escapePattern;
|
||||
$this->nonAsciiPattern = '[^\x00-\x7F]';
|
||||
$this->nmCharPattern = '[_a-z0-9-]|'.$this->escapePattern.'|'.$this->nonAsciiPattern;
|
||||
$this->nmStartPattern = '[_a-z]|'.$this->escapePattern.'|'.$this->nonAsciiPattern;
|
||||
$this->identifierPattern = '-?(?:'.$this->nmStartPattern.')(?:'.$this->nmCharPattern.')*';
|
||||
$this->hashPattern = '#((?:'.$this->nmCharPattern.')+)';
|
||||
$this->numberPattern = '[+-]?(?:[0-9]*\.[0-9]+|[0-9]+)';
|
||||
$this->quotedStringPattern = '([^\n\r\f\\\\%s]|'.$this->stringEscapePattern.')*';
|
||||
}
|
||||
|
||||
public function getNewLineEscapePattern(): string
|
||||
{
|
||||
return '~'.$this->newLineEscapePattern.'~';
|
||||
}
|
||||
|
||||
public function getSimpleEscapePattern(): string
|
||||
{
|
||||
return '~'.$this->simpleEscapePattern.'~';
|
||||
}
|
||||
|
||||
public function getUnicodeEscapePattern(): string
|
||||
{
|
||||
return '~'.$this->unicodeEscapePattern.'~i';
|
||||
}
|
||||
|
||||
public function getIdentifierPattern(): string
|
||||
{
|
||||
return '~^'.$this->identifierPattern.'~i';
|
||||
}
|
||||
|
||||
public function getHashPattern(): string
|
||||
{
|
||||
return '~^'.$this->hashPattern.'~i';
|
||||
}
|
||||
|
||||
public function getNumberPattern(): string
|
||||
{
|
||||
return '~^'.$this->numberPattern.'~';
|
||||
}
|
||||
|
||||
public function getQuotedStringPattern(string $quote): string
|
||||
{
|
||||
return '~^'.sprintf($this->quotedStringPattern, $quote).'~i';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user