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. 177
      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 { trait FauxReadOnly {
public function __get(string $name) { public function __get(string $name) {
if ($name[0] === '_') { $prop = "_$name";
return; 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 (!is_array($this->$prop)) ? $this->$prop : clone $this->prop;
return $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; protected int $endPosition;
public function __construct(string $data) { 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->data = $matches[0] ?? [];
$this->endPosition = count($this->data) - 1; $this->endPosition = count($this->data) - 1;
} }
@ -37,7 +37,7 @@ class Data {
return false; return false;
} }
return $this->data[$this->_position][1]; return $this->data[$this->_position + 1][1];
} }
/** Returns the next token without moving the pointer */ /** 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'; $expected = ($expected[0] !== false) ? $expected[0] : 'end of input';
} else { } else {
$temp = []; $temp = [];
for ($i = 0; $i < $strlen; $i++) { for ($i = 0; $i < $expectedLen; $i++) {
$temp[] = ($expected[$i] !== false) ? "{$expected[$i]}" : 'end of input'; $temp[] = ($expected[$i] !== false) ? "{$expected[$i]}" : 'end of input';
} }
$expected = $temp; $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); declare(strict_types=1);
namespace dW\Lit\Scope; namespace dW\Lit\Scope;
use dW\Lit\FauxReadOnly;
abstract class Matcher {} class Node {
use FauxReadOnly;
protected ?\WeakReference $_parent;
}

177
lib/Scope/Parser.php

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