Dustin Wilson
3 years ago
commit
4639fd9dad
14 changed files with 544 additions and 0 deletions
@ -0,0 +1,19 @@ |
|||||
|
# Temporary files and dependencies |
||||
|
test*.php |
||||
|
/vendor/ |
||||
|
|
||||
|
|
||||
|
# Windows files |
||||
|
Thumbs.db |
||||
|
ehthumbs.db |
||||
|
Desktop.ini |
||||
|
$RECYCLE.BIN/ |
||||
|
|
||||
|
|
||||
|
# macOS files |
||||
|
.DS_Store |
||||
|
.AppleDouble |
||||
|
.LSOverride |
||||
|
._* |
||||
|
.Spotlight-V100 |
||||
|
.Trashes |
@ -0,0 +1,22 @@ |
|||||
|
Copyright (c) 2021 Dustin Wilson |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person |
||||
|
obtaining a copy of this software and associated documentation |
||||
|
files (the "Software"), to deal in the Software without |
||||
|
restriction, including without limitation the rights to use, |
||||
|
copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
|
copies of the Software, and to permit persons to whom the |
||||
|
Software is furnished to do so, subject to the following |
||||
|
conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be |
||||
|
included in all copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES |
||||
|
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
||||
|
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
||||
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR |
||||
|
OTHER DEALINGS IN THE SOFTWARE. |
@ -0,0 +1,25 @@ |
|||||
|
{ |
||||
|
"name": "dw/highlighter", |
||||
|
"type": "library", |
||||
|
"description": "The clean and modern RSS server that doesn't give you any crap", |
||||
|
"license": "MIT", |
||||
|
"authors": [ |
||||
|
{ |
||||
|
"name": "Dustin Wilson", |
||||
|
"email": "dustin@dustinwilson.com", |
||||
|
"homepage": "https://dustinwilson.com/" |
||||
|
} |
||||
|
|
||||
|
], |
||||
|
"require": { |
||||
|
"php": "^7.4 || ^8.0", |
||||
|
"ext-intl": "*", |
||||
|
"ext-json": "*", |
||||
|
"ext-dom": "*" |
||||
|
}, |
||||
|
"autoload": { |
||||
|
"psr-4": { |
||||
|
"dW\\Highlighter\\": "lib/" |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,23 @@ |
|||||
|
{ |
||||
|
"_readme": [ |
||||
|
"This file locks the dependencies of your project to a known state", |
||||
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", |
||||
|
"This file is @generated automatically" |
||||
|
], |
||||
|
"content-hash": "cb22d0620bd4dc89e4d39d6eb9acaebe", |
||||
|
"packages": [], |
||||
|
"packages-dev": [], |
||||
|
"aliases": [], |
||||
|
"minimum-stability": "stable", |
||||
|
"stability-flags": [], |
||||
|
"prefer-stable": false, |
||||
|
"prefer-lowest": false, |
||||
|
"platform": { |
||||
|
"php": "^7.4 || ^8.0", |
||||
|
"ext-intl": "*", |
||||
|
"ext-json": "*", |
||||
|
"ext-dom": "*" |
||||
|
}, |
||||
|
"platform-dev": [], |
||||
|
"plugin-api-version": "2.0.0" |
||||
|
} |
@ -0,0 +1,93 @@ |
|||||
|
<?php |
||||
|
/** @license MIT |
||||
|
* Copyright 2021 Dustin Wilson et al. |
||||
|
* See LICENSE and AUTHORS files for details */ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace dW\Highlighter\Scope; |
||||
|
|
||||
|
class Data { |
||||
|
protected string $data; |
||||
|
|
||||
|
protected int $position = 0; |
||||
|
protected int $endPosition; |
||||
|
|
||||
|
public function __construct(string $data) { |
||||
|
$this->data = $data; |
||||
|
$this->endPosition = strlen($data) - 1; |
||||
|
} |
||||
|
|
||||
|
public function consume(int $length = 1): string|bool { |
||||
|
if ($this->position === $this->endPosition) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
$stop = $this->position + $length; |
||||
|
if ($stop >= $this->endPosition) { |
||||
|
$stop = $this->endPosition; |
||||
|
} |
||||
|
|
||||
|
$output = ''; |
||||
|
for ($i = $this->position; $i <= $stop; $i++) { |
||||
|
$output .= $this->data[$this->position++]; |
||||
|
} |
||||
|
|
||||
|
return $output; |
||||
|
} |
||||
|
|
||||
|
public function consumeIf(string $match): string|bool { |
||||
|
return $this->consumeWhile($match, 1); |
||||
|
} |
||||
|
|
||||
|
public function consumeUntil(string $match, $limit = null): string|bool { |
||||
|
if ($this->position === $this->endPosition) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
$length = strcspn($this->data, $match, $this->position + 1, $limit); |
||||
|
if ($length === 0) { |
||||
|
return ''; |
||||
|
} |
||||
|
|
||||
|
return $this->consume($length); |
||||
|
} |
||||
|
|
||||
|
public function consumeWhile(string $match, $limit = null): string|bool { |
||||
|
if ($this->position === $this->endPosition) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
$length = strspn($this->data, $match, $this->position + 1, $limit); |
||||
|
if ($length === 0) { |
||||
|
return ''; |
||||
|
} |
||||
|
|
||||
|
return $this->consume($length); |
||||
|
} |
||||
|
|
||||
|
public function current(): string|bool { |
||||
|
if ($this->position === $this->endPosition) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return $this->data[$this->position]; |
||||
|
} |
||||
|
|
||||
|
public function peek(int $length = 1): string|bool { |
||||
|
if ($this->position === $this->endPosition) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
$stop = $this->position + $length; |
||||
|
if ($stop >= $this->endPosition) { |
||||
|
$stop = $this->endPosition; |
||||
|
} |
||||
|
|
||||
|
$output = ''; |
||||
|
for ($i = $this->position; $i <= $stop; $i++) { |
||||
|
$output .= $this->data[$i]; |
||||
|
} |
||||
|
|
||||
|
return $output; |
||||
|
} |
||||
|
} |
@ -0,0 +1,31 @@ |
|||||
|
<?php |
||||
|
/** @license MIT |
||||
|
* Copyright 2021 Dustin Wilson et al. |
||||
|
* See LICENSE and AUTHORS files for details */ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace dW\Highlighter\Scope; |
||||
|
|
||||
|
class Exception extends \Exception { |
||||
|
const MESSAGE = '%s expected; found %s'; |
||||
|
|
||||
|
public function __construct(array|string $expected, string $found) { |
||||
|
if (is_array($expected)) { |
||||
|
$expected = array_map(function($n) { |
||||
|
return ($n !== false) ? "\"$n\"" : 'end of input'; |
||||
|
}, $expected); |
||||
|
|
||||
|
if (count($expected) > 2) { |
||||
|
$last = array_pop($expected); |
||||
|
$expected = implode(', ', $expected) . ', or ' . $last; |
||||
|
} else { |
||||
|
$expected = implode(' or ', $expected); |
||||
|
} |
||||
|
} else { |
||||
|
$expected = ($expected !== false) ? "\"$expected\"" : 'end of input'; |
||||
|
} |
||||
|
|
||||
|
$found = ($found !== false) ? "\"$found\"" : 'end of input'; |
||||
|
parent::__construct(sprintf(self::MESSAGE, $expected, $found), 2112); |
||||
|
} |
||||
|
} |
@ -0,0 +1,9 @@ |
|||||
|
<?php |
||||
|
/** @license MIT |
||||
|
* Copyright 2021 Dustin Wilson et al. |
||||
|
* See LICENSE and AUTHORS files for details */ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace dW\Highlighter\Scope; |
||||
|
|
||||
|
abstract class Matcher {} |
@ -0,0 +1,11 @@ |
|||||
|
<?php |
||||
|
/** @license MIT |
||||
|
* Copyright 2021 Dustin Wilson et al. |
||||
|
* See LICENSE and AUTHORS files for details */ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace dW\Highlighter\Scope\Matcher; |
||||
|
|
||||
|
class CompositeMatcher extends dW\Highlighter\Scope\Matcher { |
||||
|
public function __construct(Matcher $left, string $operator, Matcher $right) {} |
||||
|
} |
@ -0,0 +1,11 @@ |
|||||
|
<?php |
||||
|
/** @license MIT |
||||
|
* Copyright 2021 Dustin Wilson et al. |
||||
|
* See LICENSE and AUTHORS files for details */ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace dW\Highlighter\Scope\Matcher; |
||||
|
|
||||
|
class GroupMatcher extends dW\Highlighter\Scope\Matcher { |
||||
|
public function __construct(string $prefix, Matcher $selector) {} |
||||
|
} |
@ -0,0 +1,11 @@ |
|||||
|
<?php |
||||
|
/** @license MIT |
||||
|
* Copyright 2021 Dustin Wilson et al. |
||||
|
* See LICENSE and AUTHORS files for details */ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace dW\Highlighter\Scope\Matcher; |
||||
|
|
||||
|
class NegateMatcher extends dW\Highlighter\Scope\Matcher { |
||||
|
public function __construct(Matcher $groupOrPath) {} |
||||
|
} |
@ -0,0 +1,11 @@ |
|||||
|
<?php |
||||
|
/** @license MIT |
||||
|
* Copyright 2021 Dustin Wilson et al. |
||||
|
* See LICENSE and AUTHORS files for details */ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace dW\Highlighter\Scope\Matcher; |
||||
|
|
||||
|
class OrMatcher extends dW\Highlighter\Scope\Matcher { |
||||
|
public function __construct(Matcher $left, Matcher $right) {} |
||||
|
} |
@ -0,0 +1,11 @@ |
|||||
|
<?php |
||||
|
/** @license MIT |
||||
|
* Copyright 2021 Dustin Wilson et al. |
||||
|
* See LICENSE and AUTHORS files for details */ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace dW\Highlighter\Scope\Matcher; |
||||
|
|
||||
|
class PathMatcher extends dW\Highlighter\Scope\Matcher { |
||||
|
public function __construct(string $prefix, string $first, string $others) {} |
||||
|
} |
@ -0,0 +1,11 @@ |
|||||
|
<?php |
||||
|
/** @license MIT |
||||
|
* Copyright 2021 Dustin Wilson et al. |
||||
|
* See LICENSE and AUTHORS files for details */ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace dW\Highlighter\Scope\Matcher; |
||||
|
|
||||
|
class ScopeMatcher extends dW\Highlighter\Scope\Matcher { |
||||
|
public function __construct(string $first, string $others) {} |
||||
|
} |
@ -0,0 +1,256 @@ |
|||||
|
<?php |
||||
|
/** @license MIT |
||||
|
* Copyright 2021 Dustin Wilson et al. |
||||
|
* See LICENSE and AUTHORS files for details */ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace dW\Highlighter\Scope; |
||||
|
|
||||
|
class Parser { |
||||
|
protected Data $data; |
||||
|
protected static Parser $instance; |
||||
|
|
||||
|
protected function __construct(string $selector) { |
||||
|
$this->data = new Data($selector); |
||||
|
} |
||||
|
|
||||
|
public static function parse(string $selector): Matcher|bool { |
||||
|
self::$instance = new self($selector); |
||||
|
|
||||
|
$output = false; |
||||
|
$s1 = self::parseSpace(); |
||||
|
if ($s1 !== false) { |
||||
|
$s2 = self::parseSelector(); |
||||
|
if ($s2 !== false) { |
||||
|
$s3 = self::parseSpace(); |
||||
|
if ($s3 !== false) { |
||||
|
$output = $s2; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $output; |
||||
|
} |
||||
|
|
||||
|
protected static function parseComposite(): Matcher|bool { |
||||
|
$output = false; |
||||
|
|
||||
|
$s1 = self::parseExpression(); |
||||
|
if ($s1 !== false) { |
||||
|
$s2 = self::parseSpace(); |
||||
|
if ($s2 !== false) { |
||||
|
$s3 = self::$instance->data->consumeIf('|&-'); |
||||
|
if (!in_array($s3, [ '|', '&', '-' ])) { |
||||
|
throw new Exception([ '|', '&', '-' ], self::$instance->data->peek()); |
||||
|
} |
||||
|
|
||||
|
$s4 = self::parseSpace(); |
||||
|
if ($s4 !== false) { |
||||
|
$s5 = self::parseComposite(); |
||||
|
if ($s5 !== false) { |
||||
|
$output = new Matcher\CompositeMatcher($s1, $s3, $s5); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if ($output === false) { |
||||
|
$output = self::parseExpression(); |
||||
|
} |
||||
|
|
||||
|
return $output; |
||||
|
} |
||||
|
|
||||
|
protected static function parseExpression(): Matcher|bool { |
||||
|
$output = false; |
||||
|
$s1 = self::$instance->data->consumeIf('-'); |
||||
|
if ($s1 !== '-') { |
||||
|
throw new Exception('-', self::$instance->data->peek()); |
||||
|
} |
||||
|
|
||||
|
$s2 = self::parseSpace(); |
||||
|
if ($s2 !== false) { |
||||
|
$s3 = self::parseGroup(); |
||||
|
if ($s3 !== false) { |
||||
|
$s4 = self::parseSpace(); |
||||
|
if ($s4 !== false) { |
||||
|
$output = new Matcher\NegateMatcher($s3); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if ($output === false) { |
||||
|
$s1 = self::$instance->data->consumeIf('-', 1); |
||||
|
if ($s1 !== '-') { |
||||
|
throw new Exception('-', self::$instance->data->peek()); |
||||
|
} |
||||
|
|
||||
|
$s2 = self::parseSpace(); |
||||
|
if ($s2 !== false) { |
||||
|
$s3 = self::parsePath(); |
||||
|
if ($s3 !== false) { |
||||
|
$s4 = self::parseSpace(); |
||||
|
if ($s4 !== false) { |
||||
|
$output = new Matcher\NegateMatcher($s3); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if ($output === false) { |
||||
|
$output = self::parseGroup(); |
||||
|
if ($output === false) { |
||||
|
$output = self::parsePath(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $output; |
||||
|
} |
||||
|
|
||||
|
protected static function parseGroup(): Matcher|bool { |
||||
|
$output = false; |
||||
|
|
||||
|
$s2 = self::$instance->data->consumeIf('BLR'); |
||||
|
if (!in_array($s2, [ 'B', 'L', 'R' ])) { |
||||
|
throw new Exception([ 'B', 'L', 'R' ], self::$instance->data->peek()); |
||||
|
} |
||||
|
|
||||
|
$s3 = self::$instance->data->consumeIf(':'); |
||||
|
if ($s3 !== ':') { |
||||
|
throw new Exception(':', self::$instance->data->peek()); |
||||
|
} |
||||
|
|
||||
|
$prefix = "$s2$s3"; |
||||
|
|
||||
|
$s2 = self::$instance->data->consumeIf('('); |
||||
|
if ($s2 !== '(') { |
||||
|
throw new Exception('(', self::$instance->data->peek()); |
||||
|
} |
||||
|
|
||||
|
$s3 = self::parseSpace(); |
||||
|
if ($s3 !== false) { |
||||
|
$s4 = self::parseSelector(); |
||||
|
if ($s4 !== false) { |
||||
|
$s5 = self::parseSpace(); |
||||
|
if ($s5 !== false) { |
||||
|
$s6 = self::$instance->data->consumeIf(')'); |
||||
|
if ($s6 !== ')') { |
||||
|
throw new Exception(')', self::$instance->data->peek()); |
||||
|
} |
||||
|
|
||||
|
$output = new GroupMatcher($prefix, $s4); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $output; |
||||
|
} |
||||
|
|
||||
|
protected static function parsePath(): Matcher|bool { |
||||
|
$output = false; |
||||
|
|
||||
|
$s2 = self::$instance->data->consumeIf('BLR'); |
||||
|
if (!in_array($s2, [ 'B', 'L', 'R' ])) { |
||||
|
throw new Exception([ 'B', 'L', 'R' ], self::$instance->data->peek()); |
||||
|
} |
||||
|
|
||||
|
$s3 = self::$instance->data->consumeIf(':'); |
||||
|
if ($s3 !== ':') { |
||||
|
throw new Exception(':', self::$instance->data->peek()); |
||||
|
} |
||||
|
|
||||
|
$prefix = "$s2$s3"; |
||||
|
|
||||
|
$s2 = self::parseScope(); |
||||
|
if ($s2 !== false) { |
||||
|
$s3 = ''; |
||||
|
$s4 = ''; |
||||
|
|
||||
|
while ($s4 !== false) { |
||||
|
$s3 .= $s4; |
||||
|
$s4 = false; |
||||
|
|
||||
|
$s5 = self::parseSpace(); |
||||
|
if ($s5 !== false) { |
||||
|
$s6 = self::parseScope(); |
||||
|
if ($s6 !== false) { |
||||
|
$s4 = "$s5$s6"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (strlen($s3) > 0) { |
||||
|
$output = new Matcher\PathMatcher($prefix, $s2, $s3); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $output; |
||||
|
} |
||||
|
|
||||
|
protected static function parseSpace(): string|bool { |
||||
|
return self::$instance->data->consumeIf(" \t"); |
||||
|
} |
||||
|
|
||||
|
protected static function parseSegment(): string|bool { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
protected static function parseSelector(): Matcher|bool { |
||||
|
$output = false; |
||||
|
$s1 = self::parseComposite(); |
||||
|
if ($s1 !== false) { |
||||
|
$s2 = self::parseSpace(); |
||||
|
if ($s2 !== false) { |
||||
|
$s3 = self::$instance->data->consumeIf(','); |
||||
|
if ($s3 !== ',') { |
||||
|
throw new Exception(',', self::$instance->data->peek()); |
||||
|
} |
||||
|
|
||||
|
$s4 = self::parseSpace(); |
||||
|
if ($s4 !== false) { |
||||
|
$s5 = self::parseSelector(); |
||||
|
if ($s5 !== false) { |
||||
|
$output = new Matcher\OrMatcher($s1, $s5); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if ($output === false) { |
||||
|
$output = self::parseComposite(); |
||||
|
} |
||||
|
|
||||
|
return $output; |
||||
|
} |
||||
|
|
||||
|
protected static function parseScope(): string|bool { |
||||
|
$output = false; |
||||
|
|
||||
|
$s1 = self::parseSegment(); |
||||
|
if ($s1 !== false) { |
||||
|
$s2 = ''; |
||||
|
$s3 = ''; |
||||
|
|
||||
|
while ($s3 !== false) { |
||||
|
$s2 .= $s3; |
||||
|
$s3 = false; |
||||
|
|
||||
|
$s4 = self::$instance->data->consumeIf('.'); |
||||
|
if ($s4 !== '.') { |
||||
|
throw new Exception('.', self::$instance->data->peek()); |
||||
|
} |
||||
|
|
||||
|
$s5 = self::parseSegment(); |
||||
|
if ($s5 !== false) { |
||||
|
$s3 = "$s4$s5"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (strlen($s2) > 0) { |
||||
|
$output = new Matcher\ScopeMatcher($s1, $s2); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $output; |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue