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