• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tempestphp / tempest-framework / 14280128929

05 Apr 2025 08:26AM UTC coverage: 81.142% (+0.2%) from 80.906%
14280128929

Pull #1115

github

web-flow
Merge 79f29ed19 into 90e820853
Pull Request #1115: refactor(view): implement custom html parser

368 of 388 new or added lines in 16 files covered. (94.85%)

6 existing lines in 2 files now uncovered.

11355 of 13994 relevant lines covered (81.14%)

104.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.39
/src/Tempest/View/src/Parser/TempestViewLexer.php
1
<?php
2

3
namespace Tempest\View\Parser;
4

5
use Closure;
6

7
final class TempestViewLexer
8
{
9
    private int $position = 0;
10

11
    private ?string $current {
12
        get => $this->html[$this->position] ?? null;
13
    }
14

15
    public function __construct(
143✔
16
        private string $html,
17
    ) {}
143✔
18

19
    public function lex(): TokenCollection
143✔
20
    {
21
        $tokens = [];
143✔
22

23
        while ($this->current !== null) {
143✔
24
            if ($this->seek(2) === '<?') {
143✔
25
                $tokens[] = $this->lexPhp();
79✔
26
            } elseif ($this->seek(4) === '<!--') {
143✔
27
                $tokens[] = $this->lexComment();
10✔
28
            } elseif ($this->comesNext('<!doctype') || $this->comesNext('<!DOCTYPE')) {
143✔
29
                $tokens[] = $this->lexDocType();
5✔
30
            } elseif ($this->seek() === '<') {
143✔
31
                $tokens = [...$tokens, ...$this->lexTag()];
133✔
32
            } else {
33
                $tokens[] = $this->lexContent();
108✔
34
            }
35
        }
36

37
        return new TokenCollection($tokens);
143✔
38
    }
39

40
    private function comesNext(string $search): bool
143✔
41
    {
42
        return $this->seek(strlen($search)) === $search;
143✔
43
    }
44

45
    private function seek(int $length = 1, int $offset = 0): ?string
143✔
46
    {
47
        $seek = substr($this->html, $this->position + $offset, $length);
143✔
48

49
        if ($seek === '') {
143✔
NEW
50
            return null;
×
51
        }
52

53
        return $seek;
143✔
54
    }
55

56
    private function seekIgnoringWhitespace(int $length = 1): ?string
132✔
57
    {
58
        $offset = 0;
132✔
59

60
        while (trim($this->seek(offset: $offset)) === '') {
132✔
61
            $offset += 1;
117✔
62
        }
63

64
        return $this->seek(length: $length, offset: $offset);
132✔
65
    }
66

67
    private function consume(int $length = 1): string
143✔
68
    {
69
        $buffer = '';
143✔
70

71
        while ($length) {
143✔
72
            $buffer .= $this->current;
143✔
73
            $this->advance();
143✔
74
            $length--;
143✔
75
        }
76

77
        return $buffer;
143✔
78
    }
79

80
    private function consumeUntil(array|string|Closure $shouldStop): string
143✔
81
    {
82
        $buffer = '';
143✔
83

84
        while ($this->current !== null) {
143✔
85
            if (is_string($shouldStop) && $shouldStop === $this->current) {
143✔
86
                return $buffer;
132✔
87
            } elseif (is_array($shouldStop) && in_array($this->current, $shouldStop)) {
143✔
88
                return $buffer;
133✔
89
            } elseif ($shouldStop instanceof Closure && $shouldStop($this->current)) {
143✔
90
                return $buffer;
82✔
91
            }
92

93
            $buffer .= $this->consume();
143✔
94
        }
95

96
        return $buffer;
22✔
97
    }
98

99
    private function consumeWhile(string|array $shouldContinue): string
114✔
100
    {
101
        $buffer = '';
114✔
102

103
        while ($this->current !== null) {
114✔
104
            if (is_string($shouldContinue) && $shouldContinue !== $this->current) {
114✔
NEW
105
                return $buffer;
×
106
            } elseif (! in_array($this->current, $shouldContinue)) {
114✔
107
                return $buffer;
114✔
108
            }
109

110
            $buffer .= $this->consume();
114✔
111
        }
112

NEW
113
        return $buffer;
×
114
    }
115

116
    private function advance(): void
143✔
117
    {
118
        $this->position++;
143✔
119
    }
120

121
    private function lexTag(): array
133✔
122
    {
123
        $tagBuffer = $this->consumeUntil(['>', ' ', PHP_EOL]);
133✔
124

125
        $tokens = [];
133✔
126

127
        if (substr($tagBuffer, 1, 1) === '/') {
133✔
128
            $tagBuffer .= $this->consume();
122✔
129
            $tokens[] = new Token($tagBuffer, TokenType::CLOSING_TAG);
122✔
130
        } elseif ($this->seekIgnoringWhitespace() === '/' || str_ends_with($tagBuffer, '/')) {
132✔
131
            $tagBuffer .= $this->consumeUntil('>');
16✔
132
            $tagBuffer .= $this->consume();
16✔
133
            $tokens[] = new Token($tagBuffer, TokenType::SELF_CLOSING_TAG);
16✔
134
        } else {
135
            $tokens[] = new Token($tagBuffer, TokenType::OPEN_TAG_START);
128✔
136

137
            while ($this->seek() !== null && $this->seekIgnoringWhitespace() !== '>' && $this->seekIgnoringWhitespace() !== '/') {
128✔
138
                if ($this->seekIgnoringWhitespace(2) === '<?') {
114✔
139
                    $tokens[] = $this->lexPhp();
2✔
140
                    continue;
2✔
141
                }
142

143
                $attributeName = $this->consumeWhile([' ', PHP_EOL]);
114✔
144

145
                $attributeName .= $this->consumeUntil(['=', ' ', '>']);
114✔
146

147
                $hasValue = $this->seek() === '=';
114✔
148

149
                if ($hasValue) {
114✔
150
                    $attributeName .= $this->consume();
114✔
151
                }
152

153
                $tokens[] = new Token(
114✔
154
                    content: $attributeName,
114✔
155
                    type: TokenType::ATTRIBUTE_NAME,
114✔
156
                );
114✔
157

158
                if ($hasValue) {
114✔
159
                    $attributeValue = $this->consumeUntil('"');
114✔
160
                    $attributeValue .= $this->consume();
114✔
161
                    $attributeValue .= $this->consumeUntil('"');
114✔
162
                    $attributeValue .= $this->consume();
114✔
163

164
                    $tokens[] = new Token(
114✔
165
                        content: $attributeValue,
114✔
166
                        type: TokenType::ATTRIBUTE_VALUE,
114✔
167
                    );
114✔
168
                }
169
            }
170

171
            if ($this->seekIgnoringWhitespace() === '>') {
128✔
172
                $tokens[] = new Token(
123✔
173
                    content: $this->consumeUntil('>') . $this->consume(),
123✔
174
                    type: TokenType::OPEN_TAG_END,
123✔
175
                );
123✔
176
            } elseif ($this->seekIgnoringWhitespace() === '/') {
24✔
177
                $tokens[] = new Token(
24✔
178
                    content: $this->consumeUntil('>') . $this->consume(),
24✔
179
                    type: TokenType::SELF_CLOSING_TAG_END,
24✔
180
                );
24✔
181
            }
182
        }
183

184
        return $tokens;
133✔
185
    }
186

187
    private function lexPhp(): Token
80✔
188
    {
189
        $buffer = $this->consumeUntil(fn () => $this->seek(2) === '?>');
80✔
190

191
        $buffer .= $this->consume(2);
80✔
192

193
        return new Token($buffer, TokenType::PHP);
80✔
194
    }
195

196
    private function lexContent(): Token
108✔
197
    {
198
        $buffer = $this->consumeUntil('<');
108✔
199

200
        return new Token($buffer, TokenType::CONTENT);
108✔
201
    }
202

203
    private function lexComment(): Token
10✔
204
    {
205
        $buffer = $this->consumeUntil(fn () => $this->seek(3) === '-->');
10✔
206

207
        $buffer .= $this->consume(3);
10✔
208

209
        return new Token($buffer, TokenType::COMMENT);
10✔
210
    }
211

212
    private function lexDoctype(): Token
5✔
213
    {
214
        $buffer = $this->consumeUntil('>');
5✔
215
        $buffer .= $this->consume();
5✔
216

217
        return new Token($buffer, TokenType::DOCTYPE);
5✔
218
    }
219
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc