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

tempestphp / tempest-framework / 14282305932

05 Apr 2025 01:19PM UTC coverage: 81.133% (+0.2%) from 80.906%
14282305932

Pull #1115

github

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

374 of 397 new or added lines in 16 files covered. (94.21%)

6 existing lines in 2 files now uncovered.

11361 of 14003 relevant lines covered (81.13%)

104.76 hits per line

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

95.16
/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 = substr($this->html, $this->position, $length);
143✔
70
        $this->position += $length;
143✔
71
        $this->current = $this->html[$this->position] ?? null;
143✔
72

73
        return $buffer;
143✔
74
    }
75

76
    private function consumeUntil(array|string|Closure $shouldStop): string
143✔
77
    {
78
        // Early checks for string values to optimize performance
79
        if (is_string($shouldStop)) {
143✔
80
            $found = strpos($this->html, $shouldStop, $this->position);
143✔
81

82
            if ($found !== false) {
143✔
83
                return $this->consume($found - $this->position);
134✔
84
            }
85
        } elseif(is_array($shouldStop)) {
133✔
86
            $earliestPosition = null;
133✔
87

88
            foreach ($shouldStop as $shouldStopEntry) {
133✔
89
                $found = strpos($this->html, $shouldStopEntry, $this->position);
133✔
90

91
                if (! $found) {
133✔
92
                    continue;
132✔
93
                }
94

95
                if ($earliestPosition === null) {
133✔
96
                    $earliestPosition = $found;
133✔
97
                    continue;
133✔
98
                }
99

100
                if ($earliestPosition > $found) {
128✔
101
                    $earliestPosition = $found;
93✔
102
                }
103
            }
104

105
            if ($earliestPosition) {
133✔
106
                return $this->consume($earliestPosition - $this->position);
133✔
107
            }
108
        }
109

110
        $buffer = '';
22✔
111

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

121
            $buffer .= $this->consume();
22✔
122
        }
123

124
        return $buffer;
22✔
125
    }
126

127
    private function consumeWhile(string|array $shouldContinue): string
114✔
128
    {
129
        $buffer = '';
114✔
130

131
        while ($this->current !== null) {
114✔
132
            if (is_string($shouldContinue) && $shouldContinue !== $this->current) {
114✔
NEW
133
                return $buffer;
×
134
            } elseif (! in_array($this->current, $shouldContinue)) {
114✔
135
                return $buffer;
114✔
136
            }
137

138
            $buffer .= $this->consume();
114✔
139
        }
140

NEW
141
        return $buffer;
×
142
    }
143

144
    private function consumeIncluding(string $search): string
134✔
145
    {
146
        return $this->consumeUntil($search) . $this->consume(strlen($search));
134✔
147
    }
148

149
    private function lexTag(): array
133✔
150
    {
151
        $tagBuffer = $this->consumeUntil([' ', PHP_EOL, '>']);
133✔
152

153
        $tokens = [];
133✔
154

155
        if (substr($tagBuffer, 1, 1) === '/') {
133✔
156
            $tagBuffer .= $this->consumeIncluding('>');
122✔
157
            $tokens[] = new Token($tagBuffer, TokenType::CLOSING_TAG);
122✔
158
        } elseif ($this->seekIgnoringWhitespace() === '/' || str_ends_with($tagBuffer, '/')) {
132✔
159
            $tagBuffer .= $this->consumeIncluding('>');
16✔
160
            $tokens[] = new Token($tagBuffer, TokenType::SELF_CLOSING_TAG);
16✔
161
        } else {
162
            $tokens[] = new Token($tagBuffer, TokenType::OPEN_TAG_START);
128✔
163

164
            while ($this->seek() !== null && $this->seekIgnoringWhitespace() !== '>' && $this->seekIgnoringWhitespace() !== '/') {
128✔
165
                if ($this->seekIgnoringWhitespace(2) === '<?') {
114✔
166
                    $tokens[] = $this->lexPhp();
2✔
167
                    continue;
2✔
168
                }
169

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

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

174
                $hasValue = $this->seek() === '=';
114✔
175

176
                if ($hasValue) {
114✔
177
                    $attributeName .= $this->consume();
114✔
178
                }
179

180
                $tokens[] = new Token(
114✔
181
                    content: $attributeName,
114✔
182
                    type: TokenType::ATTRIBUTE_NAME,
114✔
183
                );
114✔
184

185
                if ($hasValue) {
114✔
186
                    $attributeValue = $this->consumeIncluding('"');
114✔
187
                    $attributeValue .= $this->consumeIncluding('"');
114✔
188

189
                    $tokens[] = new Token(
114✔
190
                        content: $attributeValue,
114✔
191
                        type: TokenType::ATTRIBUTE_VALUE,
114✔
192
                    );
114✔
193
                }
194
            }
195

196
            if ($this->seekIgnoringWhitespace() === '>') {
128✔
197
                $tokens[] = new Token(
123✔
198
                    content: $this->consumeIncluding('>'),
123✔
199
                    type: TokenType::OPEN_TAG_END,
123✔
200
                );
123✔
201
            } elseif ($this->seekIgnoringWhitespace() === '/') {
24✔
202
                $tokens[] = new Token(
24✔
203
                    content: $this->consumeIncluding('>'),
24✔
204
                    type: TokenType::SELF_CLOSING_TAG_END,
24✔
205
                );
24✔
206
            }
207
        }
208

209
        return $tokens;
133✔
210
    }
211

212
    private function lexPhp(): Token
80✔
213
    {
214
        $buffer = $this->consumeIncluding('?>');
80✔
215

216
        return new Token($buffer, TokenType::PHP);
80✔
217
    }
218

219
    private function lexContent(): Token
108✔
220
    {
221
        $buffer = $this->consumeUntil('<');
108✔
222

223
        return new Token($buffer, TokenType::CONTENT);
108✔
224
    }
225

226
    private function lexComment(): Token
10✔
227
    {
228
        $buffer = $this->consumeIncluding('-->');
10✔
229

230
        return new Token($buffer, TokenType::COMMENT);
10✔
231
    }
232

233
    private function lexDoctype(): Token
5✔
234
    {
235
        $buffer = $this->consumeIncluding('>');
5✔
236

237
        return new Token($buffer, TokenType::DOCTYPE);
5✔
238
    }
239
}
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