1 <?php
2 3 4 5 6 7 8 9 10 11
12
13 require "lessc.inc.php";
14
15
16
17
18
19
20
21
22
23 class easyparse {
24 var $buffer;
25 var $count;
26
27 function __construct($str) {
28 $this->count = 0;
29 $this->buffer = trim($str);
30 }
31
32 function seek($where = null) {
33 if ($where === null) return $this->count;
34 else $this->count = $where;
35 return true;
36 }
37
38 function preg_quote($what) {
39 return preg_quote($what, '/');
40 }
41
42 function match($regex, &$out, $eatWhitespace = true) {
43 $r = '/'.$regex.($eatWhitespace ? '\s*' : '').'/Ais';
44 if (preg_match($r, $this->buffer, $out, null, $this->count)) {
45 $this->count += strlen($out[0]);
46 return true;
47 }
48 return false;
49 }
50
51 function literal($what, $eatWhitespace = true) {
52
53 if ($this->count >= strlen($this->buffer)) return false;
54
55
56 if (!$eatWhitespace and strlen($what) == 1) {
57 if ($this->buffer{$this->count} == $what) {
58 $this->count++;
59 return true;
60 }
61 else return false;
62 }
63
64 return $this->match($this->preg_quote($what), $m, $eatWhitespace);
65 }
66
67 }
68
69 class tagparse extends easyparse {
70 static private $combinators = null;
71 static private $match_opts = null;
72
73 function parse() {
74 if (empty(self::$combinators)) {
75 self::$combinators = '('.implode('|', array_map(array($this, 'preg_quote'),
76 array('+', '>', '~'))).')';
77 self::$match_opts = '('.implode('|', array_map(array($this, 'preg_quote'),
78 array('=', '~=', '|=', '$=', '*='))).')';
79 }
80
81
82 $this->buffer = preg_replace('/\s+/', ' ', $this->buffer).' ';
83
84 $tags = array();
85 while ($this->tag($t)) $tags[] = $t;
86
87 return $tags;
88 }
89
90 static function compileString($string) {
91 list(, $delim, $str) = $string;
92 $str = str_replace($delim, "\\".$delim, $str);
93 $str = str_replace("\n", "\\\n", $str);
94 return $delim.$str.$delim;
95 }
96
97 static function compilePaths($paths) {
98 return implode(', ', array_map(array('self', 'compilePath'), $paths));
99 }
100
101
102 static function compilePath($path) {
103 return implode(' ', array_map(array('self', 'compileTag'), $path));
104 }
105
106
107 static function compileTag($tag) {
108 ob_start();
109 if (isset($tag['comb'])) echo $tag['comb']." ";
110 if (isset($tag['front'])) echo $tag['front'];
111 if (isset($tag['attr'])) {
112 echo '['.$tag['attr'];
113 if (isset($tag['op'])) {
114 echo $tag['op'].$tag['op_value'];
115 }
116 echo ']';
117 }
118 return ob_get_clean();
119 }
120
121 function string(&$out) {
122 $s = $this->seek();
123
124 if ($this->literal('"')) {
125 $delim = '"';
126 } elseif ($this->literal("'")) {
127 $delim = "'";
128 } else {
129 return false;
130 }
131
132 while (true) {
133
134 $buff = "";
135 $escapeNext = false;
136 $finished = false;
137 for ($i = $this->count; $i < strlen($this->buffer); $i++) {
138 $char = $this->buffer[$i];
139 switch ($char) {
140 case $delim:
141 if ($escapeNext) {
142 $buff .= $char;
143 $escapeNext = false;
144 break;
145 }
146 $finished = true;
147 break 2;
148 case "\\":
149 if ($escapeNext) {
150 $buff .= $char;
151 $escapeNext = false;
152 } else {
153 $escapeNext = true;
154 }
155 break;
156 case "\n":
157 if (!$escapeNext) {
158 break 3;
159 }
160
161 $buff .= $char;
162 $escapeNext = false;
163 break;
164 default:
165 if ($escapeNext) {
166 $buff .= "\\";
167 $escapeNext = false;
168 }
169 $buff .= $char;
170 }
171 }
172 if (!$finished) break;
173 $out = array('string', $delim, $buff);
174 $this->seek($i+1);
175 return true;
176 }
177
178 $this->seek($s);
179 return false;
180 }
181
182 function tag(&$out) {
183 $s = $this->seek();
184 $tag = array();
185 if ($this->combinator($op)) $tag['comb'] = $op;
186
187 if (!$this->match('(.*?)( |$|\[|'.self::$combinators.')', $match)) {
188 $this->seek($s);
189 return false;
190 }
191
192 if (!empty($match[3])) {
193
194 $this->count-=strlen($match[3]);
195 }
196
197 if (!empty($match[1])) $tag['front'] = $match[1];
198
199 if ($match[2] == '[') {
200 if ($this->ident($i)) {
201 $tag['attr'] = $i;
202
203 if ($this->match(self::$match_opts, $m) && $this->value($v)) {
204 $tag['op'] = $m[1];
205 $tag['op_value'] = $v;
206 }
207
208 if ($this->literal(']')) {
209 $out = $tag;
210 return true;
211 }
212 }
213 } elseif (isset($tag['front'])) {
214 $out = $tag;
215 return true;
216 }
217
218 $this->seek($s);
219 return false;
220 }
221
222 function ident(&$out) {
223
224
225
226 if ($this->match('(-?[_a-z][_\w]*)', $m)) {
227 $out = $m[1];
228 return true;
229 }
230 return false;
231 }
232
233 function value(&$out) {
234 if ($this->string($str)) {
235 $out = $this->compileString($str);
236 return true;
237 } elseif ($this->ident($id)) {
238 $out = $id;
239 return true;
240 }
241 return false;
242 }
243
244
245 function combinator(&$op) {
246 if ($this->match(self::$combinators, $m)) {
247 $op = $m[1];
248 return true;
249 }
250 return false;
251 }
252 }
253
254 class nodecounter {
255 var $count = 0;
256 var $children = array();
257
258 var $name;
259 var $child_blocks;
260 var $the_block;
261
262 function __construct($name) {
263 $this->name = $name;
264 }
265
266 function dump($stack = null) {
267 if (is_null($stack)) $stack = array();
268 $stack[] = $this->getName();
269 echo implode(' -> ', $stack)." ($this->count)\n";
270 foreach ($this->children as $child) {
271 $child->dump($stack);
272 }
273 }
274
275 static function compileProperties($c, $block) {
276 foreach($block as $name => $value) {
277 if ($c->isProperty($name, $value)) {
278 echo $c->compileProperty($name, $value)."\n";
279 }
280 }
281 }
282
283 function compile($c, $path = null) {
284 if (is_null($path)) $path = array();
285 $path[] = $this->name;
286
287 $isVisible = !is_null($this->the_block) || !is_null($this->child_blocks);
288
289 if ($isVisible) {
290 echo $c->indent(implode(' ', $path).' {');
291 $c->indentLevel++;
292 $path = array();
293
294 if ($this->the_block) {
295 $this->compileProperties($c, $this->the_block);
296 }
297
298 if ($this->child_blocks) {
299 foreach ($this->child_blocks as $block) {
300 echo $c->indent(tagparse::compilePaths($block['__tags']).' {');
301 $c->indentLevel++;
302 $this->compileProperties($c, $block);
303 $c->indentLevel--;
304 echo $c->indent('}');
305 }
306 }
307 }
308
309
310 foreach($this->children as $node) {
311 $node->compile($c, $path);
312 }
313
314 if ($isVisible) {
315 $c->indentLevel--;
316 echo $c->indent('}');
317 }
318
319 }
320
321 function getName() {
322 if (is_null($this->name)) return "[root]";
323 else return $this->name;
324 }
325
326 function getNode($name) {
327 if (!isset($this->children[$name])) {
328 $this->children[$name] = new nodecounter($name);
329 }
330
331 return $this->children[$name];
332 }
333
334 function findNode($path) {
335 $current = $this;
336 for ($i = 0; $i < count($path); $i++) {
337 $t = tagparse::compileTag($path[$i]);
338 $current = $current->getNode($t);
339 }
340
341 return $current;
342 }
343
344 function addBlock($path, $block) {
345 $node = $this->findNode($path);
346 if (!is_null($node->the_block)) throw new exception("can this happen?");
347
348 unset($block['__tags']);
349 $node->the_block = $block;
350 }
351
352 function addToNode($path, $block) {
353 $node = $this->findNode($path);
354 $node->child_blocks[] = $block;
355 }
356 }
357
358 359 360
361 class lessify extends lessc {
362 public function dump() {
363 print_r($this->env);
364 }
365
366 public function parse($str = null) {
367 $this->prepareParser($str ? $str : $this->buffer);
368 while (false !== $this->parseChunk());
369
370 $root = new nodecounter(null);
371
372
373 $order = array();
374
375 $visitedTags = array();
376 foreach (end($this->env) as $name => $block) {
377 if (!$this->isBlock($name, $block)) continue;
378 if (isset($visitedTags[$name])) continue;
379
380 foreach ($block['__tags'] as $t) {
381 $visitedTags[$t] = true;
382 }
383
384
385 if (count($block['__tags']) == 1) {
386 $p = new tagparse(end($block['__tags']));
387 $path = $p->parse();
388 $root->addBlock($path, $block);
389 $order[] = array('compressed', $path, $block);
390 continue;
391 } else {
392 $common = null;
393 $paths = array();
394 foreach ($block['__tags'] as $rawtag) {
395 $p = new tagparse($rawtag);
396 $paths[] = $path = $p->parse();
397 if (is_null($common)) $common = $path;
398 else {
399 $new_common = array();
400 foreach ($path as $tag) {
401 $head = array_shift($common);
402 if ($tag == $head) {
403 $new_common[] = $head;
404 } else break;
405 }
406 $common = $new_common;
407 if (empty($common)) {
408
409 break;
410 }
411 }
412 }
413
414 if (!empty($common)) {
415 $new_paths = array();
416 foreach ($paths as $p) $new_paths[] = array_slice($p, count($common));
417 $block['__tags'] = $new_paths;
418 $root->addToNode($common, $block);
419 $order[] = array('compressed', $common, $block);
420 continue;
421 }
422
423 }
424
425 $order[] = array('none', $block['__tags'], $block);
426 }
427
428
429 $compressed = $root->children;
430 foreach ($order as $item) {
431 list($type, $tags, $block) = $item;
432 if ($type == 'compressed') {
433 $top = tagparse::compileTag(reset($tags));
434 if (isset($compressed[$top])) {
435 $compressed[$top]->compile($this);
436 unset($compressed[$top]);
437 }
438 } else {
439 echo $this->indent(implode(', ', $tags).' {');
440 $this->indentLevel++;
441 nodecounter::compileProperties($this, $block);
442 $this->indentLevel--;
443 echo $this->indent('}');
444 }
445 }
446 }
447 }
448