> $tokens
29 | */
30 | public function highlighted(array $tokens): array;
31 |
32 | /**
33 | * Modify the root HTML element.
34 | */
35 | public function root(Root $root): Root;
36 |
37 | /**
38 | * Modify the tag.
39 | */
40 | public function pre(Element $pre): Element;
41 |
42 | /**
43 | * Modify the tag.
44 | */
45 | public function code(Element $code): Element;
46 |
47 | /**
48 | * Modify the for each line.
49 | *
50 | * @param array $line
51 | */
52 | public function line(Element $span, array $line, int $index): Element;
53 |
54 | /**
55 | * Modify the for each token.
56 | */
57 | public function token(Element $span, HighlightedToken $token, int $index, int $line): Element;
58 |
59 | /**
60 | * Modify the for each gutter element.
61 | */
62 | public function gutter(Element $span, int $lineNumber): Element;
63 |
64 | /**
65 | * Modify the HTML output after the AST has been converted.
66 | */
67 | public function postprocess(string $html): string;
68 |
69 | /**
70 | * Supply the meta object to the transformer.
71 | */
72 | public function withMeta(Meta $meta): void;
73 | }
74 |
--------------------------------------------------------------------------------
/src/Environment.php:
--------------------------------------------------------------------------------
1 | grammars = new GrammarRepository;
23 | $this->themes = new ThemeRepository;
24 | }
25 |
26 | public function extend(ExtensionInterface $extension): static
27 | {
28 | $extension->register($this);
29 |
30 | return $this;
31 | }
32 |
33 | public function grammar(string $slug, string|ParsedGrammar $grammar): static
34 | {
35 | $this->grammars->register($slug, $grammar);
36 |
37 | return $this;
38 | }
39 |
40 | public function theme(string $slug, string|ParsedTheme $theme): static
41 | {
42 | $this->themes->register($slug, $theme);
43 |
44 | return $this;
45 | }
46 |
47 | public function cache(CacheInterface $cache): static
48 | {
49 | $this->cache = $cache;
50 |
51 | return $this;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Exceptions/FailedToInitializePatternSearchException.php:
--------------------------------------------------------------------------------
1 | name === null) {
29 | return null;
30 | }
31 |
32 | return Str::replaceScopeNameCapture($this->name, $captures);
33 | }
34 |
35 | public function captures(): array
36 | {
37 | return count(array_filter($this->beginCaptures)) > 0 ? $this->beginCaptures : $this->captures;
38 | }
39 |
40 | public function getContentName(array $captures): ?string
41 | {
42 | if ($this->contentName === null) {
43 | return null;
44 | }
45 |
46 | return Str::replaceScopeNameCapture($this->contentName, $captures);
47 | }
48 |
49 | /**
50 | * Compile the pattern into a list of matchable patterns.
51 | *
52 | * @return array
53 | */
54 | public function compile(ParsedGrammar $grammar, GrammarRepositoryInterface $grammars, bool $allowA, bool $allowG): array
55 | {
56 | return [
57 | [$this, $this->begin->get($allowA, $allowG)],
58 | ];
59 | }
60 |
61 | /**
62 | * Create the associated `EndPattern` for this `BeginEndPattern`.
63 | */
64 | public function createEndPattern(MatchedPattern $matched): EndPattern
65 | {
66 | return new EndPattern(
67 | id: $this->id,
68 | begin: $matched,
69 | end: $this->end,
70 | name: $this->name,
71 | contentName: $this->contentName,
72 | endCaptures: $this->endCaptures,
73 | captures: $this->captures,
74 | patterns: $this->patterns,
75 | injection: $this->injection,
76 | );
77 | }
78 |
79 | public function getId(): int
80 | {
81 | return $this->id;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/Grammar/BeginWhilePattern.php:
--------------------------------------------------------------------------------
1 | name === null) {
29 | return null;
30 | }
31 |
32 | return Str::replaceScopeNameCapture($this->name, $captures);
33 | }
34 |
35 | public function captures(): array
36 | {
37 | return count(array_filter($this->beginCaptures)) > 0 ? array_filter($this->beginCaptures) : $this->captures;
38 | }
39 |
40 | public function getContentName(array $captures): ?string
41 | {
42 | if ($this->contentName === null) {
43 | return null;
44 | }
45 |
46 | return Str::replaceScopeNameCapture($this->contentName, $captures);
47 | }
48 |
49 | /**
50 | * Compile the pattern into a list of matchable patterns.
51 | *
52 | * @return array
53 | */
54 | public function compile(ParsedGrammar $grammar, GrammarRepositoryInterface $grammars, bool $allowA, bool $allowG): array
55 | {
56 | return [
57 | [$this, $this->begin->get($allowA, $allowG)],
58 | ];
59 | }
60 |
61 | public function createWhilePattern(MatchedPattern $matched): WhilePattern
62 | {
63 | return new WhilePattern(
64 | $this->id,
65 | $matched,
66 | $this->while,
67 | $this->name,
68 | $this->contentName,
69 | $this->whileCaptures,
70 | $this->captures,
71 | $this->patterns,
72 | $this->injection
73 | );
74 | }
75 |
76 | public function getId(): int
77 | {
78 | return $this->id;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Grammar/Capture.php:
--------------------------------------------------------------------------------
1 | pattern->patterns) > 0;
21 | }
22 |
23 | public function getScopeName(array $captures): ?string
24 | {
25 | if ($this->name === null) {
26 | return null;
27 | }
28 |
29 | return Str::replaceScopeNameCapture($this->name, $captures);
30 | }
31 |
32 | public function compile(ParsedGrammar $grammar, GrammarRepositoryInterface $grammars, bool $allowA, bool $allowG): array
33 | {
34 | return $this->pattern->compile($grammar, $grammars, $allowA, $allowG);
35 | }
36 |
37 | public function getId(): int
38 | {
39 | return $this->id;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Grammar/CollectionPattern.php:
--------------------------------------------------------------------------------
1 |
28 | */
29 | public function compile(ParsedGrammar $grammar, GrammarRepositoryInterface $grammars, bool $allowA, bool $allowG): array
30 | {
31 | $compiled = [];
32 |
33 | foreach ($this->patterns as $pattern) {
34 | $compiled = array_merge($compiled, $pattern->compile($grammar, $grammars, $allowA, $allowG));
35 | }
36 |
37 | return $compiled;
38 | }
39 |
40 | public function getId(): int
41 | {
42 | return $this->id;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Grammar/EndPattern.php:
--------------------------------------------------------------------------------
1 | name === null) {
27 | return null;
28 | }
29 |
30 | return Str::replaceScopeNameCapture($this->name, $captures);
31 | }
32 |
33 | public function captures(): array
34 | {
35 | return count(array_filter($this->endCaptures)) > 0 ? $this->endCaptures : $this->captures;
36 | }
37 |
38 | /**
39 | * Compile the pattern into a list of matchable patterns.
40 | *
41 | * @return array
42 | */
43 | public function compile(ParsedGrammar $grammar, GrammarRepositoryInterface $grammars, bool $allowA, bool $allowG): array
44 | {
45 | $compiled = [
46 | [$this, $this->end->get($allowA, $allowG, $this->begin->matches)],
47 | ];
48 |
49 | foreach ($this->patterns as $pattern) {
50 | $compiled = array_merge($compiled, $pattern->compile($grammar, $grammars, $allowA, $allowG));
51 | }
52 |
53 | return $compiled;
54 | }
55 |
56 | public function getId(): int
57 | {
58 | return $this->id;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Grammar/GrammarRepository.php:
--------------------------------------------------------------------------------
1 | grammars[$grammar->value] = $grammar->path();
20 | $this->scopesToGrammar[$grammar->scopeName()] = $grammar->value;
21 |
22 | foreach ($grammar->aliases() as $alias) {
23 | $this->aliases[$alias] = $grammar->value;
24 | }
25 | }
26 | }
27 |
28 | public function get(string $name): ParsedGrammar
29 | {
30 | if (! $this->has($name)) {
31 | throw UnrecognisedGrammarException::make($name);
32 | }
33 |
34 | $name = $this->aliases[$name] ?? $name;
35 | $grammar = $this->grammars[$name];
36 |
37 | if ($grammar instanceof ParsedGrammar) {
38 | return $grammar;
39 | }
40 |
41 | $parser = new GrammarParser;
42 |
43 | return $this->grammars[$name] = $parser->parse(json_decode(file_get_contents($grammar), true));
44 | }
45 |
46 | public function getFromScope(string $scope): ParsedGrammar
47 | {
48 | if (! isset($this->scopesToGrammar[$scope])) {
49 | throw UnrecognisedGrammarException::make($scope);
50 | }
51 |
52 | return $this->get($this->scopesToGrammar[$scope]);
53 | }
54 |
55 | public function has(string $name): bool
56 | {
57 | return isset($this->grammars[$name]) || isset($this->aliases[$name]);
58 | }
59 |
60 | public function alias(string $alias, string $target): void
61 | {
62 | $this->aliases[$alias] = $target;
63 | }
64 |
65 | public function register(string $name, string|ParsedGrammar $pathOrGrammar): void
66 | {
67 | $this->grammars[$name] = $pathOrGrammar;
68 | }
69 |
70 | public function resolve(string|Grammar|ParsedGrammar $grammar): ParsedGrammar
71 | {
72 | if ($grammar instanceof ParsedGrammar) {
73 | return $grammar;
74 | }
75 |
76 | return match (true) {
77 | is_string($grammar) => $this->get($grammar),
78 | $grammar instanceof Grammar => $grammar->toParsedGrammar($this),
79 | };
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Grammar/IncludePattern.php:
--------------------------------------------------------------------------------
1 |
27 | */
28 | public function compile(ParsedGrammar $grammar, GrammarRepositoryInterface $grammars, bool $allowA, bool $allowG): array
29 | {
30 | try {
31 | $resolved = match (true) {
32 | // "include": "$self"
33 | $this->reference === '$self' => $grammars->getFromScope($this->scopeName ?? $grammar->scopeName),
34 | // "include": "$base"
35 | $this->reference === '$base' => $grammar,
36 | // "include": "#name"
37 | $this->reference !== null && $this->scopeName === $grammar->scopeName => $grammar->resolve($this->reference),
38 | // "include": "scope#name"
39 | $this->reference !== null && $this->scopeName !== $grammar->scopeName => $grammars->getFromScope($this->scopeName)->resolve($this->reference),
40 | // "include": "scope"
41 | default => $grammars->getFromScope($this->scopeName),
42 | };
43 | } catch (UnrecognisedGrammarException) {
44 | $resolved = null;
45 | }
46 |
47 | if ($resolved === null) {
48 | return [];
49 | }
50 |
51 | return $resolved->compile($grammar, $grammars, $allowA, $allowG);
52 | }
53 |
54 | public function getId(): int
55 | {
56 | return $this->id;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Grammar/Injections/Composite.php:
--------------------------------------------------------------------------------
1 | $expressions
11 | */
12 | public function __construct(
13 | public array $expressions,
14 | ) {}
15 |
16 | public function getPrefix(array $scopes): ?Prefix
17 | {
18 | if (! $this->matches($scopes)) {
19 | return null;
20 | }
21 |
22 | return $this->expressions[0]->getPrefix($scopes);
23 | }
24 |
25 | public function matches(array $scopes): bool
26 | {
27 | $carry = false;
28 |
29 | foreach ($this->expressions as $expression) {
30 | if (
31 | ($carry && $expression->operator === Operator::Or) ||
32 | (! $carry && $expression->operator === Operator::And) ||
33 | (! $carry && $expression->operator === Operator::Not)
34 | ) {
35 | continue;
36 | }
37 |
38 | $matches = $expression->matches($scopes);
39 |
40 | match ($expression->operator) {
41 | Operator::None => $carry = $matches,
42 | Operator::And => $carry = $carry && $matches,
43 | Operator::Or => $carry = $carry || $matches,
44 | Operator::Not => $carry = $carry && ! $matches,
45 | };
46 | }
47 |
48 | return $carry;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Grammar/Injections/Expression.php:
--------------------------------------------------------------------------------
1 | matches($scopes)) {
18 | return null;
19 | }
20 |
21 | return $this->child->getPrefix($scopes);
22 | }
23 |
24 | public function matches(array $scopes): bool
25 | {
26 | $result = $this->child->matches($scopes);
27 |
28 | if ($this->negated) {
29 | return ! $result;
30 | }
31 |
32 | return $result;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Grammar/Injections/Filter.php:
--------------------------------------------------------------------------------
1 | matches($scopes)) {
17 | return null;
18 | }
19 |
20 | return $this->prefix;
21 | }
22 |
23 | public function matches(array $scopes): bool
24 | {
25 | return $this->child->matches($scopes);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Grammar/Injections/Group.php:
--------------------------------------------------------------------------------
1 | matches($scopes)) {
16 | return null;
17 | }
18 |
19 | return $this->child->getPrefix($scopes);
20 | }
21 |
22 | public function matches(array $scopes): bool
23 | {
24 | return $this->child->matches($scopes);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Grammar/Injections/Injection.php:
--------------------------------------------------------------------------------
1 | selector;
21 | }
22 |
23 | public function getPrefix(array $scopes): ?Prefix
24 | {
25 | return $this->selector->getPrefix($scopes);
26 | }
27 |
28 | public function matches(array $scopes): bool
29 | {
30 | return $this->selector->matches($scopes);
31 | }
32 |
33 | public function getScopeName(array $captures): ?string
34 | {
35 | return $this->pattern->getScopeName($captures);
36 | }
37 |
38 | public function compile(ParsedGrammar $grammar, GrammarRepositoryInterface $grammars, bool $allowA, bool $allowG): array
39 | {
40 | return $this->pattern->compile($grammar, $grammars, $allowA, $allowG);
41 | }
42 |
43 | public function getId(): int
44 | {
45 | return $this->id;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Grammar/Injections/Operator.php:
--------------------------------------------------------------------------------
1 | scopes[$index];
22 |
23 | foreach ($scopes as $scope) {
24 | $scope = Scope::fromString($scope);
25 |
26 | if ($current->matches($scope)) {
27 | $current = $this->scopes[++$index] ?? null;
28 | }
29 |
30 | if ($current === null) {
31 | return true;
32 | }
33 | }
34 |
35 | return false;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Grammar/Injections/Prefix.php:
--------------------------------------------------------------------------------
1 | parts as $i => $part) {
16 | if ($part === '*') {
17 | continue;
18 | }
19 |
20 | if ($part !== ($scope->parts[$i] ?? null)) {
21 | return false;
22 | }
23 | }
24 |
25 | return true;
26 | }
27 |
28 | public function __toString(): string
29 | {
30 | return implode('.', $this->parts);
31 | }
32 |
33 | public static function fromString(string $scope): self
34 | {
35 | return new self(explode('.', $scope));
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Grammar/Injections/Selector.php:
--------------------------------------------------------------------------------
1 | $composites
11 | */
12 | public function __construct(
13 | public array $composites,
14 | ) {}
15 |
16 | public function getPrefix(array $scopes): ?Prefix
17 | {
18 | foreach ($this->composites as $composite) {
19 | if ($composite->matches($scopes)) {
20 | return $composite->getPrefix($scopes);
21 | }
22 | }
23 |
24 | return null;
25 | }
26 |
27 | public function matches(array $scopes): bool
28 | {
29 | foreach ($this->composites as $composite) {
30 | if ($composite->matches($scopes)) {
31 | return true;
32 | }
33 | }
34 |
35 | return false;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Grammar/MatchPattern.php:
--------------------------------------------------------------------------------
1 | name === null) {
26 | return null;
27 | }
28 |
29 | return Str::replaceScopeNameCapture($this->name, $captures);
30 | }
31 |
32 | /**
33 | * Compile the pattern into a list of matchable patterns.
34 | *
35 | * @return array
36 | */
37 | public function compile(ParsedGrammar $grammar, GrammarRepositoryInterface $grammars, bool $allowA, bool $allowG): array
38 | {
39 | return [
40 | [$this, $this->match->get($allowA, $allowG)],
41 | ];
42 | }
43 |
44 | public function getId(): int
45 | {
46 | return $this->id;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Grammar/MatchedInjection.php:
--------------------------------------------------------------------------------
1 | matchedPattern->offset();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Grammar/MatchedPattern.php:
--------------------------------------------------------------------------------
1 | matches[0][0];
20 | }
21 |
22 | public function end(): int
23 | {
24 | return $this->matches[0][1] + strlen($this->matches[0][0]);
25 | }
26 |
27 | /**
28 | * Get the start position of the matched pattern.
29 | */
30 | public function offset(): int
31 | {
32 | return $this->matches[0][1];
33 | }
34 |
35 | public function getCaptureGroup(int|string $index): ?array
36 | {
37 | return $this->matches[$index] ?? null;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Grammar/ParsedGrammar.php:
--------------------------------------------------------------------------------
1 | $repository
13 | * @param Injections\Injection[] $injections
14 | */
15 | public function __construct(
16 | public ?string $name,
17 | public string $scopeName,
18 | public array $patterns,
19 | public array $repository,
20 | public array $injections,
21 | ) {}
22 |
23 | public function getScopeName(array $captures): ?string
24 | {
25 | return null;
26 | }
27 |
28 | /**
29 | * Compile the pattern into a list of matchable patterns.
30 | *
31 | * @return array
32 | */
33 | public function compile(ParsedGrammar $grammar, GrammarRepositoryInterface $grammars, bool $allowA, bool $allowG): array
34 | {
35 | $compiled = [];
36 |
37 | foreach ($this->patterns as $pattern) {
38 | $compiled = array_merge($compiled, $pattern->compile($grammar, $grammars, $allowA, $allowG));
39 | }
40 |
41 | return $compiled;
42 | }
43 |
44 | /** @return Injections\Injection[] */
45 | public function getInjections(): array
46 | {
47 | return $this->injections;
48 | }
49 |
50 | public function hasInjections(): bool
51 | {
52 | return count($this->injections) > 0;
53 | }
54 |
55 | public function resolve(string $reference): ?PatternInterface
56 | {
57 | return $this->repository[$reference] ?? null;
58 | }
59 |
60 | public static function fromArray(array $grammar): ParsedGrammar
61 | {
62 | $parser = new GrammarParser;
63 |
64 | return $parser->parse($grammar);
65 | }
66 |
67 | public function getId(): int
68 | {
69 | return 0;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Grammar/WhilePattern.php:
--------------------------------------------------------------------------------
1 | name === null) {
27 | return null;
28 | }
29 |
30 | return Str::replaceScopeNameCapture($this->name, $captures);
31 | }
32 |
33 | public function captures(): array
34 | {
35 | return count(array_filter($this->whileCaptures)) > 0 ? $this->whileCaptures : $this->captures;
36 | }
37 |
38 | /**
39 | * Compile the pattern into a list of matchable patterns.
40 | *
41 | * @return array
42 | */
43 | public function compile(ParsedGrammar $grammar, GrammarRepositoryInterface $grammars, bool $allowA, bool $allowG): array
44 | {
45 | $compiled = [];
46 |
47 | foreach ($this->patterns as $pattern) {
48 | $compiled = array_merge($compiled, $pattern->compile($grammar, $grammars, $allowA, $allowG));
49 | }
50 |
51 | return $compiled;
52 | }
53 |
54 | public function getId(): int
55 | {
56 | return $this->id;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Highlighting/Highlighter.php:
--------------------------------------------------------------------------------
1 | $themes
12 | */
13 | public function __construct(
14 | public array $themes
15 | ) {}
16 |
17 | public function highlight(array $tokens): array
18 | {
19 | $highlightedTokens = [];
20 |
21 | foreach ($tokens as $i => $line) {
22 | foreach ($line as $token) {
23 | $settings = [];
24 |
25 | foreach ($this->themes as $id => $theme) {
26 | if ($matched = $theme->match($token->scopes)) {
27 | $settings[$id] = $matched;
28 | }
29 | }
30 |
31 | $highlightedTokens[$i][] = new HighlightedToken($token, $settings);
32 | }
33 | }
34 |
35 | return $highlightedTokens;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Phast/ClassList.php:
--------------------------------------------------------------------------------
1 | classes, true);
16 | }
17 |
18 | public function toggle(string $class, bool $state = true): self
19 | {
20 | if ($state && ! $this->contains($class)) {
21 | $this->add($class);
22 | } elseif (! $state && $this->contains($class)) {
23 | $this->remove($class);
24 | }
25 |
26 | return $this;
27 | }
28 |
29 | public function add(string ...$class): self
30 | {
31 | $this->classes = array_unique(array_merge($this->classes, $class));
32 |
33 | return $this;
34 | }
35 |
36 | public function remove(string ...$class): self
37 | {
38 | $this->classes = array_filter($this->classes, fn (string $c) => ! in_array($c, $class, true));
39 |
40 | return $this;
41 | }
42 |
43 | public function all(): array
44 | {
45 | return $this->classes;
46 | }
47 |
48 | public function isEmpty(): bool
49 | {
50 | return empty($this->classes);
51 | }
52 |
53 | public function __toString(): string
54 | {
55 | return implode(' ', array_filter($this->classes, fn (string $class) => trim($class) !== ''));
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Phast/Element.php:
--------------------------------------------------------------------------------
1 | $children
11 | */
12 | public function __construct(
13 | public string $tagName,
14 | public Properties $properties = new Properties,
15 | public array $children = [],
16 | ) {}
17 |
18 | public function __toString(): string
19 | {
20 | $properties = (string) $this->properties;
21 |
22 | $element = sprintf(
23 | '<%s%s>',
24 | $this->tagName,
25 | $properties ? ' '.$properties : ''
26 | );
27 |
28 | foreach ($this->children as $child) {
29 | $element .= (string) $child;
30 | }
31 |
32 | $element .= sprintf('%s>', $this->tagName);
33 |
34 | return $element;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Phast/Literal.php:
--------------------------------------------------------------------------------
1 | value;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Phast/Properties.php:
--------------------------------------------------------------------------------
1 | $properties
11 | */
12 | public function __construct(
13 | public array $properties = [],
14 | ) {}
15 |
16 | public function set(string $key, string|Stringable $value): self
17 | {
18 | $this->properties[$key] = $value;
19 |
20 | return $this;
21 | }
22 |
23 | public function get(string $key): mixed
24 | {
25 | return $this->properties[$key] ?? null;
26 | }
27 |
28 | public function has(string $key): bool
29 | {
30 | return array_key_exists($key, $this->properties);
31 | }
32 |
33 | public function remove(string $key): self
34 | {
35 | unset($this->properties[$key]);
36 |
37 | return $this;
38 | }
39 |
40 | public function __toString(): string
41 | {
42 | $properties = array_filter($this->properties, fn ($value) => $value instanceof ClassList ? (! $value->isEmpty()) : ((bool) $value));
43 |
44 | return implode(' ', array_map(
45 | fn ($key, $value) => sprintf('%s="%s"', $key, $value),
46 | array_keys($properties),
47 | $properties,
48 | ));
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Phast/Root.php:
--------------------------------------------------------------------------------
1 | $children
11 | */
12 | public function __construct(
13 | public array $children = [],
14 | ) {}
15 |
16 | public function __toString(): string
17 | {
18 | return implode('', array_map(fn (Element|Text $child) => (string) $child, $this->children));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Phast/Text.php:
--------------------------------------------------------------------------------
1 | $array
13 | * @return T
14 | */
15 | public static function first(array $array): mixed
16 | {
17 | return reset($array);
18 | }
19 |
20 | /**
21 | * @template K
22 | * @template V
23 | *
24 | * @param array $array
25 | * @return K
26 | */
27 | public static function firstKey(array $array): mixed
28 | {
29 | return array_key_first($array);
30 | }
31 |
32 | public static function map(array $array, Closure $callback): array
33 | {
34 | return array_map($callback, $array);
35 | }
36 |
37 | public static function wrap(mixed $value): array
38 | {
39 | if (is_array($value)) {
40 | return $value;
41 | }
42 |
43 | return [$value];
44 | }
45 |
46 | public static function filterMap(array $array, callable $callback): array
47 | {
48 | return array_filter(array_map($callback, $array));
49 | }
50 |
51 | public static function any(array $array, callable $callback): bool
52 | {
53 | foreach ($array as $value) {
54 | if ($callback($value)) {
55 | return true;
56 | }
57 | }
58 |
59 | return false;
60 | }
61 |
62 | public static function partition(array $array, callable $callback): array
63 | {
64 | $matches = [];
65 | $nonMatches = [];
66 |
67 | foreach ($array as $key => $value) {
68 | if ($callback($value, $key)) {
69 | $matches[$key] = $value;
70 | } else {
71 | $nonMatches[$key] = $value;
72 | }
73 | }
74 |
75 | return [$matches, $nonMatches];
76 | }
77 |
78 | public static function flatten(array $array): array
79 | {
80 | return array_merge(...array_map(fn ($item) => is_array($item) ? self::flatten($item) : [$item], $array));
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Support/Str.php:
--------------------------------------------------------------------------------
1 | `storage.type.const.php`
52 | */
53 | public static function replaceScopeNameCapture(string $scopeName, array $captures): string
54 | {
55 | return preg_replace_callback(self::CAPTURING_REGEX_SOURCE, function (array $matches) use ($captures) {
56 | $capture = $captures[intval($matches[1])];
57 |
58 | if (! $capture) {
59 | return $matches[0];
60 | }
61 |
62 | $result = $capture[0];
63 |
64 | if ($result === '') {
65 | return '';
66 | }
67 |
68 | while ($result && $result[0] === '.') {
69 | $result = substr($result, 1);
70 | }
71 |
72 | return match ($matches[3] ?? null) {
73 | 'downcase' => strtolower($result),
74 | 'upcase' => strtoupper($result),
75 | default => $result,
76 | };
77 | }, $scopeName);
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Tests/Adapters/Laravel/TestCase.php:
--------------------------------------------------------------------------------
1 | scopePath->push($scopeName));
26 | }
27 |
28 | $scopeNames = explode(' ', $scopeName);
29 | $result = $this;
30 |
31 | foreach ($scopeNames as $name) {
32 | $result = new AttributedScopeStack($result, $result->scopePath->push($name));
33 | }
34 |
35 | return $result;
36 | }
37 |
38 | /**
39 | * Get the scope names for this stack.
40 | *
41 | * @return list
42 | */
43 | public function getScopeNames(): array
44 | {
45 | return $this->scopePath->getSegments();
46 | }
47 |
48 | /**
49 | * Create the root scope stack.
50 | */
51 | public static function createRoot(string $rootScopeName): AttributedScopeStack
52 | {
53 | return new AttributedScopeStack(null, new ScopeStack(null, $rootScopeName));
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/TextMate/LineTokens.php:
--------------------------------------------------------------------------------
1 | $tokens
18 | */
19 | public function __construct(
20 | public string $lineText,
21 | public array $tokens = [],
22 | ) {}
23 |
24 | /**
25 | * Produce a set of tokens from the given state stack.
26 | */
27 | public function produce(StateStack $stack, int $endIndex): void
28 | {
29 | $this->produceFromScopes($stack->contentNameScopesList, $endIndex);
30 | }
31 |
32 | /**
33 | * Produce a set of tokens from the given scope list.
34 | */
35 | public function produceFromScopes(?AttributedScopeStack $scopesList, int $endIndex): void
36 | {
37 | if ($this->lastTokenEndIndex >= $endIndex) {
38 | return;
39 | }
40 |
41 | $scopes = $scopesList?->getScopeNames() ?? [];
42 |
43 | $this->tokens[] = new Token(
44 | scopes: $scopes,
45 | text: substr($this->lineText, $this->lastTokenEndIndex, $endIndex - $this->lastTokenEndIndex),
46 | start: $this->lastTokenEndIndex,
47 | end: $endIndex,
48 | );
49 |
50 | $this->lastTokenEndIndex = $endIndex;
51 | }
52 |
53 | /**
54 | * Get all of the tokens in this line.
55 | *
56 | * @return array
57 | */
58 | public function getResult(StateStack $stack, int $lineLength): array
59 | {
60 | if (count($this->tokens) > 0 && $this->tokens[count($this->tokens) - 1]->start === $lineLength) {
61 | array_pop($this->tokens);
62 | }
63 |
64 | if (count($this->tokens) === 0) {
65 | $this->lastTokenEndIndex = -1;
66 | $this->produce($stack, $lineLength);
67 | $this->tokens[count($this->tokens) - 1]->start = 0;
68 | }
69 |
70 | return $this->tokens;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/TextMate/LocalStackElement.php:
--------------------------------------------------------------------------------
1 |
29 | */
30 | public function getSegments(): array
31 | {
32 | $stack = $this;
33 | $result = [];
34 |
35 | while ($stack !== null) {
36 | $result[] = $stack->scopeName;
37 | $stack = $stack->parent;
38 | }
39 |
40 | return array_reverse($result);
41 | }
42 |
43 | /**
44 | * Get a string representation of the stack.
45 | */
46 | public function __toString(): string
47 | {
48 | return implode(' ', $this->getSegments());
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/TextMate/StateStack.php:
--------------------------------------------------------------------------------
1 | depth = $parent ? $parent->depth + 1 : 0;
30 | }
31 |
32 | /**
33 | * Pop the current state stack.
34 | */
35 | public function pop(): ?StateStack
36 | {
37 | return $this->parent;
38 | }
39 |
40 | /**
41 | * Safely pop the current state stack.
42 | */
43 | public function safePop(): StateStack
44 | {
45 | if ($this->parent === null) {
46 | return $this;
47 | }
48 |
49 | return $this->parent;
50 | }
51 |
52 | /**
53 | * Push the given data into a new state stack.
54 | */
55 | public function push(PatternInterface $pattern, int $enterPos, int $anchorPos, bool $beginRuleCapturedEOL, ?string $endRule, ?AttributedScopeStack $nameScopesList, ?AttributedScopeStack $contentNameScopesList): StateStack
56 | {
57 | return new StateStack(
58 | parent: $this,
59 | pattern: $pattern,
60 | enterPos: $enterPos,
61 | anchorPos: $anchorPos,
62 | beginRuleCapturedEOL: $beginRuleCapturedEOL,
63 | endRule: $endRule,
64 | nameScopesList: $nameScopesList,
65 | contentNameScopesList: $contentNameScopesList,
66 | );
67 | }
68 |
69 | /**
70 | * Generate a near-identical state stack with the given content name scopes list.
71 | */
72 | public function withContentNameScopesList(AttributedScopeStack $contentNameScopesList): StateStack
73 | {
74 | $stack = clone $this;
75 | $stack->contentNameScopesList = $contentNameScopesList;
76 |
77 | return $stack;
78 | }
79 |
80 | /**
81 | * Generate a near-identical state stack with the given end rule.
82 | */
83 | public function withEndRule(EndPattern|WhilePattern $rule): StateStack
84 | {
85 | $stack = clone $this;
86 | $stack->pattern = $rule;
87 |
88 | return $stack;
89 | }
90 |
91 | /**
92 | * Check whether this state stack has the same rule as the given state stack.
93 | */
94 | public function hasSameRuleAs(StateStack $other): bool
95 | {
96 | $el = $this;
97 |
98 | while ($el !== null && $el->enterPos === $other->enterPos) {
99 | if ($el->pattern->getId() === $other->pattern->getId()) {
100 | return true;
101 | }
102 |
103 | $el = $el->parent;
104 | }
105 |
106 | return false;
107 | }
108 |
109 | /**
110 | * Reset the state stack.
111 | */
112 | public function reset(): void
113 | {
114 | $el = $this;
115 |
116 | while ($el !== null) {
117 | $el->enterPos = -1;
118 | $el->anchorPos = -1;
119 | $el = $el->parent;
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/TextMate/WhileStackElement.php:
--------------------------------------------------------------------------------
1 | $colors
9 | * @param TokenColor[] $tokenColors
10 | */
11 | public function __construct(
12 | public string $name,
13 | public array $colors = [],
14 | public array $tokenColors = [],
15 | ) {}
16 |
17 | public function match(array $scopes): ?TokenSettings
18 | {
19 | $matches = [];
20 |
21 | foreach ($this->tokenColors as $tokenColor) {
22 | if ($result = $tokenColor->match($scopes)) {
23 | $matches[] = new TokenColorMatchResult($tokenColor, $result);
24 | }
25 | }
26 |
27 | // No matches, so no need to highlight.
28 | if ($matches === []) {
29 | return null;
30 | }
31 |
32 | // We've only got a single match so no need to do any specificity calculations.
33 | if (count($matches) === 1) {
34 | return $matches[0]->tokenColor->settings;
35 | }
36 |
37 | // We need to sort the matches based on specificity.
38 | // The precedence logic based on `vscode-textmate` is:
39 | // 1. The depth of the match in the token's scope hierarchy -> deeper matches are more specific.
40 | // 2. The dot count of the matching scope selector -> more segments makes it more specific.
41 | // 3. If there's a tie, figure out how many ancestral matches there were and prefer the one with more ancestors.
42 | usort($matches, function (TokenColorMatchResult $a, TokenColorMatchResult $b): int {
43 | if ($a->scopeMatchResult->depth !== $b->scopeMatchResult->depth) {
44 | return $b->scopeMatchResult->depth - $a->scopeMatchResult->depth;
45 | }
46 |
47 | if ($a->scopeMatchResult->length !== $b->scopeMatchResult->length) {
48 | return $b->scopeMatchResult->length - $a->scopeMatchResult->length;
49 | }
50 |
51 | return $b->scopeMatchResult->ancestral - $a->scopeMatchResult->ancestral;
52 | });
53 |
54 | return TokenSettings::flatten(array_map(fn (TokenColorMatchResult $match) => $match->tokenColor->settings, $matches));
55 | }
56 |
57 | public function base(): TokenSettings
58 | {
59 | return new TokenSettings(
60 | $this->colors['editor.background'] ?? null,
61 | $this->colors['editor.foreground'] ?? null,
62 | null,
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Theme/ScopeMatchResult.php:
--------------------------------------------------------------------------------
1 | __DIR__."/../../resources/themes/{$this->value}.json",
74 | };
75 | }
76 |
77 | public function toParsedTheme(ThemeRepositoryInterface $repository): ParsedTheme
78 | {
79 | return $repository->get($this->value);
80 | }
81 |
82 | public static function parse(array $theme): ParsedTheme
83 | {
84 | return (new ThemeParser)->parse($theme);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/Theme/ThemeParser.php:
--------------------------------------------------------------------------------
1 | trim($part), explode(',', $part));
43 |
44 | foreach ($parts as $part) {
45 | $scope[] = new Scope(array_map(fn (string $p) => trim($p), explode(' ', $part)));
46 | }
47 | }
48 |
49 | return new TokenColor($scope, new TokenSettings(
50 | $tokenColor['settings']['background'] ?? null,
51 | $tokenColor['settings']['foreground'] ?? null,
52 | $tokenColor['settings']['fontStyle'] ?? null
53 | ));
54 | }, $theme['tokenColors']);
55 |
56 | return new ParsedTheme($name, $colors, $tokenColors);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Theme/ThemeRepository.php:
--------------------------------------------------------------------------------
1 | themes[$theme->value] = $theme->path();
16 | }
17 | }
18 |
19 | public function get(string $name): ParsedTheme
20 | {
21 | if (! $this->has($name)) {
22 | throw UnrecognisedThemeException::make($name);
23 | }
24 |
25 | $theme = $this->themes[$name];
26 |
27 | if ($theme instanceof ParsedTheme) {
28 | return $theme;
29 | }
30 |
31 | $parser = new ThemeParser;
32 |
33 | return $this->themes[$name] = $parser->parse(json_decode(file_get_contents($theme), true));
34 | }
35 |
36 | public function has(string $name): bool
37 | {
38 | return isset($this->themes[$name]);
39 | }
40 |
41 | public function register(string $name, string|ParsedTheme $pathOrTheme): void
42 | {
43 | $this->themes[$name] = $pathOrTheme;
44 | }
45 |
46 | public function resolve(string|Theme|ParsedTheme $theme): ParsedTheme
47 | {
48 | if ($theme instanceof ParsedTheme) {
49 | return $theme;
50 | }
51 |
52 | return match (true) {
53 | is_string($theme) => $this->get($theme),
54 | $theme instanceof Theme => $theme->toParsedTheme($this),
55 | };
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Theme/TokenColor.php:
--------------------------------------------------------------------------------
1 | scope as $scope) {
18 | if ($result = $scope->matches($scopes)) {
19 | return $result;
20 | }
21 | }
22 |
23 | return false;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Theme/TokenColorMatchResult.php:
--------------------------------------------------------------------------------
1 | $settings
18 | */
19 | public static function flatten(array $settings): TokenSettings
20 | {
21 | $flattened = [
22 | 'background' => null,
23 | 'foreground' => null,
24 | 'fontStyle' => null,
25 | ];
26 |
27 | foreach ($settings as $setting) {
28 | if (! isset($flattened['background']) && isset($setting->background)) {
29 | $flattened['background'] = $setting->background;
30 | }
31 |
32 | if (! isset($flattened['foreground']) && isset($setting->foreground)) {
33 | $flattened['foreground'] = $setting->foreground;
34 | }
35 |
36 | if (! isset($flattened['fontStyle']) && isset($setting->fontStyle)) {
37 | $flattened['fontStyle'] = $setting->fontStyle;
38 | }
39 | }
40 |
41 | return new TokenSettings(
42 | $flattened['background'],
43 | $flattened['foreground'],
44 | $flattened['fontStyle']
45 | );
46 | }
47 |
48 | public function toCssVarString(string $prefix): string
49 | {
50 | $styles = $this->toStyleArray();
51 | $vars = [];
52 |
53 | foreach ($styles as $property => $value) {
54 | $vars[] = "--phiki-{$prefix}-{$property}: {$value}";
55 | }
56 |
57 | return implode(';', $vars);
58 | }
59 |
60 | public function toStyleArray(): array
61 | {
62 | $styles = [];
63 |
64 | if (isset($this->background)) {
65 | $styles['background-color'] = $this->background;
66 | }
67 |
68 | if (isset($this->foreground)) {
69 | $styles['color'] = $this->foreground;
70 | }
71 |
72 | $fontStyles = explode(' ', $this->fontStyle ?? '');
73 |
74 | foreach ($fontStyles as $fontStyle) {
75 | if ($fontStyle === 'underline') {
76 | $styles['text-decoration'] = 'underline';
77 | }
78 |
79 | if ($fontStyle === 'italic') {
80 | $styles['font-style'] = 'italic';
81 | }
82 |
83 | if ($fontStyle === 'bold') {
84 | $styles['font-weight'] = 'bold';
85 | }
86 |
87 | if ($fontStyle === 'strikethrough') {
88 | $styles['text-decoration'] = 'line-through';
89 | }
90 | }
91 |
92 | return $styles;
93 | }
94 |
95 | public function toStyleString(): string
96 | {
97 | $styles = $this->toStyleArray();
98 | $styleString = '';
99 |
100 | foreach ($styles as $property => $value) {
101 | $styleString .= "{$property}: {$value};";
102 | }
103 |
104 | return $styleString;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/Token/HighlightedToken.php:
--------------------------------------------------------------------------------
1 | $settings
11 | */
12 | public function __construct(
13 | public Token $token,
14 | public array $settings,
15 | ) {}
16 | }
17 |
--------------------------------------------------------------------------------
/src/Token/Token.php:
--------------------------------------------------------------------------------
1 | > $tokens
30 | */
31 | public function tokens(array $tokens): array
32 | {
33 | return $tokens;
34 | }
35 |
36 | /**
37 | * Modify the highlighted tokens before they are converted into the HTML AST.
38 | *
39 | * @param array> $tokens
40 | */
41 | public function highlighted(array $tokens): array
42 | {
43 | return $tokens;
44 | }
45 |
46 | /**
47 | * Modify the root HTML element.
48 | */
49 | public function root(Root $root): Root
50 | {
51 | return $root;
52 | }
53 |
54 | /**
55 | * Modify the tag.
56 | */
57 | public function pre(Element $pre): Element
58 | {
59 | return $pre;
60 | }
61 |
62 | /**
63 | * Modify the tag.
64 | */
65 | public function code(Element $code): Element
66 | {
67 | return $code;
68 | }
69 |
70 | /**
71 | * Modify the for each line.
72 | *
73 | * @param array $tokens
74 | */
75 | public function line(Element $span, array $tokens, int $index): Element
76 | {
77 | return $span;
78 | }
79 |
80 | /**
81 | * Modify the for each token.
82 | */
83 | public function token(Element $span, HighlightedToken $token, int $index, int $line): Element
84 | {
85 | return $span;
86 | }
87 |
88 | /**
89 | * Modify the for line number.
90 | */
91 | public function gutter(Element $span, int $lineNumber): Element
92 | {
93 | return $span;
94 | }
95 |
96 | /**
97 | * Modify the HTML output after the AST has been converted.
98 | */
99 | public function postprocess(string $html): string
100 | {
101 | return $html;
102 | }
103 |
104 | /**
105 | * Store the meta object.
106 | */
107 | public function withMeta(Meta $meta): void
108 | {
109 | $this->meta = $meta;
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/Transformers/AddClassesTransformer.php:
--------------------------------------------------------------------------------
1 | ` for each token.
18 | */
19 | public function token(Element $span, HighlightedToken $token, int $index, int $line): Element
20 | {
21 | $classList = $span->properties->get('class') ?? new ClassList;
22 |
23 | foreach ($token->token->scopes as $scope) {
24 | $classList->add('phiki-'.$scope);
25 | }
26 |
27 | $span->properties->set('class', $classList);
28 |
29 | if (! $this->styles) {
30 | $span->properties->remove('style');
31 | }
32 |
33 | return $span;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Transformers/Concerns/RequiresGrammar.php:
--------------------------------------------------------------------------------
1 | grammar = $grammar;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Transformers/Concerns/RequiresThemes.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | protected array $themes;
11 |
12 | /**
13 | * @param array $themes
14 | */
15 | public function withThemes(array $themes): void
16 | {
17 | $this->themes = $themes;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Transformers/Decorations/CodeDecoration.php:
--------------------------------------------------------------------------------
1 | classes->add(...$classes);
21 |
22 | return $this;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Transformers/Decorations/DecorationTransformer.php:
--------------------------------------------------------------------------------
1 | $decorations
12 | */
13 | public function __construct(
14 | public array &$decorations,
15 | ) {}
16 |
17 | public function pre(Element $pre): Element
18 | {
19 | foreach ($this->decorations as $decoration) {
20 | if (! $decoration instanceof PreDecoration) {
21 | continue;
22 | }
23 |
24 | $pre->properties->get('class')->add(...$decoration->classes->all());
25 | }
26 |
27 | return $pre;
28 | }
29 |
30 | public function code(Element $code): Element
31 | {
32 | foreach ($this->decorations as $decoration) {
33 | if (! $decoration instanceof CodeDecoration) {
34 | continue;
35 | }
36 |
37 | $code->properties->get('class')->add(...$decoration->classes->all());
38 | }
39 |
40 | return $code;
41 | }
42 |
43 | public function line(Element $span, array $tokens, int $index): Element
44 | {
45 | foreach ($this->decorations as $decoration) {
46 | if (! $decoration instanceof LineDecoration) {
47 | continue;
48 | }
49 |
50 | if (! $decoration->appliesToLine($index)) {
51 | continue;
52 | }
53 |
54 | $span->properties->get('class')->add(...$decoration->classes->all());
55 | }
56 |
57 | return $span;
58 | }
59 |
60 | public function gutter(Element $span, int $lineNumber): Element
61 | {
62 | foreach ($this->decorations as $decoration) {
63 | if (! $decoration instanceof GutterDecoration) {
64 | continue;
65 | }
66 |
67 | $span->properties->get('class')->add(...$decoration->classes->all());
68 | }
69 |
70 | return $span;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Transformers/Decorations/GutterDecoration.php:
--------------------------------------------------------------------------------
1 | classes->add(...$classes);
21 |
22 | return $this;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Transformers/Decorations/LineDecoration.php:
--------------------------------------------------------------------------------
1 | $line
11 | */
12 | public function __construct(
13 | public int|array $line,
14 | public ClassList $classes,
15 | ) {}
16 |
17 | public static function forLine(int $line): self
18 | {
19 | return new self($line, new ClassList);
20 | }
21 |
22 | public function class(string ...$classes): self
23 | {
24 | $this->classes->add(...$classes);
25 |
26 | return $this;
27 | }
28 |
29 | public function appliesToLine(int $line): bool
30 | {
31 | return $this->line === $line || (is_array($this->line) && $line >= $this->line[0] && $line <= $this->line[1]);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Transformers/Decorations/PreDecoration.php:
--------------------------------------------------------------------------------
1 | classes->add(...$classes);
21 |
22 | return $this;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Transformers/Meta.php:
--------------------------------------------------------------------------------
1 |