From 4639fd9dadc5a0cf26b74148de3a49122fd09a14 Mon Sep 17 00:00:00 2001 From: Dustin Wilson Date: Mon, 31 May 2021 23:04:08 -0500 Subject: [PATCH] Initial commit --- .gitignore | 19 ++ LICENSE | 22 +++ composer.json | 25 +++ composer.lock | 23 +++ lib/Scope/Data.php | 93 +++++++++ lib/Scope/Exception.php | 31 +++ lib/Scope/Matcher.php | 9 + lib/Scope/Matcher/CompositeMatcher.php | 11 ++ lib/Scope/Matcher/GroupMatcher.php | 11 ++ lib/Scope/Matcher/NegateMatcher.php | 11 ++ lib/Scope/Matcher/OrMatcher.php | 11 ++ lib/Scope/Matcher/PathMatcher.php | 11 ++ lib/Scope/Matcher/ScopeMatcher.php | 11 ++ lib/Scope/Parser.php | 256 +++++++++++++++++++++++++ 14 files changed, 544 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 lib/Scope/Data.php create mode 100644 lib/Scope/Exception.php create mode 100644 lib/Scope/Matcher.php create mode 100644 lib/Scope/Matcher/CompositeMatcher.php create mode 100644 lib/Scope/Matcher/GroupMatcher.php create mode 100644 lib/Scope/Matcher/NegateMatcher.php create mode 100644 lib/Scope/Matcher/OrMatcher.php create mode 100644 lib/Scope/Matcher/PathMatcher.php create mode 100644 lib/Scope/Matcher/ScopeMatcher.php create mode 100644 lib/Scope/Parser.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..927bf50 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3572224 --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6867a62 --- /dev/null +++ b/composer.json @@ -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/" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..641c3a0 --- /dev/null +++ b/composer.lock @@ -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" +} diff --git a/lib/Scope/Data.php b/lib/Scope/Data.php new file mode 100644 index 0000000..245c84c --- /dev/null +++ b/lib/Scope/Data.php @@ -0,0 +1,93 @@ +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; + } +} diff --git a/lib/Scope/Exception.php b/lib/Scope/Exception.php new file mode 100644 index 0000000..23690a1 --- /dev/null +++ b/lib/Scope/Exception.php @@ -0,0 +1,31 @@ + 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); + } +} diff --git a/lib/Scope/Matcher.php b/lib/Scope/Matcher.php new file mode 100644 index 0000000..de2d285 --- /dev/null +++ b/lib/Scope/Matcher.php @@ -0,0 +1,9 @@ +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; + } +}