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

tempestphp / tempest-framework / 14279067431

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

Pull #1115

github

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

357 of 375 new or added lines in 16 files covered. (95.2%)

6 existing lines in 2 files now uncovered.

11344 of 13981 relevant lines covered (81.14%)

104.8 hits per line

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

99.02
/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(Closure $shouldStop): string
143✔
81
    {
82
        $buffer = '';
143✔
83

84
        while ($this->current !== null && $shouldStop($this->seek()) === false) {
143✔
85
            $buffer .= $this->consume();
143✔
86
        }
87

88
        return $buffer;
143✔
89
    }
90

91
    private function consumeWhile(Closure $shouldContinue): string
114✔
92
    {
93
        return $this->consumeUntil(fn (string $next) => $shouldContinue($next) === false);
114✔
94
    }
95

96
    private function advance(): void
143✔
97
    {
98
        $this->position++;
143✔
99
    }
100

101
    private function lexTag(): array
133✔
102
    {
103
        $tagBuffer = $this->consumeUntil(fn (string $next) => $next === '>' || $next === ' ' || $next === PHP_EOL);
133✔
104

105
        $tokens = [];
133✔
106

107
        if (substr($tagBuffer, 1, 1) === '/') {
133✔
108
            $tagBuffer .= $this->consume();
122✔
109
            $tokens[] = new Token($tagBuffer, TokenType::CLOSING_TAG);
122✔
110
        } elseif ($this->seekIgnoringWhitespace() === '/' || str_ends_with($tagBuffer, '/')) {
132✔
111
            $tagBuffer .= $this->consumeUntil(fn (string $next) => $next === '>');
16✔
112
            $tagBuffer .= $this->consume();
16✔
113
            $tokens[] = new Token($tagBuffer, TokenType::SELF_CLOSING_TAG);
16✔
114
        } else {
115
            $tokens[] = new Token($tagBuffer, TokenType::OPEN_TAG_START);
128✔
116

117
            while ($this->seek() !== null && $this->seekIgnoringWhitespace() !== '>' && $this->seekIgnoringWhitespace() !== '/') {
128✔
118
                if ($this->seekIgnoringWhitespace(2) === '<?') {
114✔
119
                    $tokens[] = $this->lexPhp();
2✔
120
                    continue;
2✔
121
                }
122

123
                $attributeName = $this->consumeWhile(fn (string $next) => $next === ' ' || $next === PHP_EOL);
114✔
124

125
                $attributeName .= $this->consumeUntil(fn (string $next) => $next === '=' || $next === ' ' || $next === '>');
114✔
126

127
                $hasValue = $this->seek() === '=';
114✔
128

129
                if ($hasValue) {
114✔
130
                    $attributeName .= $this->consume();
114✔
131
                }
132

133
                $tokens[] = new Token(
114✔
134
                    content: $attributeName,
114✔
135
                    type: TokenType::ATTRIBUTE_NAME,
114✔
136
                );
114✔
137

138
                if ($hasValue) {
114✔
139
                    $attributeValue = $this->consumeUntil(fn (string $next) => $next === '"');
114✔
140
                    $attributeValue .= $this->consume();
114✔
141
                    $attributeValue .= $this->consumeUntil(fn (string $next) => $next === '"');
114✔
142
                    $attributeValue .= $this->consume();
114✔
143

144
                    $tokens[] = new Token(
114✔
145
                        content: $attributeValue,
114✔
146
                        type: TokenType::ATTRIBUTE_VALUE,
114✔
147
                    );
114✔
148
                }
149
            }
150

151
            if ($this->seekIgnoringWhitespace() === '>') {
128✔
152
                $tokens[] = new Token(
123✔
153
                    content: $this->consumeUntil(fn (string $next) => $next === '>') . $this->consume(),
123✔
154
                    type: TokenType::OPEN_TAG_END,
123✔
155
                );
123✔
156
            } elseif ($this->seekIgnoringWhitespace() === '/') {
24✔
157
                $tokens[] = new Token(
24✔
158
                    content: $this->consumeUntil(fn (string $next) => $next === '>') . $this->consume(),
24✔
159
                    type: TokenType::SELF_CLOSING_TAG_END,
24✔
160
                );
24✔
161
            }
162
        }
163

164
        return $tokens;
133✔
165
    }
166

167
    private function lexPhp(): Token
80✔
168
    {
169
        $buffer = $this->consumeUntil(fn () => $this->seek(2) === '?>');
80✔
170

171
        $buffer .= $this->consume(2);
80✔
172

173
        return new Token($buffer, TokenType::PHP);
80✔
174
    }
175

176
    private function lexContent(): Token
108✔
177
    {
178
        $buffer = $this->consumeUntil(fn () => $this->seek() === '<');
108✔
179

180
        return new Token($buffer, TokenType::CONTENT);
108✔
181
    }
182

183
    private function lexComment(): Token
10✔
184
    {
185
        $buffer = $this->consumeUntil(fn () => $this->seek(3) === '-->');
10✔
186

187
        $buffer .= $this->consume(3);
10✔
188

189
        return new Token($buffer, TokenType::COMMENT);
10✔
190
    }
191

192
    private function lexDoctype(): Token
5✔
193
    {
194
        $buffer = $this->consumeUntil(fn (string $next) => $next === '>');
5✔
195
        $buffer .= $this->consume();
5✔
196

197
        return new Token($buffer, TokenType::DOCTYPE);
5✔
198
    }
199
}
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