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

tempestphp / tempest-framework / 14280422660

05 Apr 2025 09:06AM UTC coverage: 81.139% (+0.2%) from 80.906%
14280422660

Pull #1115

github

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

379 of 402 new or added lines in 16 files covered. (94.28%)

6 existing lines in 2 files now uncovered.

11366 of 14008 relevant lines covered (81.14%)

104.77 hits per line

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

95.35
/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);
133✔
88
            }
89
        } elseif(is_array($shouldStop)) {
133✔
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 = '';
22✔
115

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

125
            $buffer .= $this->consume();
22✔
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

148
    private function consumeIncluding(string $search): string
133✔
149
    {
150
        return $this->consumeUntil($search) . $this->consume(strlen($search));
133✔
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->consumeIncluding('>');
16✔
170
            $tokens[] = new Token($tagBuffer, TokenType::SELF_CLOSING_TAG);
16✔
171
        } else {
172
            $tokens[] = new Token($tagBuffer, TokenType::OPEN_TAG_START);
128✔
173

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

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

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

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

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

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

195
                if ($hasValue) {
114✔
196
                    $attributeValue = $this->consumeIncluding('"');
114✔
197
                    $attributeValue .= $this->consumeIncluding('"');
114✔
198

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

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

219
        return $tokens;
133✔
220
    }
221

222
    private function lexPhp(): Token
80✔
223
    {
224
        $buffer = $this->consumeIncluding('?>');
80✔
225

226
        return new Token($buffer, TokenType::PHP);
80✔
227
    }
228

229
    private function lexContent(): Token
108✔
230
    {
231
        $buffer = $this->consumeUntil('<');
108✔
232

233
        return new Token($buffer, TokenType::CONTENT);
108✔
234
    }
235

236
    private function lexComment(): Token
10✔
237
    {
238
        $buffer = $this->consumeIncluding('-->');
10✔
239

240
        return new Token($buffer, TokenType::COMMENT);
10✔
241
    }
242

243
    private function lexDoctype(): Token
5✔
244
    {
245
        $buffer = $this->consumeIncluding('>');
5✔
246

247
        return new Token($buffer, TokenType::DOCTYPE);
5✔
248
    }
249
}
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