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

lmc-eu / twigx-bundle / 4329044787

pending completion
4329044787

push

github

literat
Release 3.2.0

80 of 94 relevant lines covered (85.11%)

25.66 hits per line

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

92.59
/src/Compiler/ComponentTagCompiler.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Lmc\TwigXBundle\Compiler;
6

7
/**
8
 * Transforms <Tags /> to twig embed tags
9
 * Most parts shamelessly stolen form Laravel's ComponentTagCompiler {@link https://laravel.com/api/8.x/Illuminate/View/Compilers/ComponentTagCompiler.html}
10
 */
11
class ComponentTagCompiler
12
{
13
    protected string $source;
14

15
    private string $twigPathAlias;
16

17
    public function __construct(string $source, string $twigPathAlias)
18
    {
19
        $this->source = $source;
48✔
20
        $this->twigPathAlias = $twigPathAlias;
48✔
21
    }
22

23
    public function compile(): string
24
    {
25
        $value = $this->source;
48✔
26
        $value = $this->compileSelfClosingTags($value);
48✔
27
        $value = $this->compileOpeningTags($value);
48✔
28

29
        return $this->compileClosingTags($value);
48✔
30
    }
31

32
    /**
33
     * Compile the opening tags within the given string.
34
     */
35
    protected function compileOpeningTags(string $value): string
36
    {
37
        $pattern = "/
48✔
38
            <
39
                \s*
40
                ([[A-Z]\w+]*)
41
                (?<attributes>
42
                    (?:
43
                        \s+
44
                        (?:
45
                            (?:
46
                                \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
47
                            )
48
                            |
49
                            (?!\#\}).+?                                 # Use negative lookahead to match until the first occurrence of #}
50
                            |
51
                            (?:
52
                                [\w\-:.@]+
53
                                (
54
                                    =
55
                                    (?:
56
                                        \\\"[^\\\"]*\\\"
57
                                        |
58
                                        \'[^\']*\'
59
                                        |
60
                                        [^\'\\\"=<>]+
61
                                    )
62
                                )?
63
                            )
64
                        )
65
                    )*
66
                    \s*
67
                )
68
                (?<![\/=\-])
69
            >
70
        /x";
36✔
71

72
        return (string) preg_replace_callback(
48✔
73
            $pattern,
36✔
74
            function (array $matches) {
48✔
75
                $attributes = $this->getAttributesFromAttributeString($matches['attributes']);
16✔
76
                $name = $matches[1];
16✔
77

78
                return $this->componentStartString($name, $attributes) . '{% block content %}';
16✔
79
            },
36✔
80
            $value
36✔
81
        );
36✔
82
    }
83

84
    protected function componentStartString(string $component, string $attributes): string
85
    {
86
        return "{% embed \"@" . $this->twigPathAlias . "/" . lcfirst($component) . ".twig\" with { props: $attributes } %}";
48✔
87
    }
88

89
    /**
90
     * Compile the closing tags within the given string.
91
     */
92
    protected function compileClosingTags(string $value): string
93
    {
94
        return (string) preg_replace("/<\/\s*([[A-Z]\w+]*)\s*>/", '{% endblock %}{% endembed %}', $value);
48✔
95
    }
96

97
    /**
98
     * Compile the self-closing tags within the given string.
99
     */
100
    protected function compileSelfClosingTags(string $value): string
101
    {
102
        $pattern = "/
48✔
103
            <
104
                \s*
105
                ([[A-Z]\w+]*)                                            # First capturing group - component name
106
                (?<attributes>                                           # Named capturing group - attributes
107
                    (?:                                                  # Non-capturing group
108
                        \s+                                              # Matches zero or more whitespace character
109
                        (?:                                              # Non-capturing group
110
                            (?:                                          # Non-capturing group
111
                                \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}  # Matches attributes between {{ and }}
112
                            )                                            # End of non-capturing group
113
                            |                                            # or
114
                            (?:\{\#.*\#\})                               # Matches Twig comments, non-capturing
115
                            |                                            # or
116
                            (?:                                          # Non-capturing group
117
                                [\w\-:.@]+                               # Matches list of one or more words and characters -:.@ literally
118
                                (                                        # Third capturing group
119
                                    =                                    # Matches the character =
120
                                    (?:                                  # Non-capturing group
121
                                        \\\"[^\\\"]*\\\"                 # Matches list of characters
122
                                        |                                # or
123
                                        \'[^\']*\'                       # Matches list of characters
124
                                        |                                # or
125
                                        [^\'\\\"=<>]+                    # Matches list of characters
126
                                    )                                    # End of non-capturing group
127
                                )?                                       # Matches group zero or one time
128
                            )                                            # End of non-capturing group
129
                        )                                                # End of non-capturing group
130
                    )*                                                   # End of non-capturing group
131
                    \s*                                                  # Matches whitespace character zero or more
132
                )                                                        # End of non-capturing group
133
            \/>                                                          # Matches string literally
134
        /x";
36✔
135

136
        return (string) preg_replace_callback(
48✔
137
            $pattern,
36✔
138
            function (array $matches) {
48✔
139
                $attributes = $this->getAttributesFromAttributeString($matches['attributes']);
32✔
140
                $name = $matches[1];
32✔
141

142
                return $this->componentStartString($name, $attributes) . '{% endembed %}';
32✔
143
            },
36✔
144
            $value
36✔
145
        );
36✔
146
    }
147

148
    private function valueParser(?string $value, string $attribute): string
149
    {
150
        if ($value === null) {
48✔
151
            return 'true';
×
152
        }
153

154
        // enable an argument that begins with a colon
155
        if (\str_starts_with($attribute, ':')) {
48✔
156
            return $this->stripQuotes($value);
×
157
        }
158

159
        $valueWithoutQuotes = $this->stripQuotes($value);
48✔
160

161
        if (\str_starts_with($valueWithoutQuotes, '{{') && (mb_strpos($valueWithoutQuotes, '}}') === mb_strlen($valueWithoutQuotes) - 2)) {
48✔
162
            return mb_substr($valueWithoutQuotes, 2, -2);
36✔
163
        }
164

165
        if (\str_starts_with($valueWithoutQuotes, '{') && (mb_strpos($valueWithoutQuotes, '}') === mb_strlen($valueWithoutQuotes) - 1)) {
12✔
166
            return trim(mb_substr($valueWithoutQuotes, 1, -1));
8✔
167
        }
168

169
        return $value;
4✔
170
    }
171

172
    protected function getAttributesFromAttributeString(string $attributeString): string
173
    {
174
        $attributeString = $this->parseAttributeBag($attributeString);
48✔
175
        $attributeString = $this->pourOutCommentsFromAttributeBag($attributeString);
48✔
176

177
        $pattern = '/
48✔
178
            (?<attribute>[\w\-:.@]+)
179
            (
180
                =
181
                (?<value>
182
                    (
183
                        \"[^\"]+\"
184
                        |
185
                        \\\'[^\\\']+\\\'
186
                        |
187
                        \{[^\{]+\}
188
                        |
189
                        [^\s>]+
190
                    )
191
                )
192
            )?
193
        /x';
36✔
194

195
        if (! preg_match_all($pattern, $attributeString, $matches, PREG_SET_ORDER)) {
48✔
196
            return '{}';
×
197
        }
198

199
        $attributes = [];
48✔
200

201
        foreach ($matches as $match) {
48✔
202
            $attribute = $match['attribute'];
48✔
203
            $value = $this->valueParser($match['value'] ?? null, $attribute);
48✔
204

205
            // enable an argument that begins with a colon
206
            if (\str_starts_with($attribute, ':')) {
48✔
207
                $attribute = str_replace(':', '', $attribute);
×
208
            }
209

210
            $attributes[$attribute] = $value;
48✔
211
        }
212

213
        $out = '{';
48✔
214
        foreach ($attributes as $key => $value) {
48✔
215
            $key = "'$key'";
48✔
216
            $out .= "$key: $value,";
48✔
217
        }
218

219
        return rtrim($out, ',') . '}';
48✔
220
    }
221

222
    /**
223
     * Strip any quotes from the given string.
224
     */
225
    public function stripQuotes(string $value): string
226
    {
227
        return \str_starts_with($value, '"') || \str_starts_with($value, '\'')
48✔
228
            ? mb_substr($value, 1, -1)
32✔
229
            : $value;
48✔
230
    }
231

232
    /**
233
     * Parse the attribute bag in a given attribute string into it's fully-qualified syntax.
234
     */
235
    protected function parseAttributeBag(string $attributeString): string
236
    {
237
        $pattern = "/
48✔
238
            (?:^|\s+)                                        # start of the string or whitespace between attributes
239
            \{\{\s*(\\\$attributes(?:[^}]+?(?<!\s))?)\s*\}\} # exact match of attributes variable being echoed
240
        /x";
36✔
241

242
        return (string) preg_replace($pattern, ' :attributes="$1"', $attributeString);
48✔
243
    }
244

245
    /**
246
     * Remove Twig comments from given attribute string
247
     */
248
    protected function pourOutCommentsFromAttributeBag(string $attributeString): string
249
    {
250
        $pattern = "/
48✔
251
            (                                               # First capturing group
252
                \{\#                                        # Match {# literally
253
                    (                                       # Second capturing group
254
                        (?!\#\}).+?                         # Use negative lookahead to match until the first occurrence of #}
255
                    )                                       # End of second capturing group
256
                \#\}                                        # Match #} literally
257
            )                                               # End of first capturing group
258
        /x";
36✔
259

260
        return (string) preg_replace($pattern, '', $attributeString);
48✔
261
    }
262
}
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