Browse Source

Starting rewriting scope parsing

main
Dustin Wilson 3 years ago
parent
commit
7e918f48be
  1. 10
      lib/FauxReadOnly.php
  2. 28
      lib/Scope/Composite.php
  3. 4
      lib/Scope/Data.php
  4. 2
      lib/Scope/Exception.php
  5. 41
      lib/Scope/Expression.php
  6. 37
      lib/Scope/Filter.php
  7. 34
      lib/Scope/Group.php
  8. 37
      lib/Scope/Matchers/AndMatcher.php
  9. 27
      lib/Scope/Matchers/GroupMatcher.php
  10. 23
      lib/Scope/Matchers/NegateMatcher.php
  11. 40
      lib/Scope/Matchers/OrMatcher.php
  12. 37
      lib/Scope/Matchers/PathMatcher.php
  13. 39
      lib/Scope/Matchers/ScopeMatcher.php
  14. 7
      lib/Scope/Node.php
  15. 183
      lib/Scope/Parser.php
  16. 56
      lib/Scope/Path.php
  17. 30
      lib/Scope/Scope.php
  18. 28
      lib/Scope/Selector.php

10
lib/FauxReadOnly.php

@ -8,11 +8,13 @@ namespace dW\Lit;
trait FauxReadOnly {
public function __get(string $name) {
if ($name[0] === '_') {
return;
$prop = "_$name";
if (!property_exists($this, $prop)) {
$trace = debug_backtrace();
trigger_error("Cannot get undefined property $name in {$trace[0]['file']} on line {$trace[0]['line']}", E_USER_NOTICE);
return null;
}
$prop = "_$name";
return $this->$prop;
return (!is_array($this->$prop)) ? $this->$prop : clone $this->prop;
}
}

28
lib/Scope/Composite.php

@ -0,0 +1,28 @@
<?php
/** @license MIT
* Copyright 2021 Dustin Wilson et al.
* See LICENSE file for details */
declare(strict_types=1);
namespace dW\Lit\Scope;
class Composite extends Node {
protected array $expressions = [];
protected bool $frozen = false;
public function __construct(Selector $parent) {
$this->_parent = \WeakReference::create($parent);
}
public function add(Expression ...$expressions): bool {
if ($this->frozen) {
return false;
}
$this->expressions = $expressions;
$this->frozen = true;
return true;
}
}

4
lib/Scope/Data.php

@ -17,7 +17,7 @@ class Data {
protected int $endPosition;
public function __construct(string $data) {
preg_match_all('/[BLR]:|[A-Za-z0-9-+_\*\.]+|[\,\|\-\(\)&]/', $data, $matches, PREG_OFFSET_CAPTURE);
preg_match_all('/[BLR]:|\.|[A-Za-z0-9-+_\*]+|[\,\|\(\)&\^\$\>]/', $data, $matches, PREG_OFFSET_CAPTURE);
$this->data = $matches[0] ?? [];
$this->endPosition = count($this->data) - 1;
}
@ -37,7 +37,7 @@ class Data {
return false;
}
return $this->data[$this->_position][1];
return $this->data[$this->_position + 1][1];
}
/** Returns the next token without moving the pointer */

2
lib/Scope/Exception.php

@ -16,7 +16,7 @@ class Exception extends \Exception {
$expected = ($expected[0] !== false) ? $expected[0] : 'end of input';
} else {
$temp = [];
for ($i = 0; $i < $strlen; $i++) {
for ($i = 0; $i < $expectedLen; $i++) {
$temp[] = ($expected[$i] !== false) ? "{$expected[$i]}" : 'end of input';
}
$expected = $temp;

41
lib/Scope/Expression.php

@ -0,0 +1,41 @@
<?php
/** @license MIT
* Copyright 2021 Dustin Wilson et al.
* See LICENSE file for details */
declare(strict_types=1);
namespace dW\Lit\Scope;
class Expression extends Node {
protected Filter|Group|Path $_child;
protected bool $frozen = false;
protected bool $_negate = false;
protected ?string $_operator;
public function __construct(Composite $parent, ?string $operator = null) {
$this->_operator = $operator;
$this->_parent = \WeakReference::create($parent);
if ($operator === '-') {
$this->negate = true;
}
}
public function __set(string $name, $value) {
if ($name !== 'child') {
$trace = debug_backtrace();
trigger_error("Cannot set undefined property $name in {$trace[0]['file']} on line {$trace[0]['line']}", E_USER_NOTICE);
}
if ($this->frozen) {
$trace = debug_backtrace();
trigger_error("Cannot set readonly $name property in {$trace[0]['file']} on line {$trace[0]['line']}", E_USER_NOTICE);
return;
}
$this->frozen = true;
$this->_child = $value;
}
}

37
lib/Scope/Filter.php

@ -0,0 +1,37 @@
<?php
/** @license MIT
* Copyright 2021 Dustin Wilson et al.
* See LICENSE file for details */
declare(strict_types=1);
namespace dW\Lit\Scope;
class Filter extends Node {
protected Group|Path $_child;
protected bool $frozen = false;
protected string $_operator;
protected string $_side;
public function __construct(Selector $parent, string $side) {
$this->_parent = \WeakReference::create($parent);
$this->_side = $side;
}
public function __set(string $name, $value) {
if ($name !== 'child') {
$trace = debug_backtrace();
trigger_error("Cannot set undefined property $name in {$trace[0]['file']} on line {$trace[0]['line']}", E_USER_NOTICE);
}
if ($this->frozen) {
$trace = debug_backtrace();
trigger_error("Cannot set readonly $name property in {$trace[0]['file']} on line {$trace[0]['line']}", E_USER_NOTICE);
return;
}
$this->frozen = true;
$this->_child = $value;
}
}

34
lib/Scope/Group.php

@ -0,0 +1,34 @@
<?php
/** @license MIT
* Copyright 2021 Dustin Wilson et al.
* See LICENSE file for details */
declare(strict_types=1);
namespace dW\Lit\Scope;
class Group extends Node {
protected Selector $_child;
protected bool $frozen = false;
public function __construct(Expression $parent) {
$this->_parent = \WeakReference::create($parent);
}
public function __set(string $name, $value) {
if ($name !== 'child') {
$trace = debug_backtrace();
trigger_error("Cannot set undefined property $name in {$trace[0]['file']} on line {$trace[0]['line']}", E_USER_NOTICE);
}
if ($this->frozen) {
$trace = debug_backtrace();
trigger_error("Cannot set readonly $name property in {$trace[0]['file']} on line {$trace[0]['line']}", E_USER_NOTICE);
return;
}
$this->frozen = true;
$this->_child = $value;
}
}

37
lib/Scope/Matchers/AndMatcher.php

@ -1,37 +0,0 @@
<?php
/** @license MIT
* Copyright 2021 Dustin Wilson et al.
* See LICENSE file for details */
declare(strict_types=1);
namespace dW\Lit\Scope;
class AndMatcher extends Matcher {
protected array $matchers = [];
public function __construct(Matcher ...$matchers) {
$this->matchers = $matchers;
}
public function add(Matcher $matcher) {
$this->matchers[] = $matcher;
}
public function matches(string ...$scopes): bool {
foreach ($this->matchers as $m) {
if (!$m->matches(...$scopes)) {
return false;
}
}
return true;
}
public function getPrefix(string ...$scopes): string|null|false {
if ($this->matches(...$scopes)) {
return $this->matches[0]->getPrefix(...$scopes);
}
return null;
}
}

27
lib/Scope/Matchers/GroupMatcher.php

@ -1,27 +0,0 @@
<?php
/** @license MIT
* Copyright 2021 Dustin Wilson et al.
* See LICENSE file for details */
declare(strict_types=1);
namespace dW\Lit\Scope;
class GroupMatcher extends Matcher {
protected ?string $prefix;
protected Matcher $selector;
public function __construct(?string $prefix, Matcher $selector) {
$this->prefix = ($prefix !== null) ? $prefix[0] : null;
$this->selector = $selector;
}
public function matches(string ...$scopes): bool {
return $this->selector->matches(...$scopes);
}
public function getPrefix(string ...$scopes): string|null|false {
if ($this->selector->matches(...$scopes)) {
return $this->prefix;
}
}
}

23
lib/Scope/Matchers/NegateMatcher.php

@ -1,23 +0,0 @@
<?php
/** @license MIT
* Copyright 2021 Dustin Wilson et al.
* See LICENSE file for details */
declare(strict_types=1);
namespace dW\Lit\Scope;
class NegateMatcher extends Matcher {
protected Matcher $matcher;
public function __construct(Matcher $matcher) {
$this->matcher = $matcher;
}
public function matches(string ...$scopes): bool {
return !($this->matcher->matches(...$scopes));
}
public function getPrefix(string ...$scopes) {
return null;
}
}

40
lib/Scope/Matchers/OrMatcher.php

@ -1,40 +0,0 @@
<?php
/** @license MIT
* Copyright 2021 Dustin Wilson et al.
* See LICENSE file for details */
declare(strict_types=1);
namespace dW\Lit\Scope;
class OrMatcher extends Matcher {
protected array $matchers = [];
public function __construct(Matcher ...$matchers) {
$this->matchers = $matchers;
}
public function add(Matcher $matcher) {
$this->matchers[] = $matcher;
}
public function matches(string ...$scopes): bool {
foreach ($this->matchers as $m) {
if ($m->matches(...$scopes)) {
return true;
}
}
return false;
}
public function getPrefix(string ...$scopes): string|null|false {
foreach ($this->matchers as $m) {
$prefix = $m->getPrefix(...$scopes);
if ($prefix !== null) {
return $prefix;
}
}
return null;
}
}

37
lib/Scope/Matchers/PathMatcher.php

@ -1,37 +0,0 @@
<?php
/** @license MIT
* Copyright 2021 Dustin Wilson et al.
* See LICENSE file for details */
declare(strict_types=1);
namespace dW\Lit\Scope;
class PathMatcher extends Matcher {
protected ?string $prefix;
protected array $matchers;
public function __construct(?string $prefix, ScopeMatcher ...$matchers) {
$this->prefix = ($prefix !== null) ? $prefix[0] : null;
$this->matchers = $matchers;
}
public function matches(string ...$scopes): bool {
$count = 0;
$matcher = $this->matchers[$count];
foreach ($scopes as $scope) {
if ($matcher->matches($scope)) {
$matcher = $this->matchers[++$count] ?? null;
}
if ($matcher === null) {
return true;
}
}
return false;
}
public function getPrefix(string ...$scopes): string|null|false {
if ($this->matches($scopes)) {
return $this->prefix;
}
}
}

39
lib/Scope/Matchers/ScopeMatcher.php

@ -1,39 +0,0 @@
<?php
/** @license MIT
* Copyright 2021 Dustin Wilson et al.
* See LICENSE file for details */
declare(strict_types=1);
namespace dW\Lit\Scope;
class ScopeMatcher extends Matcher {
protected array $segments;
public function __construct(string ...$segments) {
$this->segments = $segments;
}
public function matches(string $scope): bool {
$scopeSegments = explode('.', $scope);
if (count($this->segments) !== count($scopeSegments)) {
return false;
}
foreach ($this->segments as $index => $segment) {
if ($segment === '*') {
continue;
}
if ($segment !== $scopeSegments[$index]) {
return false;
}
}
return true;
}
public function getPrefix(string $scope) {
return null;
}
}

7
lib/Scope/Matcher.php → lib/Scope/Node.php

@ -5,5 +5,10 @@
declare(strict_types=1);
namespace dW\Lit\Scope;
use dW\Lit\FauxReadOnly;
abstract class Matcher {}
class Node {
use FauxReadOnly;
protected ?\WeakReference $_parent;
}

183
lib/Scope/Parser.php

@ -6,9 +6,9 @@
declare(strict_types=1);
namespace dW\Lit\Scope;
/** Parses scope strings into a matcher tree */
/** Parses strings into a scope selector */
class Parser {
// Used to cache parsed scopes
// Used to cache parsed selectors
protected static array $cache = [];
// When true prints out detailed data about the construction of the matcher
@ -25,56 +25,40 @@ class Parser {
protected static Parser $instance;
// strspn mask used to check whether a token could be a valid scope.
protected const SCOPE_MASK = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-+_.*';
protected const SCOPE_MASK = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-+_*';
protected function __construct(string $selector) {
$this->data = new Data($selector);
}
/**
* Static method entry point for the class. Creates the instance and parses the
* string.
*/
public static function parse(string $selector): Matcher|false {
if (isset(self::$cache[$selector])) {
return self::$cache[$selector];
/** Static method entry point for the class. Parses the string. */
public static function parse(string $string): Selector {
if (isset(self::$cache[$string])) {
return self::$cache[$string];
}
self::$instance = new self($selector);
self::$instance = new self($string);
$result = self::parseSelector();
self::$cache[$selector] = $result;
self::$cache[$string] = $result;
return $result;
}
protected static function parseComposite(): Matcher {
protected static function parseComposite(?Selector $parent = null): Matcher {
if (self::$debug) {
self::debug();
}
$result = self::parseExpression();
$result = new Composite($parent);
$expressions = [ self::parseExpression($result) ];
$peek = self::$instance->data->peek();
while (in_array($peek, [ '|', '&', '-' ])) {
$token = self::$instance->data->consume();
$new = self::parseExpression();
switch ($token) {
case '|':
$result = ($result instanceof OrMatcher) ? $result->add($new) : new OrMatcher($result, $new);
break;
case '-':
$new = new NegateMatcher($new);
case '&':
$result = ($result instanceof AndMatcher) ? $result->add($new) : new AndMatcher($result, $new);
break;
}
$peek = self::$instance->data->peek();
while ($peek = self::$instance->data->peek() && in_array($peek, [ '&', '|', '-' ])) {
$expressions[] = self::parseExpression($result, self::$instance->data->consume());
}
$result->add(...$expressions);
if (self::$debug) {
self::debugResult($result);
}
@ -82,27 +66,40 @@ class Parser {
return $result;
}
protected static function parseExpression(): Matcher {
protected static function parseExpression(Composite $parent, ?string $operator = null): Expression {
if (self::$debug) {
self::debug();
}
$result = new Expression($parent, $operator);
$peek = self::$instance->data->peek();
$prefix = null;
if ($peek !== false && in_array($peek[0], [ 'B', 'L', 'R' ]) && $peek[1] === ':') {
$prefix = $peek[0];
self::$instance->data->consume();
$peek = self::$instance->data->peek();
if (in_array($peek[0], [ 'B', 'L', 'R' ])) {
$result->child = self::parseFilter($result, self::$instance->data->consume()[0]);
} elseif ($peek === '(') {
$result->child = self::parseGroup($result);
} else {
$result->child = self::parsePath($result);
}
if (self::$debug) {
self::debugResult($result);
}
return $result;
}
protected static function parseFilter(Expression $parent, string $side): Filter {
if (self::$debug) {
self::debug();
}
$result = new Filter($parent, $side);
$peek = self::$instance->data->peek();
if ($peek === '(') {
self::$instance->data->consume();
$result = self::parseGroup($prefix);
} elseif (!in_array($peek, [ '-', false ]) && strspn($peek, self::SCOPE_MASK) === strlen($peek)) {
$result = self::parsePath($prefix);
$result->child = self::parseGroup($result);
} else {
// TODO: Take the effort to make this more descriptive
self::throw([ 'Group', 'Path', 'Scope' ], $peek);
$result->child = self::parsePath($result);
}
if (self::$debug) {
@ -112,19 +109,27 @@ class Parser {
return $result;
}
protected static function parseGroup(?string $prefix = null): Matcher {
protected static function parseGroup(Expression $parent): Group {
if (self::$debug) {
self::debug();
}
$result = self::parseSelector();
$result = new Group($parent);
$token = self::$instance->data->consume();
if ($token !== '(') {
self::throw('"("', $token);
}
if (!$group->child = self::parseSelector($result)) {
return false;
}
$token = self::$instance->data->consume();
if ($token !== ')') {
self::throw('")"', $token);
}
$result = ($prefix === null) ? $result : new GroupMatcher($prefix, $result);
if (self::$debug) {
self::debugResult($result);
}
@ -132,21 +137,44 @@ class Parser {
return $result;
}
protected static function parsePath(?string $prefix = null): PathMatcher|ScopeMatcher {
protected static function parsePath(Expression $parent): Path {
if (self::$debug) {
self::debug();
}
$result = [];
$result[] = self::parseScope();
$result = new Path($parent);
$peek = self::$instance->data->peek();
while (!in_array($peek, [ '-', false ]) && strspn($peek, self::SCOPE_MASK) === strlen($peek)) {
$result[] = self::parseScope();
$peek = self::$instance->data->peek();
$anchorStart = false;
if (self::$instance->data->peek() === '^') {
$anchorStart = true;
self::$instance->data->consume();
}
$first = self::parseScope($result);
if ($first->anchorToPrevious) {
self::throw('first scope', '>');
}
$scopes = [ $first ];
while ($peek = self::$instance->data->peek() && strspn($peek, self::SCOPE_MASK) === strlen($peek)) {
$scopes[] = self::parseScope($result);
}
$result = ($prefix !== null || count($result) > 1) ? new PathMatcher($prefix, ...$result) : $result[0];
$result->add(...$scopes);
$anchorEnd = false;
if (self::$instance->data->peek() === '$') {
$anchorEnd = true;
self::$instance->data->consume();
}
if ($anchorStart && $anchorEnd) {
$result->anchor = Path::ANCHOR_BOTH;
} elseif ($anchorStart) {
$result->anchor = Path::ANCHOR_START;
} else {
$result->anchor = Path::ANCHOR_END;
}
if (self::$debug) {
self::debugResult($result);
@ -155,22 +183,20 @@ class Parser {
return $result;
}
protected static function parseSelector(): Matcher {
protected static function parseSelector(?Group $parent = null): Matcher {
if (self::$debug) {
self::debug();
}
$result = [];
$result[] = self::parseComposite();
$result = new Selector($parent);
$peek = self::$instance->data->peek();
while ($peek === ',') {
$composites = [ self::parseComposite($result) ];
while ($peek = self::$instance->data->peek() && $peek === ',') {
self::$instance->data->consume();
$result[] = self::parseComposite();
$peek = self::$instance->data->peek();
$composites[] = self::parseComposite($result);
}
$result = (count($result) > 1) ? new OrMatcher(...$result) : $result[0];
$result->add(...$composites);
if (self::$debug) {
self::debugResult($result);
@ -179,18 +205,35 @@ class Parser {
return $result;
}
protected static function parseScope(): ScopeMatcher {
protected static function parseScope(Path $parent): Scope {
if (self::$debug) {
self::debug();
}
$token = self::$instance->data->consume();
if ($token === false || !preg_match('/^(?:[A-Za-z0-9-_]+|\*)(?:\.(?:[A-Za-z0-9-+_]+|\*))*$/S', $token)) {
// TODO: Take the effort to make this more descriptive
self::throw('valid scope syntax', $token);
$peek = self::$instance->data->peek();
if ($peek === '>') {
self::$instance->data->consume();
}
$result = new ScopeMatcher(...explode('.', $token));
$result = new Scope($parent, ($peek === '>'));
$atoms = [];
$first = true;
do {
if (!$first) {
// Consume the period
self::$instance->data->consume();
}
$peek = self::$instance->data->peek();
if (strspn($peek, self::SCOPE_MASK) !== strlen($peek)) {
self::throw([ 'A-Z', 'a-z', '0-9', '-', '+', '_', '*' ], $peek);
}
$atoms[] = self::$instance->data->consume();
$first = false;
} while (self::$instance->data->peek() === '.');
$result->add(...$atoms);
if (self::$debug) {
self::debugResult($result);

56
lib/Scope/Path.php

@ -0,0 +1,56 @@
<?php
/** @license MIT
* Copyright 2021 Dustin Wilson et al.
* See LICENSE file for details */
declare(strict_types=1);
namespace dW\Lit\Scope;
class Path extends Node {
protected int $_anchor;
protected array $frozen = [
'add' => false,
'anchor' => false
];
protected array $scopes = [];
const ANCHOR_NONE = 0;
const ANCHOR_START = 1;
const ANCHOR_END = 2;
const ANCHOR_BOTH = 3;
public function __construct(Expression $parent) {
$this->_parent = \WeakReference::create($parent);
}
public function add(Scope ...$scopes): bool {
if ($this->frozen['add']) {
return false;
}
$this->scopes = $scopes;
$this->frozen['add'] = true;
return true;
}
public function __set(string $name, $value) {
if ($name !== 'anchor') {
$trace = debug_backtrace();
trigger_error("Cannot set undefined property $name in {$trace[0]['file']} on line {$trace[0]['line']}", E_USER_NOTICE);
}
if ($this->frozen['anchor']) {
$trace = debug_backtrace();
trigger_error("Cannot set readonly $name property in {$trace[0]['file']} on line {$trace[0]['line']}", E_USER_NOTICE);
return;
}
$this->frozen['anchor'] = true;
$this->_anchor = $value;
}
}

30
lib/Scope/Scope.php

@ -0,0 +1,30 @@
<?php
/** @license MIT
* Copyright 2021 Dustin Wilson et al.
* See LICENSE file for details */
declare(strict_types=1);
namespace dW\Lit\Scope;
class Scope extends Node {
protected bool $_anchorToPrevious;
protected array $atoms = [];
protected bool $frozen = false;
public function __construct(Path $parent, bool $anchorToPrevious = false) {
$this->_anchorToPrevious = $anchorToPrevious;
$this->_parent = \WeakReference::create($parent);
}
public function add(string ...$atoms): bool {
if ($this->frozen) {
return false;
}
$this->atoms = $atoms;
$this->frozen = true;
return true;
}
}

28
lib/Scope/Selector.php

@ -0,0 +1,28 @@
<?php
/** @license MIT
* Copyright 2021 Dustin Wilson et al.
* See LICENSE file for details */
declare(strict_types=1);
namespace dW\Lit\Scope;
class Selector extends Node {
protected array $composites = [];
protected bool $frozen = false;
public function __construct(?Group $parent = null) {
$this->_parent = ($parent === null) ? null : \WeakReference::create($parent);
}
public function add(Composite ...$composites): bool {
if ($this->frozen) {
return false;
}
$this->composites = $this->composites;
$this->frozen = true;
return true;
}
}
Loading…
Cancel
Save