1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Jade\Lexer; |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* Class Jade\Lexer\Scanner. |
7
|
|
|
*/ |
8
|
|
|
abstract class Scanner extends MixinScanner |
9
|
|
|
{ |
10
|
|
|
/** |
11
|
|
|
* Helper to create tokens. |
12
|
|
|
*/ |
13
|
|
|
protected function scan($regex, $type, $captureIndex = 1) |
14
|
|
|
{ |
15
|
|
|
if (preg_match($regex, $this->input, $matches)) { |
16
|
|
|
$this->consume($matches[0]); |
17
|
|
|
|
18
|
|
|
return $this->token($type, isset($matches[$captureIndex]) && strlen($matches[$captureIndex]) > 0 ? $matches[$captureIndex] : ''); |
19
|
|
|
} |
20
|
|
|
} |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Scan comment from input & return it if found. |
24
|
|
|
* |
25
|
|
|
* @return object|null |
26
|
|
|
*/ |
27
|
|
|
protected function scanComment() |
28
|
|
|
{ |
29
|
|
|
$indent = count($this->indentStack) ? $this->indentStack[0] : 0; |
30
|
|
|
if (preg_match('/^ *\/\/(-)?([^\n]*(\n+[ \t]{' . ($indent + 1) . ',}[^\n]*)*)/', $this->input, $matches)) { |
31
|
|
|
$this->consume($matches[0]); |
32
|
|
|
$value = isset($matches[2]) ? $matches[2] : ''; |
33
|
|
|
if (isset($matches[3])) { |
34
|
|
|
$value .= "\n"; |
35
|
|
|
} |
36
|
|
|
$token = $this->token('comment', $value); |
37
|
|
|
$token->buffer = '-' !== $matches[1]; |
38
|
|
|
|
39
|
|
|
return $token; |
40
|
|
|
} |
41
|
|
|
} |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @return object |
45
|
|
|
*/ |
46
|
|
|
protected function scanInterpolation() |
47
|
|
|
{ |
48
|
|
|
return $this->scan('/^#{(.*?)}/', 'interpolation'); |
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @return object |
53
|
|
|
*/ |
54
|
|
|
protected function scanTag() |
55
|
|
|
{ |
56
|
|
|
if (preg_match('/^(\w[-:\w]*)(\/?)/', $this->input, $matches)) { |
57
|
|
|
$this->consume($matches[0]); |
58
|
|
|
$name = $matches[1]; |
59
|
|
|
|
60
|
|
|
if (':' === substr($name, -1) && ':' !== substr($name, -2, 1)) { |
61
|
|
|
$name = substr($name, 0, -1); |
62
|
|
|
$this->defer($this->token(':')); |
63
|
|
|
|
64
|
|
|
while (' ' === substr($this->input, 0, 1)) { |
65
|
|
|
$this->consume(' '); |
66
|
|
|
} |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
$token = $this->token('tag', $name); |
70
|
|
|
$token->selfClosing = ($matches[2] === '/'); |
71
|
|
|
|
72
|
|
|
return $token; |
73
|
|
|
} |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* @return object |
78
|
|
|
*/ |
79
|
|
|
protected function scanFilter() |
80
|
|
|
{ |
81
|
|
|
return $this->scan('/^(?<!:):(?!:)(\w+(?:-\w+)*)/', 'filter'); |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* @return object |
86
|
|
|
*/ |
87
|
|
|
protected function scanDoctype() |
88
|
|
|
{ |
89
|
|
|
return $this->scan('/^(?:!!!|doctype) *([^\n]+)?/', 'doctype'); |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* @return object |
94
|
|
|
*/ |
95
|
|
|
protected function scanId() |
96
|
|
|
{ |
97
|
|
|
return $this->scan('/^#([\w-]+)/', 'id'); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* @return object |
102
|
|
|
*/ |
103
|
|
|
protected function scanClassName() |
104
|
|
|
{ |
105
|
|
|
// http://www.w3.org/TR/CSS21/grammar.html#scanner |
106
|
|
|
// |
107
|
|
|
// ident: |
108
|
|
|
// -?{nmstart}{nmchar}* |
|
|
|
|
109
|
|
|
// nmstart: |
110
|
|
|
// [_a-z]|{nonascii}|{escape} |
|
|
|
|
111
|
|
|
// nonascii: |
112
|
|
|
// [\240-\377] |
113
|
|
|
// escape: |
114
|
|
|
// {unicode}|\\[^\r\n\f0-9a-f] |
|
|
|
|
115
|
|
|
// unicode: |
116
|
|
|
// \\{h}{1,6}(\r\n|[ \t\r\n\f])? |
|
|
|
|
117
|
|
|
// nmchar: |
118
|
|
|
// [_a-z0-9-]|{nonascii}|{escape} |
|
|
|
|
119
|
|
|
// |
120
|
|
|
// /^(-?(?!=[0-9-])(?:[_a-z0-9-]|[\240-\377]|\\{h}{1,6}(?:\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-f])+)/ |
|
|
|
|
121
|
|
|
return $this->scan('/^\.([\w-]+)/', 'class'); |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* @return object |
126
|
|
|
*/ |
127
|
|
|
protected function scanText() |
128
|
|
|
{ |
129
|
|
|
return $this->scan('/^(?:\| ?| ?)?([^\n]+)/', 'text'); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* @return object |
134
|
|
|
*/ |
135
|
|
|
protected function scanAssignment() |
136
|
|
|
{ |
137
|
|
|
if (preg_match('/^(\$?\w+) += *([^;\n]+|\'[^\']+\'|"[^"]+")( *;? *)/', $this->input, $matches)) { |
138
|
|
|
$this->consume($matches[0]); |
139
|
|
|
|
140
|
|
|
return $this->token('code', (substr($matches[1], 0, 1) === '$' ? '' : '$') . $matches[1] . '=' . $matches[2]); |
141
|
|
|
} |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* @return object |
146
|
|
|
*/ |
147
|
|
|
protected function scanConditional() |
148
|
|
|
{ |
149
|
|
|
if (preg_match('/^(if|unless|else if|elseif|else|while)\b([^\n]*)/', $this->input, $matches)) { |
150
|
|
|
$this->consume($matches[0]); |
151
|
|
|
|
152
|
|
|
/*switch ($matches[1]) { |
|
|
|
|
153
|
|
|
case 'if': $code = 'if (' . $matches[2] . '):'; break; |
154
|
|
|
case 'unless': $code = 'if (!(' . $matches[2] . ')):'; break; |
155
|
|
|
case 'else if': $code = 'elseif (' . $matches[2] . '):'; break; |
156
|
|
|
case 'else': $code = 'else (' . $matches[2] . '):'; break; |
157
|
|
|
}*/ |
158
|
|
|
$code = $this->normalizeCode($matches[0]); |
159
|
|
|
$token = $this->token('code', $code); |
160
|
|
|
$token->buffer = false; |
161
|
|
|
|
162
|
|
|
return $token; |
163
|
|
|
} |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* @return object |
168
|
|
|
*/ |
169
|
|
View Code Duplication |
protected function scanEach() |
|
|
|
|
170
|
|
|
{ |
171
|
|
|
if (preg_match('/^(?:- *)?(?:each|for) +(\w+)(?: *, *(\w+))? +in *([^\n]+)/', $this->input, $matches)) { |
172
|
|
|
$this->consume($matches[0]); |
173
|
|
|
|
174
|
|
|
$token = $this->token('each', $matches[1]); |
175
|
|
|
$token->key = $matches[2]; |
176
|
|
|
$token->code = $this->normalizeCode($matches[3]); |
177
|
|
|
|
178
|
|
|
return $token; |
179
|
|
|
} |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* @return object |
184
|
|
|
*/ |
185
|
|
|
protected function scanCustomKeyword() |
186
|
|
|
{ |
187
|
|
|
if ( |
188
|
|
|
count($this->customKeywords) && |
|
|
|
|
189
|
|
|
preg_match('/^([\w-]+)([^\n]*)/', $this->input, $matches) && |
190
|
|
|
isset($this->customKeywords[$matches[1]]) && |
191
|
|
|
is_callable($this->customKeywords[$matches[1]]) |
192
|
|
|
) { |
193
|
|
|
$this->consume($matches[0]); |
194
|
|
|
|
195
|
|
|
$token = $this->token('customKeyword', $matches[1]); |
196
|
|
|
$token->args = trim($matches[2]); |
197
|
|
|
|
198
|
|
|
return $token; |
199
|
|
|
} |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* @return object |
204
|
|
|
*/ |
205
|
|
|
protected function scanCode() |
206
|
|
|
{ |
207
|
|
|
if (preg_match('/^(!?=|-)([^\n]+)/', $this->input, $matches)) { |
208
|
|
|
$this->consume($matches[0]); |
209
|
|
|
$flags = $matches[1]; |
210
|
|
|
$code = $this->normalizeCode($matches[2]); |
211
|
|
|
|
212
|
|
|
$token = $this->token('code', $code); |
213
|
|
|
$token->escape = $flags[0] === '='; |
214
|
|
|
$token->buffer = '=' === $flags[0] || (isset($flags[1]) && '=' === $flags[1]); |
215
|
|
|
|
216
|
|
|
return $token; |
217
|
|
|
} |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* @throws \ErrorException |
222
|
|
|
* |
223
|
|
|
* @return object |
224
|
|
|
*/ |
225
|
|
|
protected function scanAttributes() |
226
|
|
|
{ |
227
|
|
|
if (substr($this->input, 0, 1) === '(') { |
228
|
|
|
// cant use ^ anchor in the regex because the pattern is recursive |
229
|
|
|
// but this restriction is asserted by the if above |
230
|
|
|
//$this->input = preg_replace('/([a-zA-Z0-9\'"\\]\\}\\)])([\t ]+[a-zA-Z])/', '$1,$2', $this->input); |
231
|
|
|
if (!preg_match('/\((?:"(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\'|[^()\'"]++|(?R))*+\)/', $this->input, $matches)) { |
232
|
|
|
throw new \ErrorException('Unable to find attributes closing parenthesis.', 21); |
233
|
|
|
} |
234
|
|
|
$this->consume($matches[0]); |
235
|
|
|
|
236
|
|
|
//$str = preg_replace('/()([a-zA-Z0-9_\\x7f-\\xff\\)\\]\\}"\'])(\s+[a-zA-Z_])/', '$1,$2', $str); |
237
|
|
|
|
238
|
|
|
$token = $this->token('attributes'); |
239
|
|
|
$token->attributes = array(); |
240
|
|
|
$token->escaped = array(); |
241
|
|
|
$token->selfClosing = false; |
242
|
|
|
|
243
|
|
|
$parser = new Attributes($token); |
244
|
|
|
$parser->parseWith(substr($matches[0], 1, strlen($matches[0]) - 2)); |
245
|
|
|
|
246
|
|
|
if ($this->length() && '/' === $this->input[0]) { |
247
|
|
|
$this->consume(1); |
248
|
|
|
$token->selfClosing = true; |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
return $token; |
252
|
|
|
} |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* @return object |
257
|
|
|
*/ |
258
|
|
|
protected function scanPipelessText() |
259
|
|
|
{ |
260
|
|
|
if ($this->pipeless && "\n" !== substr($this->input, 0, 1)) { |
|
|
|
|
261
|
|
|
$pos = strpos($this->input, "\n"); |
262
|
|
|
|
263
|
|
|
if ($pos === false) { |
264
|
|
|
$pos = $this->length(); |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
$str = substr($this->input, 0, $pos); // do not include the \n char |
268
|
|
|
|
269
|
|
|
$this->consume($str); |
270
|
|
|
|
271
|
|
|
return $this->token('text', ltrim($str)); |
272
|
|
|
} |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* @return object |
277
|
|
|
*/ |
278
|
|
|
protected function scanColon() |
279
|
|
|
{ |
280
|
|
|
return $this->scan('/^:(?!:) */', ':'); |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
/** |
284
|
|
|
* @return object |
285
|
|
|
*/ |
286
|
|
|
protected function scanAndAttributes() |
287
|
|
|
{ |
288
|
|
|
return $this->scan('/^&attributes(\(((?>[^()]+|(?1))*)\))/', '&attributes', 2); |
289
|
|
|
} |
290
|
|
|
} |
291
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.