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

tempestphp / tempest-framework / 14280395039

05 Apr 2025 09:03AM UTC coverage: 81.14% (+0.2%) from 80.906%
14280395039

Pull #1115

github

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

380 of 404 new or added lines in 16 files covered. (94.06%)

6 existing lines in 2 files now uncovered.

11371 of 14014 relevant lines covered (81.14%)

104.77 hits per line

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

94.81
/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

13
    public function __construct(
143✔
14
        private string $html,
15
    ) {
16
        $this->current = $this->html[$this->position] ?? null;
143✔
17
    }
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
        // Early checks for string values to optimize performance
83
        if (is_string($shouldStop)) {
143✔
84
            $found = strpos($this->html, $shouldStop, $this->position);
142✔
85

86
            if ($found !== false) {
142✔
87
                return $this->consume($found - $this->position);
132✔
88
            }
89
        } elseif(is_array($shouldStop)) {
134✔
90
            $earliestPosition = null;
133✔
91

92
            foreach ($shouldStop as $shouldStopEntry) {
133✔
93
                $found = strpos($this->html, $shouldStopEntry, $this->position);
133✔
94

95
                if (! $found) {
133✔
96
                    continue;
132✔
97
                }
98

99
                if ($earliestPosition === null) {
133✔
100
                    $earliestPosition = $found;
133✔
101
                    continue;
133✔
102
                }
103

104
                if ($earliestPosition > $found) {
128✔
105
                    $earliestPosition = $found;
93✔
106
                }
107
            }
108

109
            if ($earliestPosition) {
133✔
110
                return $this->consume($earliestPosition - $this->position);
133✔
111
            }
112
        }
113

114
        $buffer = '';
91✔
115

116
        while ($this->current !== null) {
91✔
117
            if (is_string($shouldStop) && $shouldStop === $this->current) {
91✔
NEW
118
                return $buffer;
×
119
            } elseif (is_array($shouldStop) && in_array($this->current, $shouldStop)) {
91✔
NEW
120
                return $buffer;
×
121
            } elseif ($shouldStop instanceof Closure && $shouldStop($this->current)) {
91✔
122
                return $buffer;
82✔
123
            }
124

125
            $buffer .= $this->consume();
91✔
126
        }
127

128
        return $buffer;
22✔
129
    }
130

131
    private function consumeWhile(string|array $shouldContinue): string
114✔
132
    {
133
        $buffer = '';
114✔
134

135
        while ($this->current !== null) {
114✔
136
            if (is_string($shouldContinue) && $shouldContinue !== $this->current) {
114✔
NEW
137
                return $buffer;
×
138
            } elseif (! in_array($this->current, $shouldContinue)) {
114✔
139
                return $buffer;
114✔
140
            }
141

142
            $buffer .= $this->consume();
114✔
143
        }
144

NEW
145
        return $buffer;
×
146
    }
147

NEW
148
    private function consumeIncluding(string $character): string
×
149
    {
NEW
150
        return $this->consumeUntil($character) . $this->consume();
×
151
    }
152

153
    private function advance(int $amount = 1): void
143✔
154
    {
155
        $this->position += $amount;
143✔
156
        $this->current = $this->html[$this->position] ?? null;
143✔
157
    }
158

159
    private function lexTag(): array
133✔
160
    {
161
        $tagBuffer = $this->consumeUntil([' ', PHP_EOL, '>']);
133✔
162

163
        $tokens = [];
133✔
164

165
        if (substr($tagBuffer, 1, 1) === '/') {
133✔
166
            $tagBuffer .= $this->consume();
122✔
167
            $tokens[] = new Token($tagBuffer, TokenType::CLOSING_TAG);
122✔
168
        } elseif ($this->seekIgnoringWhitespace() === '/' || str_ends_with($tagBuffer, '/')) {
132✔
169
            $tagBuffer .= $this->consumeUntil('>');
16✔
170
            $tagBuffer .= $this->consume();
16✔
171
            $tokens[] = new Token($tagBuffer, TokenType::SELF_CLOSING_TAG);
16✔
172
        } else {
173
            $tokens[] = new Token($tagBuffer, TokenType::OPEN_TAG_START);
128✔
174

175
            while ($this->seek() !== null && $this->seekIgnoringWhitespace() !== '>' && $this->seekIgnoringWhitespace() !== '/') {
128✔
176
                if ($this->seekIgnoringWhitespace(2) === '<?') {
114✔
177
                    $tokens[] = $this->lexPhp();
2✔
178
                    continue;
2✔
179
                }
180

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

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

185
                $hasValue = $this->seek() === '=';
114✔
186

187
                if ($hasValue) {
114✔
188
                    $attributeName .= $this->consume();
114✔
189
                }
190

191
                $tokens[] = new Token(
114✔
192
                    content: $attributeName,
114✔
193
                    type: TokenType::ATTRIBUTE_NAME,
114✔
194
                );
114✔
195

196
                if ($hasValue) {
114✔
197
                    $attributeValue = $this->consumeUntil('"');
114✔
198
                    $attributeValue .= $this->consume();
114✔
199
                    $attributeValue .= $this->consumeUntil('"');
114✔
200
                    $attributeValue .= $this->consume();
114✔
201

202
                    $tokens[] = new Token(
114✔
203
                        content: $attributeValue,
114✔
204
                        type: TokenType::ATTRIBUTE_VALUE,
114✔
205
                    );
114✔
206
                }
207
            }
208

209
            if ($this->seekIgnoringWhitespace() === '>') {
128✔
210
                $tokens[] = new Token(
123✔
211
                    content: $this->consumeUntil('>') . $this->consume(),
123✔
212
                    type: TokenType::OPEN_TAG_END,
123✔
213
                );
123✔
214
            } elseif ($this->seekIgnoringWhitespace() === '/') {
24✔
215
                $tokens[] = new Token(
24✔
216
                    content: $this->consumeUntil('>') . $this->consume(),
24✔
217
                    type: TokenType::SELF_CLOSING_TAG_END,
24✔
218
                );
24✔
219
            }
220
        }
221

222
        return $tokens;
133✔
223
    }
224

225
    private function lexPhp(): Token
80✔
226
    {
227
        $buffer = $this->consumeUntil(fn () => $this->seek(2) === '?>');
80✔
228

229
        $buffer .= $this->consume(2);
80✔
230

231
        return new Token($buffer, TokenType::PHP);
80✔
232
    }
233

234
    private function lexContent(): Token
108✔
235
    {
236
        $buffer = $this->consumeUntil('<');
108✔
237

238
        return new Token($buffer, TokenType::CONTENT);
108✔
239
    }
240

241
    private function lexComment(): Token
10✔
242
    {
243
        $buffer = $this->consumeUntil(fn () => $this->seek(3) === '-->');
10✔
244

245
        $buffer .= $this->consume(3);
10✔
246

247
        return new Token($buffer, TokenType::COMMENT);
10✔
248
    }
249

250
    private function lexDoctype(): Token
5✔
251
    {
252
        $buffer = $this->consumeUntil('>');
5✔
253
        $buffer .= $this->consume();
5✔
254

255
        return new Token($buffer, TokenType::DOCTYPE);
5✔
256
    }
257
}
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