1
+ <?php
2
+
3
+ namespace Symfony \UX \TwigComponent \Twig ;
4
+
5
+ use Twig \Lexer ;
6
+ use Twig \Source ;
7
+ use Twig \TokenStream ;
8
+
9
+ class ComponentLexer extends Lexer
10
+ {
11
+ const ATTRIBUTES_REGEX = '(?<attributes>(?:\s+[\w\-:.@]+(=(?: \\\"[^ \\\"]* \\\"| \'[^ \']* \'|[^ \'\\\"=<>]+))?)*\s*) ' ;
12
+ const COMPONENTS_REGEX = [
13
+ 'open_tags ' => '/<\s*([A-Z][\w\-\:\.]+)\s* ' . self ::ATTRIBUTES_REGEX . '(\s?)+>/ ' ,
14
+ 'close_tags ' => '/<\/\s*([A-Z][\w\-\:\.]+)\s*>/ ' ,
15
+ 'self_close_tags ' => '/<\s*([A-Z][\w\-\:\.]+)\s* ' . self ::ATTRIBUTES_REGEX . '*(\s?)+\/>/ ' ,
16
+ ];
17
+
18
+ public function tokenize (Source $ source ): TokenStream
19
+ {
20
+ $ preparsed = $ this ->preparsed ($ source ->getCode ());
21
+
22
+ return parent ::tokenize (
23
+ new Source (
24
+ $ preparsed ,
25
+ $ source ->getName (),
26
+ $ source ->getPath ()
27
+ )
28
+ );
29
+ }
30
+
31
+ private function preparsed (string $ value )
32
+ {
33
+ $ value = $ this ->lexSelfCloseTag ($ value );
34
+ $ value = $ this ->lexOpeningTags ($ value );
35
+ $ value = $ this ->lexClosingTag ($ value );
36
+
37
+ return $ value ;
38
+ }
39
+
40
+ private function lexOpeningTags (string $ value )
41
+ {
42
+ return preg_replace_callback (
43
+ self ::COMPONENTS_REGEX ['open_tags ' ],
44
+ function (array $ matches ) {
45
+ $ name = lcfirst ($ matches [1 ]);
46
+ $ attributes = $ this ->getAttributesFromAttributeString ($ matches ['attributes ' ]);
47
+
48
+ return "{% component " . $ name . " with " . $ attributes . "%} " ;
49
+ },
50
+ $ value
51
+
52
+ );
53
+ }
54
+
55
+ private function lexClosingTag (string $ value )
56
+ {
57
+ return preg_replace (self ::COMPONENTS_REGEX ['close_tags ' ], '{% endcomponent %} ' , $ value );
58
+ }
59
+
60
+ private function lexSelfCloseTag (string $ value )
61
+ {
62
+ return preg_replace_callback (
63
+ self ::COMPONENTS_REGEX ['self_close_tags ' ],
64
+ function (array $ matches ) {
65
+ $ name = lcfirst ($ matches [1 ]);
66
+ $ attributes = $ this ->getAttributesFromAttributeString ($ matches ['attributes ' ]);
67
+
68
+ return "{{ component(' " . $ name . "', " . $ attributes . ") }} " ;
69
+ },
70
+ $ value
71
+ );
72
+ }
73
+
74
+ protected function getAttributesFromAttributeString (string $ attributeString )
75
+ {
76
+ $ attributeString = $ this ->parseAttributeBag ($ attributeString );
77
+
78
+ $ pattern = '/
79
+ (?<attribute>[\w\-:.@]+)
80
+ (
81
+ =
82
+ (?<value>
83
+ (
84
+ \"[^\"]+\"
85
+ |
86
+ \\\'[^ \\\']+ \\\'
87
+ |
88
+ [^\s>]+
89
+ )
90
+ )
91
+ )?
92
+ /x ' ;
93
+
94
+ if (! preg_match_all ($ pattern , $ attributeString , $ matches , PREG_SET_ORDER )) {
95
+ return '{} ' ;
96
+ }
97
+
98
+
99
+ $ attributes = [];
100
+
101
+ foreach ($ matches as $ match ) {
102
+ $ attribute = $ match ['attribute ' ];
103
+ $ value = $ match ['value ' ] ?? null ;
104
+
105
+ if (is_null ($ value )) {
106
+ $ value = 'true ' ;
107
+ }
108
+
109
+
110
+ if (strpos ($ attribute , ": " ) === 0 ) {
111
+ $ attribute = str_replace (": " , "" , $ attribute );
112
+ $ value = $ this ->stripQuotes ($ value );
113
+ }
114
+
115
+ $ valueWithoutQuotes = $ this ->stripQuotes ($ value );
116
+
117
+ if ((strpos ($ valueWithoutQuotes , '{{ ' ) === 0 ) && (strpos ($ valueWithoutQuotes , '}} ' ) === strlen ($ valueWithoutQuotes ) - 2 )) {
118
+ $ value = substr ($ valueWithoutQuotes , 2 , -2 );
119
+ } else {
120
+ $ value = $ value ;
121
+ }
122
+
123
+ $ attributes [$ attribute ] = $ value ;
124
+ }
125
+
126
+ $ out = "{ " ;
127
+ foreach ($ attributes as $ key => $ value ) {
128
+ $ key = "' $ key' " ;
129
+ $ out .= "$ key: $ value, " ;
130
+ };
131
+
132
+ return rtrim ($ out , ', ' ) . "} " ;
133
+ }
134
+
135
+ public function stripQuotes (string $ value )
136
+ {
137
+ return strpos ($ value , '" ' ) === 0 || strpos ($ value , '\'' ) === 0
138
+ ? substr ($ value , 1 , -1 )
139
+ : $ value ;
140
+ }
141
+
142
+ protected function parseAttributeBag (string $ attributeString )
143
+ {
144
+ $ pattern = "/
145
+ (?:^|\s+) # start of the string or whitespace between attributes
146
+ \{\{\s*(attributes(?:.+?(?<!\s))?)\s*\}\} # exact match of attributes variable being echoed
147
+ /x " ;
148
+
149
+ return preg_replace ($ pattern , ' :attributes="$1" ' , $ attributeString );
150
+ }
151
+ }
0 commit comments