diff --git a/lib/Grammar/Registry.php b/lib/Grammar/Registry.php index 37fc4d3..3371cc5 100644 --- a/lib/Grammar/Registry.php +++ b/lib/Grammar/Registry.php @@ -9,13 +9,34 @@ namespace dW\Highlighter\Grammar; class Registry { protected static array $grammars = []; - public static function add(string $grammarPath): bool { - if (!file_exists($grammarPath)) { - throw new \Exception("Path \"$grammarPath\" either does not exist or you do not have permission to view the file."); + public static function clear(): bool { + self::$grammars = []; + return true; + } + + public static function delete(string $scopeName): bool { + try { + unset(self::$grammars[$scopeName]); + } catch (Exception $e) { + return false; + } + + return true; + } + + public static function get(string $scopeName): array|bool { + foreach (self::$grammars as $grammar) { + if ($grammar['scopeName'] === $scopeName) { + return $grammar; + } } - if (isset(self::$grammars[$grammarPath])) { - return true; + return false; + } + + public static function set(string $grammarPath, bool $force = false): bool { + if (!file_exists($grammarPath)) { + throw new \Exception("Path \"$grammarPath\" either does not exist or you do not have permission to view the file."); } $grammar = json_decode(file_get_contents($grammarPath), true); @@ -23,7 +44,15 @@ class Registry { throw new \Exception("\"$grammarPath\" is not a valid grammar file."); } - self::$grammars[$grammarPath] = $grammar; + if (!isset($grammar['scopeName'])) { + throw new \Exception("\"$grammarPath\" is missing the required scopeName property."); + } + + if (!$force && isset(self::$grammars[$grammar['scopeName']])) { + throw new \Exception("Grammar \"{$grammar['scopeName']}\" already exists."); + } + + self::$grammars[$grammar['scopeName']] = $grammar; return true; } } \ No newline at end of file diff --git a/lib/Scope/Data.php b/lib/Scope/Data.php index ebb3b82..f146ec3 100644 --- a/lib/Scope/Data.php +++ b/lib/Scope/Data.php @@ -6,6 +6,10 @@ declare(strict_types=1); namespace dW\Highlighter\Scope; +/** + * Tokenizes scope strings into an array of segments of the original string and + * provides an interface for iterating through them. + */ class Data { protected array $data; @@ -18,6 +22,7 @@ class Data { $this->endPosition = count($this->data) - 1; } + /** Moves the pointer up one position and returns the corresponding token */ public function consume(): string|bool { if ($this->_position === $this->endPosition) { return false; @@ -26,6 +31,7 @@ class Data { return $this->data[++$this->_position][0]; } + /** Returns the character offset of the current token */ public function offset(): int|bool { if ($this->_position > $this->endPosition) { return false; @@ -34,6 +40,7 @@ class Data { return $this->data[$this->_position][1]; } + /** Returns the next token without moving the pointer */ public function peek(): string|bool { if ($this->_position === $this->endPosition) { return false; diff --git a/lib/Scope/Parser.php b/lib/Scope/Parser.php index 5c8198e..10fa677 100644 --- a/lib/Scope/Parser.php +++ b/lib/Scope/Parser.php @@ -6,20 +6,33 @@ declare(strict_types=1); namespace dW\Highlighter\Scope; +/** Parses scope strings into a matcher tree */ class Parser { + // When true prints out detailed data about the construction of the matcher + // tree. public static bool $debug = false; + // The tokenized scope string protected Data $data; + // Used for incrementing the blocks of debug information; useful for creating + // breakpoints when debugging. protected int $debugCount = 1; + // Used for instancing data tokens in static methods. protected static Parser $instance; + // strspn mask used to check whether a token could be a valid scope. + 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 { self::$instance = new self($selector); return self::parseSelector(); @@ -76,11 +89,12 @@ class Parser { if ($peek === '(') { self::$instance->data->consume(); $result = self::parseGroup($prefix); - } elseif (strspn($peek, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-+_.*') === strlen($peek)) { + } elseif (!in_array($peek, [ '-', false ]) && strspn($peek, self::SCOPE_MASK) === strlen($peek)) { $result = self::parsePath($prefix); } else { + if (!in_array($peek, [ '-', false ]) && ) // TODO: Take the effort to make this more descriptive - self::throw([ 'Group', 'Path' ], $peek); + self::throw([ 'Group', 'Path', 'Scope' ], $peek); } if (self::$debug) { @@ -119,7 +133,7 @@ class Parser { $result[] = self::parseScope(); $peek = self::$instance->data->peek(); - while (!in_array($peek, [ '-', false ]) && strspn($peek, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-+_.*') === strlen($peek)) { + while (!in_array($peek, [ '-', false ]) && strspn($peek, self::SCOPE_MASK) === strlen($peek)) { $result[] = self::parseScope(); $peek = self::$instance->data->peek(); }