Browse Source

Maybe have scope selector matchers working

main
Dustin Wilson 3 years ago
parent
commit
379d9c791d
  1. 22
      lib/Scope/Composite.php
  2. 22
      lib/Scope/Expression.php
  3. 26
      lib/Scope/Filter.php
  4. 26
      lib/Scope/Group.php
  5. 2
      lib/Scope/Node.php
  6. 115
      lib/Scope/Parser.php
  7. 2
      lib/Scope/ParserException.php
  8. 62
      lib/Scope/Path.php
  9. 34
      lib/Scope/Scope.php
  10. 28
      lib/Scope/Selector.php

22
lib/Scope/Composite.php

@ -8,25 +8,14 @@ namespace dW\Lit\Scope;
class Composite extends Node { class Composite extends Node {
protected array $_expressions = []; protected array $_expressions = [];
protected bool $frozen = false;
public function __construct(Selector $parent) { public function __construct(Expression ...$expressions) {
$this->_parent = \WeakReference::create($parent);
}
public function add(Expression ...$expressions): bool {
if ($this->frozen) {
return false;
}
$this->_expressions = $expressions; $this->_expressions = $expressions;
$this->frozen = true;
return true;
} }
public function matches(Path $path): bool {
public function matches(array $scopes): bool {
$result = false; $result = false;
foreach ($this->_expressions as $expression) { foreach ($this->_expressions as $expression) {
$operator = $expression->operator; $operator = $expression->operator;
@ -38,10 +27,7 @@ class Composite extends Node {
continue; continue;
} }
$local = $expression->child->matches($path); $local = $expression->matches($scopes);
if ($expression->negate) {
$local = !$local;
}
switch ($operator) { switch ($operator) {
case Expression::OPERATOR_NONE: $result = $local; case Expression::OPERATOR_NONE: $result = $local;

22
lib/Scope/Expression.php

@ -8,7 +8,6 @@ namespace dW\Lit\Scope;
class Expression extends Node { class Expression extends Node {
protected Filter|Group|Path $_child; protected Filter|Group|Path $_child;
protected bool $frozen = false;
protected bool $_negate = false; protected bool $_negate = false;
protected int $_operator; protected int $_operator;
@ -18,27 +17,16 @@ class Expression extends Node {
const OPERATOR_NOT = 3; const OPERATOR_NOT = 3;
public function __construct(Composite $parent, int $operator = self::OPERATOR_NONE, bool $negate = false) { public function __construct(Filter|Group|Path $child, int $operator = self::OPERATOR_NONE, bool $negate = false) {
$this->_child = $child;
$this->_negate = $negate; $this->_negate = $negate;
$this->_operator = $operator; $this->_operator = $operator;
$this->_parent = \WeakReference::create($parent);
} }
public function __set(string $name, $value) { public function matches(array $scopes): bool {
if ($name !== 'child') { $matches = $this->_child->matches($scopes);
$trace = debug_backtrace(); return ($this->_negate) ? !$matches : $matches;
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;
} }

26
lib/Scope/Filter.php

@ -8,7 +8,6 @@ namespace dW\Lit\Scope;
class Filter extends Node { class Filter extends Node {
protected Group|Path $_child; protected Group|Path $_child;
protected bool $frozen = false;
protected int $_prefix; protected int $_prefix;
const SIDE_LEFT = 0; const SIDE_LEFT = 0;
@ -16,8 +15,8 @@ class Filter extends Node {
const SIDE_BOTH = 2; const SIDE_BOTH = 2;
public function __construct(Expression $parent, string $prefix) { public function __construct(Group|Path $child, string $prefix) {
$this->_parent = \WeakReference::create($parent); $this->_child = $child;
switch ($prefix) { switch ($prefix) {
case 'L': $this->_prefix = self::SIDE_LEFT; case 'L': $this->_prefix = self::SIDE_LEFT;
@ -30,27 +29,10 @@ class Filter extends Node {
} }
public function matches(Path $path): bool { public function matches(array $scopes): bool {
// No idea if prefixes are supposed to affect matches. Appears to in the // No idea if prefixes are supposed to affect matches. Appears to in the
// TextMate original but not in Atom's implementation... // TextMate original but not in Atom's implementation...
return $this->_child->matches($path); return $this->_child->matches($scopes);
}
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;
} }

26
lib/Scope/Group.php

@ -8,33 +8,15 @@ namespace dW\Lit\Scope;
class Group extends Node { class Group extends Node {
protected Selector $_child; protected Selector $_child;
protected bool $frozen = false;
public function __construct(Expression|Filter $parent) { public function __construct(Selector $child) {
$this->_parent = \WeakReference::create($parent); $this->_child = $child;
} }
public function matches(Path $path): bool { public function matches(array $scopes): bool {
return $this->_child->matches($path); return $this->_child->matches($scopes);
}
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;
} }

2
lib/Scope/Node.php

@ -9,6 +9,4 @@ use dW\Lit\FauxReadOnly;
class Node { class Node {
use FauxReadOnly; use FauxReadOnly;
protected ?\WeakReference $_parent;
} }

115
lib/Scope/Parser.php

@ -8,8 +8,11 @@ namespace dW\Lit\Scope;
/** Parses strings into a scope selector */ /** Parses strings into a scope selector */
class Parser { class Parser {
// Used to cache parsed selectors // Used to cache parsed scopes and selectors
protected static array $cache = []; protected static array $cache = [
'selector' => [],
'scope' => []
];
// The tokenized scope string // The tokenized scope string
protected Data $data; protected Data $data;
@ -30,8 +33,8 @@ class Parser {
/** Parses strings into Selectors */ /** Parses strings into Selectors */
public static function parseSelector(string $string): Selector { public static function parseSelector(string $string): Selector {
if (isset(self::$cache[$string])) { if (isset(self::$cache['selector'][$string])) {
return self::$cache[$string]; return self::$cache['selector'][$string];
} }
self::$instance = new self($string); self::$instance = new self($string);
@ -43,18 +46,18 @@ class Parser {
self::throw(false, $token); self::throw(false, $token);
} }
self::$cache[$string] = $result; self::$cache['selector'][$string] = $result;
return $result; return $result;
} }
/** Parses strings into Selectors */ /** Parses strings into Scopes */
public static function parsePath(string $string): Path { public static function parseScope(string $string): Scope {
if (isset(self::$cache[$string])) { if (isset(self::$cache['scope'][$string])) {
return self::$cache[$string]; return self::$cache['scope'][$string];
} }
self::$instance = new self($string); self::$instance = new self($string);
$result = self::_parsePath(); $result = self::_parseScope();
// If not at the end of input throw an exception. // If not at the end of input throw an exception.
$token = self::$instance->data->consume(); $token = self::$instance->data->consume();
@ -62,16 +65,15 @@ class Parser {
self::throw(false, $token); self::throw(false, $token);
} }
self::$cache[$string] = $result; self::$cache['scope'][$string] = $result;
return $result; return $result;
} }
protected static function parseComposite(?Selector $parent = null): Composite { protected static function parseComposite(): Composite {
assert((fn() => self::debug())()); assert((fn() => self::debug())());
$result = new Composite($parent); $expressions = [ self::parseExpression() ];
$expressions = [ self::parseExpression($result) ];
$peek = self::$instance->data->peek(); $peek = self::$instance->data->peek();
while ($peek !== false && in_array($peek, [ '&', '|', '-' ])) { while ($peek !== false && in_array($peek, [ '&', '|', '-' ])) {
@ -85,18 +87,16 @@ class Parser {
break; break;
} }
$expressions[] = self::parseExpression($result, $operator); $expressions[] = self::parseExpression($operator);
$peek = self::$instance->data->peek(); $peek = self::$instance->data->peek();
} }
$result->add(...$expressions); $result = new Composite(...$expressions);
assert((fn() => self::debugResult($result))()); assert((fn() => self::debugResult($result))());
return $result; return $result;
} }
protected static function parseExpression(Composite $parent, int $operator = Expression::OPERATOR_NONE): Expression { protected static function parseExpression(int $operator = Expression::OPERATOR_NONE): Expression {
assert((fn() => self::debug())()); assert((fn() => self::debug())());
$peek = self::$instance->data->peek(); $peek = self::$instance->data->peek();
@ -107,71 +107,64 @@ class Parser {
$negate = true; $negate = true;
} }
$result = new Expression($parent, $operator, $negate);
if (in_array($peek[0], [ 'B', 'L', 'R' ])) { if (in_array($peek[0], [ 'B', 'L', 'R' ])) {
$result->child = self::parseFilter($result, self::$instance->data->consume()[0]); $child = self::parseFilter(self::$instance->data->consume()[0]);
} elseif ($peek === '(') { } elseif ($peek === '(') {
$result->child = self::parseGroup($result); $child = self::parseGroup();
} else { } else {
$result->child = self::_parsePath($result); $child = self::parsePath();
} }
$result = new Expression($child, $operator, $negate);
assert((fn() => self::debugResult($result))()); assert((fn() => self::debugResult($result))());
return $result; return $result;
} }
protected static function parseFilter(Expression $parent, string $prefix): Filter { protected static function parseFilter(string $prefix): Filter {
assert((fn() => self::debug())()); assert((fn() => self::debug())());
$result = new Filter($parent, $prefix);
$peek = self::$instance->data->peek(); $peek = self::$instance->data->peek();
if ($peek === '(') { if ($peek === '(') {
$result->child = self::parseGroup($result); $child = self::parseGroup();
} else { } else {
$result->child = self::_parsePath($result); $child = self::parsePath();
} }
$result = new Filter($child, $prefix);
assert((fn() => self::debugResult($result))()); assert((fn() => self::debugResult($result))());
return $result; return $result;
} }
protected static function parseGroup(Expression|Filter $parent): Group { protected static function parseGroup(): Group {
assert((fn() => self::debug())()); assert((fn() => self::debug())());
$result = new Group($parent);
$token = self::$instance->data->consume(); $token = self::$instance->data->consume();
if ($token !== '(') { if ($token !== '(') {
self::throw('"("', $token); self::throw('"("', $token);
} }
$result->child = self::_parseSelector($result); $child = self::_parseSelector();
$token = self::$instance->data->consume(); $token = self::$instance->data->consume();
if ($token !== ')') { if ($token !== ')') {
self::throw('")"', $token); self::throw('")"', $token);
} }
$result = new Group($child);
assert((fn() => self::debugResult($result))()); assert((fn() => self::debugResult($result))());
return $result; return $result;
} }
protected static function _parsePath(Expression $parent): Path { protected static function parsePath(): Path {
assert((fn() => self::debug())()); assert((fn() => self::debug())());
$result = new Path($parent);
$anchorStart = false; $anchorStart = false;
if (self::$instance->data->peek() === '^') { if (self::$instance->data->peek() === '^') {
$anchorStart = true; $anchorStart = true;
self::$instance->data->consume(); self::$instance->data->consume();
} }
$first = self::parseScope($result); $first = self::_parseScope();
if ($first->anchorToPrevious) { if ($first->anchorToPrevious) {
self::throw('first scope', '>'); self::throw('first scope', '>');
} }
@ -185,12 +178,10 @@ class Parser {
$peek = self::$instance->data->peek(); $peek = self::$instance->data->peek();
$anchorToPrevious = true; $anchorToPrevious = true;
} }
$scopes[] = self::parseScope($result, $anchorToPrevious); $scopes[] = self::_parseScope(end($scopes), $anchorToPrevious);
$peek = self::$instance->data->peek(); $peek = self::$instance->data->peek();
} }
$result->add(...$scopes);
$anchorEnd = false; $anchorEnd = false;
if ($peek === '$') { if ($peek === '$') {
$anchorEnd = true; $anchorEnd = true;
@ -198,49 +189,39 @@ class Parser {
} }
if ($anchorStart && $anchorEnd) { if ($anchorStart && $anchorEnd) {
$result->anchor = Path::ANCHOR_BOTH; $anchor = Path::ANCHOR_BOTH;
} elseif ($anchorStart) { } elseif ($anchorStart) {
$result->anchor = Path::ANCHOR_START; $anchor = Path::ANCHOR_START;
} elseif ($anchorEnd) { } elseif ($anchorEnd) {
$result->anchor = Path::ANCHOR_END; $anchor = Path::ANCHOR_END;
} else { } else {
$result->anchor = Path::ANCHOR_NONE; $anchor = Path::ANCHOR_NONE;
} }
$result = new Path($anchor, ...$scopes);
assert((fn() => self::debugResult($result))()); assert((fn() => self::debugResult($result))());
return $result; return $result;
} }
protected static function _parseSelector(?Group $parent = null): Selector { protected static function _parseSelector(): Selector {
assert((fn() => self::debug())()); assert((fn() => self::debug())());
$result = new Selector($parent); $composites = [ self::parseComposite() ];
$composites = [ self::parseComposite($result) ];
$peek = self::$instance->data->peek(); $peek = self::$instance->data->peek();
while ($peek === ',') { while ($peek === ',') {
self::$instance->data->consume(); self::$instance->data->consume();
$composites[] = self::parseComposite($result); $composites[] = self::parseComposite();
$peek = self::$instance->data->peek(); $peek = self::$instance->data->peek();
} }
$result->add(...$composites); $result = new Selector(...$composites);
assert((fn() => self::debugResult($result))()); assert((fn() => self::debugResult($result))());
return $result; return $result;
} }
protected static function parseScope(Path $parent): Scope { protected static function _parseScope(?Scope $parent = null, bool $anchorToPrevious = false): Scope {
assert((fn() => self::debug())()); assert((fn() => self::debug())());
$peek = self::$instance->data->peek();
if ($peek === '>') {
self::$instance->data->consume();
}
$result = new Scope($parent, ($peek === '>'));
$atoms = []; $atoms = [];
$first = true; $first = true;
do { do {
@ -258,10 +239,8 @@ class Parser {
$first = false; $first = false;
} while (self::$instance->data->peek() === '.'); } while (self::$instance->data->peek() === '.');
$result->add(...$atoms); $result = new Scope($parent, $anchorToPrevious, ...$atoms);
assert((fn() => self::debugResult($result))()); assert((fn() => self::debugResult($result))());
return $result; return $result;
} }
@ -300,11 +279,13 @@ class Parser {
printf("%s Result: %s\n", printf("%s Result: %s\n",
debug_backtrace()[2]['function'], debug_backtrace()[2]['function'],
// Removes bullshit from var_exported classes for easier reading // Removes bullshit from var_exported classes for easier reading
str_replace([ '::__set_state(array', __NAMESPACE__.'\\', '))' ], [ '', '', ')' ], var_export($result, true))); str_replace([ '::__set_state(array', __NAMESPACE__.'\\', '))' ], [ '', '', ')' ], var_export($result, true))
);
return true; return true;
} }
protected static function throw(array|string|bool $expected, string|bool $found) { protected static function throw(array|string|bool $expected, string|bool $found) {
throw new Exception($expected, $found, self::$instance->data->offset()); throw new ParserException($expected, $found, self::$instance->data->offset());
} }
} }

2
lib/Scope/Exception.php → lib/Scope/ParserException.php

@ -6,7 +6,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace dW\Lit\Scope; namespace dW\Lit\Scope;
class Exception extends \Exception { class ParserException extends \Exception {
const MESSAGE = '%s expected; found %s at offset %s'.\PHP_EOL; const MESSAGE = '%s expected; found %s at offset %s'.\PHP_EOL;
public function __construct(array|string|bool $expected, string|bool $found, int $offset) { public function __construct(array|string|bool $expected, string|bool $found, int $offset) {

62
lib/Scope/Path.php

@ -8,12 +8,6 @@ namespace dW\Lit\Scope;
class Path extends Node { class Path extends Node {
protected int $_anchor; protected int $_anchor;
protected array $frozen = [
'add' => false,
'anchor' => false
];
protected array $_scopes = []; protected array $_scopes = [];
const ANCHOR_NONE = 0; const ANCHOR_NONE = 0;
@ -22,55 +16,37 @@ class Path extends Node {
const ANCHOR_BOTH = 3; const ANCHOR_BOTH = 3;
public function __construct(Expression $parent) { public function __construct(int $anchor, Scope ...$scopes) {
$this->_parent = \WeakReference::create($parent); if ($anchor < 0 || $anchor > 3) {
throw new \Exception("Anchor must be a value between 0 and 3.\n");
}
$this->_anchor = $anchor;
$this->_scopes = $scopes;
} }
public function matches(Path $path): bool { public function matches(array $scopes): bool {
$start = reset($this->_scopes); // TODO: Handle anchors; while they are parsed they're not factored in when
$bt = end($this->_scopes); // matching because I can't find any documentation anywhere on what they do, and
$node = $this; // my brain can't tie itself into knots to read that part of the original C++.
if ($this->_anchor === self::ANCHOR_END || $this->_anchor === self::ANCHOR_BOTH) { $index = 0;
while ($node && $node->isAuxiliary()) { $cur = $this->_scopes[$index];
$node = $node->parent->get(); foreach ($scopes as $s) {
if ($cur->matches($s)) {
$cur = $this->_scopes[++$index] ?? null;
} }
$bt = $start; if ($cur === null) {
return true;
}
} }
return false; return false;
} }
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;
}
public function __toString(): string { public function __toString(): string {
$result = ''; $result = '';

34
lib/Scope/Scope.php

@ -9,30 +9,42 @@ namespace dW\Lit\Scope;
class Scope extends Node { class Scope extends Node {
protected bool $_anchorToPrevious; protected bool $_anchorToPrevious;
protected array $_atoms = []; protected array $_atoms = [];
protected bool $frozen = false; protected ?\WeakReference $_parent;
public function __construct(Path $parent, bool $anchorToPrevious = false) { public function __construct(?Scope $parent = null, bool $anchorToPrevious = false, string ...$atoms) {
$this->_anchorToPrevious = $anchorToPrevious; $this->_anchorToPrevious = $anchorToPrevious;
$this->_parent = \WeakReference::create($parent); $this->_atoms = $atoms;
$this->_parent = ($parent !== null) ? \WeakReference::create($parent) : null;
} }
public function add(string ...$atoms): bool { public function matches(Scope $scope): bool {
if ($this->frozen) { /*if (count($this->_atoms) !== count($scope->atoms)) {
return false; return false;
}*/
foreach ($this->_atoms as $index => $atom) {
if ($atom === '*') {
continue;
}
if ($atom !== $scope->atoms[$index]) {
return false;
}
} }
$this->_atoms = $atoms;
$this->frozen = true;
return true; return true;
} }
public function isAuxiliary(): bool {
$serialized = (string)$this;
return(strncmp($serialized, 'attr.', 5) === 0 || strncmp($serialized, 'dyn.', 4) === 0);
}
public function __get(string $name) {
if ($name === 'parent') {
return ($this->_parent !== null) ? $this->_parent->get() : null;
}
return parent::__get($name);
}
public function __toString(): string { public function __toString(): string {
$result = ''; $result = '';

28
lib/Scope/Selector.php

@ -8,31 +8,27 @@ namespace dW\Lit\Scope;
class Selector extends Node { class Selector extends Node {
protected array $_composites = []; protected array $_composites = [];
protected bool $frozen = false;
public function __construct(?Group $parent = null) { public function __construct(Composite ...$composites) {
$this->_parent = ($parent === null) ? null : \WeakReference::create($parent); $this->_composites = $composites;
} }
public function add(Composite ...$composites): bool { public function matches(array $scopes, &$match = null): bool {
if ($this->frozen) { foreach ($scopes as &$s) {
return false; $isString = is_string($s);
} if (!$isString && !$s instanceof Scope) {
throw new \Exception("Argument \$scopes must be an array containing only Scopes and/or strings.\n");
$this->_composites = $composites; }
$this->frozen = true;
return true;
}
public function matches(Path|string $path, &$match = null): bool { if ($isString) {
if (is_string($selector)) { $s = Parser::parseScope($s);
$path = Parser::parsePath($path); }
} }
foreach ($this->_composites as $composite) { foreach ($this->_composites as $composite) {
if ($composite->matches($path)) { if ($composite->matches($scopes)) {
$match = $composite; $match = $composite;
return true; return true;
} }

Loading…
Cancel
Save