Initial commit
This commit is contained in:
commit
4639fd9dad
14 changed files with 544 additions and 0 deletions
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
|
@ -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
|
22
LICENSE
Normal file
22
LICENSE
Normal file
|
@ -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.
|
25
composer.json
Normal file
25
composer.json
Normal file
|
@ -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/"
|
||||
}
|
||||
}
|
||||
}
|
23
composer.lock
generated
Normal file
23
composer.lock
generated
Normal file
|
@ -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"
|
||||
}
|
93
lib/Scope/Data.php
Normal file
93
lib/Scope/Data.php
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
31
lib/Scope/Exception.php
Normal file
31
lib/Scope/Exception.php
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
9
lib/Scope/Matcher.php
Normal file
9
lib/Scope/Matcher.php
Normal file
|
@ -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 {}
|
11
lib/Scope/Matcher/CompositeMatcher.php
Normal file
11
lib/Scope/Matcher/CompositeMatcher.php
Normal file
|
@ -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) {}
|
||||
}
|
11
lib/Scope/Matcher/GroupMatcher.php
Normal file
11
lib/Scope/Matcher/GroupMatcher.php
Normal file
|
@ -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) {}
|
||||
}
|
11
lib/Scope/Matcher/NegateMatcher.php
Normal file
11
lib/Scope/Matcher/NegateMatcher.php
Normal file
|
@ -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) {}
|
||||
}
|
11
lib/Scope/Matcher/OrMatcher.php
Normal file
11
lib/Scope/Matcher/OrMatcher.php
Normal file
|
@ -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) {}
|
||||
}
|
11
lib/Scope/Matcher/PathMatcher.php
Normal file
11
lib/Scope/Matcher/PathMatcher.php
Normal file
|
@ -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) {}
|
||||
}
|
11
lib/Scope/Matcher/ScopeMatcher.php
Normal file
11
lib/Scope/Matcher/ScopeMatcher.php
Normal file
|
@ -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) {}
|
||||
}
|
256
lib/Scope/Parser.php
Normal file
256
lib/Scope/Parser.php
Normal file
|
@ -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 a new issue