Details | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
37 | PointedEar | 1 | <?php |
2 | |||
3 | namespace de\pointedears\css\least; |
||
4 | |||
5 | require_once __DIR__ . '/../../string/parser/Lexer.php'; |
||
6 | require_once __DIR__ . '/../../string/parser/Parser.php'; |
||
7 | |||
8 | /** |
||
9 | * Parses a LEAST stylesheet into a CSS stylesheet |
||
10 | * |
||
11 | * @author Thomas 'PointedEars' Lahn |
||
12 | * @property-read string $compiled |
||
13 | */ |
||
14 | class Parser extends \de\pointedears\string\parser\Parser |
||
15 | { |
||
16 | /** |
||
17 | * CSS stylesheet code compiled from the LEAST stylesheet |
||
18 | * |
||
19 | * @var string |
||
20 | */ |
||
21 | protected $_compiled; |
||
22 | |||
23 | /** |
||
24 | * Array of arrays of template variables, one inner array for |
||
25 | * each scope level. |
||
26 | * @var array[array] |
||
27 | */ |
||
28 | protected $_vars = array(array()); |
||
29 | |||
30 | /** |
||
31 | * The nesting level of the current variable scope. |
||
32 | * |
||
33 | * Used to hold variable definitions per nesting level. |
||
34 | * |
||
35 | * @var int |
||
36 | */ |
||
37 | protected $_scope_level = 0; |
||
38 | |||
39 | /** |
||
40 | * Cache for lookup results. |
||
41 | * |
||
42 | * The lookup cache improves parser performance with |
||
43 | * nested scopes. By contrast to <code>$_vars</code>, it is |
||
44 | * a _shallow_ storage for template variable definitions and |
||
45 | * represents the current _certain_ variable knowledge of the |
||
46 | * parser, i.e. variables that have been defined in outer scopes |
||
47 | * and the same scope, and whose values have been looked up in |
||
48 | * <code>$_vars</code> before. |
||
49 | * |
||
50 | * The lookup cache is replaced by the variables of the outer |
||
51 | * scope when a scope is exited because definitions in the |
||
52 | * outer scope may have been shadowed by the inner scope. |
||
53 | * Note that definitions from the second-next, third-next |
||
54 | * aso. scopes need to be looked up again then. |
||
55 | * |
||
56 | * @var array |
||
57 | */ |
||
58 | protected $_lookup_cache = array(); |
||
59 | |||
60 | /** |
||
61 | * Mixins |
||
62 | * @var array |
||
63 | */ |
||
64 | protected $_mixins = array(); |
||
65 | |||
66 | /** |
||
67 | * <code>true</code> if the parser is parsing a mix-in |
||
68 | * @var boolean |
||
69 | */ |
||
70 | protected $_in_mixin = false; |
||
71 | |||
72 | /** |
||
73 | * The currently parsed mix-in |
||
74 | * @var Mixin |
||
75 | */ |
||
76 | protected $_current_mixin; |
||
77 | |||
78 | /** |
||
79 | * The nesting level in the currently parsed mix-in, 0 again when |
||
80 | * it just ended |
||
81 | * @var int |
||
82 | */ |
||
83 | protected $_mixin_level = 0; |
||
84 | |||
85 | public function __construct ($code) |
||
86 | { |
||
87 | $lexer = new \de\pointedears\string\parser\Lexer($code); |
||
88 | |||
89 | $nl = 'n'; |
||
90 | $escape = '\\[0-9a-f]{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-f]'; |
||
91 | $lexer->addTokens(array( |
||
92 | '(?P<PHP><\?php.*?\?>)', |
||
93 | "/\*[^*]*\*+([^/*][^*]*\*+)*/", |
||
94 | "\"([^\n\r\f\\\"]|\\{$nl}|{$escape})*\"", |
||
95 | "'([^\n\r\f\\']|\\{$nl}|{$escape})*'", |
||
96 | // '\.(?P<mixin_def>[\w-]+)\s*\((?P<params>.*?)\)\s*(?!;)', |
||
97 | // '\.(?P<mixin_call>[\w-]+)\s*\((?P<args>.*)?\)\s*;\\s*', |
||
98 | '[{}]', |
||
99 | // '\$(?P<name>\w+)\s*=\s*(?P<value>\'.+?\'|".+"|.+?)\s*;\s*', |
||
100 | // '\$(?P<ref>\w+)' |
||
101 | )); |
||
102 | |||
103 | $lexer->ignoreCase = true; |
||
104 | $lexer->dotAll = true; |
||
105 | |||
106 | parent::__construct($lexer); |
||
107 | $this->_compiled = $code; |
||
108 | } |
||
109 | |||
110 | protected function defineVar($name, $value) |
||
111 | { |
||
112 | $scope_level = $this->_scope_level; |
||
113 | |||
114 | if (defined('DEBUG') && DEBUG > 0) |
||
115 | { |
||
116 | echo "<?php // Least.php: Found definition \${$name} = `{$value}'" |
||
117 | . " at scope level {$scope_level} ?>\n"; |
||
118 | } |
||
119 | |||
120 | $this->_vars[$scope_level][$name] = $value; |
||
121 | $this->_lookup_cache[$name] = $value; |
||
122 | } |
||
123 | |||
124 | /** |
||
125 | * Resolves a template variable reference against the template scope chain |
||
126 | * @param string $ref |
||
127 | * @return mixed |
||
128 | */ |
||
129 | protected function resolveVar ($name) |
||
130 | { |
||
131 | $scope_level = $this->_scope_level + 1; |
||
132 | $orig_level = $scope_level - 1; |
||
133 | |||
134 | if (defined('DEBUG') && DEBUG > 0) |
||
135 | { |
||
136 | echo "<?php // Least.php: Found reference \${$name} at scope level " |
||
137 | . ($scope_level - 1) . " ?>\n"; |
||
138 | } |
||
139 | |||
140 | /* Try the lookup cache for this scope level first */ |
||
141 | if (array_key_exists($name, $this->_lookup_cache)) |
||
142 | { |
||
143 | $value = $this->_lookup_cache[$name]; |
||
144 | |||
145 | if (defined('DEBUG') && DEBUG > 1) |
||
146 | { |
||
147 | echo "<?php // Least.php: Resolved reference \${$name}" |
||
148 | . " at scope level {$orig_level}" |
||
149 | . " from lookup cache, value `{$value}' ?>\n"; |
||
150 | } |
||
151 | |||
152 | return $value; |
||
153 | } |
||
154 | |||
155 | while ($scope_level--) |
||
156 | { |
||
157 | if (array_key_exists($name, $this->_vars[$scope_level])) |
||
158 | { |
||
159 | $value = $this->_vars[$scope_level][$name]; |
||
160 | $this->_lookup_cache[$name] = $value; |
||
161 | |||
162 | if (defined('DEBUG') && DEBUG > 1) |
||
163 | { |
||
164 | echo "<?php // Least.php: Resolved reference \${$name}" |
||
165 | . " from scope level {$orig_level}" |
||
166 | . " at scope level {$scope_level}, value `{$value}' ?>\n"; |
||
167 | } |
||
168 | |||
169 | return $value; |
||
170 | } |
||
171 | } |
||
172 | |||
173 | if (defined('DEBUG') && DEBUG > 0) |
||
174 | { |
||
175 | echo "<?php // Least.php: WARNING: Unresolved reference \${$name}" |
||
176 | . " at scope level {$orig_level}. Variable not in scope? ?>\n"; |
||
177 | } |
||
178 | } |
||
179 | |||
180 | protected function defineMixin($name, $params) |
||
181 | { |
||
182 | $scope_level = $this->_scope_level; |
||
183 | |||
184 | if (defined('DEBUG') && DEBUG > 0) |
||
185 | { |
||
186 | echo "<?php // Least.php: Found mixin definition {$name}($params)" |
||
187 | . " at scope level {$scope_level}. ?>\n"; |
||
188 | } |
||
189 | |||
190 | $mixin = new Mixin($params); |
||
191 | $this->_mixins[$name] = $mixin; |
||
192 | |||
193 | return $mixin; |
||
194 | } |
||
195 | |||
196 | protected function callMixin($name, $args) |
||
197 | { |
||
198 | return $this->$_mixins[$name]->apply($args); |
||
199 | } |
||
200 | |||
201 | public function parseToken ($matches) |
||
202 | { |
||
203 | ini_set('html_errors', 0); |
||
204 | // var_dump($matches); |
||
205 | // /* Get match offset and match length */ |
||
206 | // $match = $matches[0]; |
||
207 | // $match_start = $match[1]; |
||
208 | // $match_length = mb_strlen($match[0]); |
||
209 | |||
210 | // /* Transform $matches to the format it is usually set as (without PREG_OFFSET_CAPTURE set) */ |
||
211 | // $matches = array_map(function ($m) { |
||
212 | // return $m[0]; |
||
213 | // }, $matches); |
||
214 | // $match = $matches[0]; |
||
215 | |||
216 | // if (defined('DEBUG') && DEBUG > 1) |
||
217 | // { |
||
218 | // echo print_r(array( |
||
219 | // 'match_start' => $match_start, |
||
220 | // 'match_length' => $match_length, |
||
221 | // 'matches' => $matches, |
||
222 | // 'in_mixin' => $in_mixin, |
||
223 | // 'scope_level' => $this->_scope_level, |
||
224 | // ), true) . "\n"; |
||
225 | // } |
||
226 | |||
227 | // if (isset($matches['mixin_def']) && $matches['mixin_def']) |
||
228 | // { |
||
229 | // $this->_in_mixin = true; |
||
230 | // $this->_current_mixin = $this->defineMixin($matches['mixin_def'], $matches['params']); |
||
231 | // // $code = self::replace(mb_strlen($match), '', $code, $match_start); |
||
232 | // // $match_length = 0; |
||
233 | // } |
||
234 | // else if (isset($matches['mixin_call']) && $matches['mixin_call']) |
||
235 | // { |
||
236 | // //return |
||
237 | // $this->callMixin($matches['mixin_call'], $matches['args']); |
||
238 | // //$code = self::replace(mb_strlen($match), '', $code, $match_start); |
||
239 | // //$match_length = 0; |
||
240 | // $mixin_start = $match_start; |
||
241 | // } |
||
242 | // else if ($match === '{') |
||
243 | // { |
||
244 | // ++$this->_scope_level; |
||
245 | // $this->_vars[$this->_scope_level] = array(); |
||
246 | |||
247 | // if ($this->_in_mixin) |
||
248 | // { |
||
249 | // if ($mixin_body_start === 0) |
||
250 | // { |
||
251 | // $mixin_body_start = $match_start; |
||
252 | // } |
||
253 | |||
254 | // ++$this->_mixin_level; |
||
255 | // } |
||
256 | // } |
||
257 | // else if ($match === '}') |
||
258 | // { |
||
259 | // --$this->_scope_level; |
||
260 | // $this->_lookup_cache = $this->_vars[$this->_scope_level]; |
||
261 | |||
262 | // if ($this->_in_mixin) |
||
263 | // { |
||
264 | // --$this->_mixin_level; |
||
265 | // if ($this->_mixin_level === 0) |
||
266 | // { |
||
267 | // $this->_in_mixin = false; |
||
268 | // $this->_current_mixin->setBody(substr($code, $mixin_start + 1, $match_start - $mixin_start - 1)); |
||
269 | // echo '"'.print_r(substr($code, $mixin_start, $match_start - $mixin_start + 1), true) . "\"\n"; |
||
270 | // // $code = self::replace($match_start - $mixin_start + 1, '', $code, $mixin_start); |
||
271 | // // $match_length = 0; |
||
272 | // } |
||
273 | // } |
||
274 | // } |
||
275 | // else if (isset($matches['name']) && $matches['name']) |
||
276 | // { |
||
277 | // $this->defineVar($matches['name'], $matches['value']); |
||
278 | // $code = self::replace(mb_strlen($match), '', $code, $match_start); |
||
279 | // $match_length = 0; |
||
280 | // } |
||
281 | // else if (isset($matches['ref']) && $matches['ref']) |
||
282 | // { |
||
283 | // $name = $matches['ref']; |
||
284 | |||
285 | // if (!$in_mixin || !in_array($name, $mixin->params, true)) |
||
286 | // { |
||
287 | // $value = $this->resolveVar($matches['ref']); |
||
288 | // $this->_lexer->text = self::replace(mb_strlen($match), $value, $code, $match_start); |
||
289 | // $this->_lexer->offset -= $match_length + mb_strlen($value); |
||
290 | // } |
||
291 | // } |
||
292 | } |
||
293 | |||
294 | public function getCompiled () |
||
295 | { |
||
296 | return $this->_compiled; |
||
297 | } |
||
298 | } |
||
299 | |||
300 | class Mixin extends \de\pointedears\Base |
||
301 | { |
||
302 | /** |
||
303 | * Parameters of the mixin |
||
304 | * @var array |
||
305 | */ |
||
306 | protected $_params; |
||
307 | |||
308 | /** |
||
309 | * Body of the mixin |
||
310 | * @var string |
||
311 | */ |
||
312 | protected $_body = ''; |
||
313 | |||
314 | /** |
||
315 | * @param string $params |
||
316 | */ |
||
317 | public function __construct ($params, $body = '') |
||
318 | { |
||
319 | $this->_params = is_array($params) |
||
320 | ? $params |
||
321 | : array_map( |
||
322 | function ($e) { |
||
323 | return preg_replace('/^\$/', '', $e); |
||
324 | }, |
||
325 | preg_split('/\s*,\s*/', $params)); |
||
326 | |||
327 | if ($body !== '') |
||
328 | { |
||
329 | $this->setBody($body); |
||
330 | } |
||
331 | } |
||
332 | |||
333 | public function setBody ($body) |
||
334 | { |
||
335 | $this->body = (string) $body; |
||
336 | } |
||
337 | |||
338 | /** |
||
339 | * @param string $arguments |
||
340 | */ |
||
341 | public function apply ($arguments) |
||
342 | { |
||
343 | return ''; |
||
344 | } |
||
345 | } |