PageRenderTime 50ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/lessify.inc.php

http://github.com/leafo/lessphp
PHP | 451 lines | 347 code | 72 blank | 32 comment | 66 complexity | 80b7ebc6f28fcc363221152179b52f16 MD5 | raw file
Possible License(s): GPL-3.0
  1. <?php
  2. /**
  3. * lessify
  4. * Convert a css file into a less file
  5. * http://leafo.net/lessphp
  6. * Copyright 2010, leaf corcoran <leafot@gmail.com>
  7. *
  8. * WARNING: THIS DOES NOT WORK ANYMORE. NEEDS TO BE UPDATED FOR
  9. * LATEST VERSION OF LESSPHP.
  10. *
  11. */
  12. require "lessc.inc.php";
  13. //
  14. // check if the merge during mixin is overwriting values. should or should it not?
  15. //
  16. //
  17. // 1. split apart class tags
  18. //
  19. class easyparse {
  20. public $buffer;
  21. public $count;
  22. public function __construct($str) {
  23. $this->count = 0;
  24. $this->buffer = trim($str);
  25. }
  26. public function seek($where = null) {
  27. if ($where === null) {
  28. return $this->count;
  29. }
  30. $this->count = $where;
  31. return true;
  32. }
  33. public function preg_quote($what) {
  34. return preg_quote($what, '/');
  35. }
  36. public function match($regex, &$out, $eatWhitespace = true) {
  37. $r = '/'.$regex.($eatWhitespace ? '\s*' : '').'/Ais';
  38. if (preg_match($r, $this->buffer, $out, null, $this->count)) {
  39. $this->count += strlen($out[0]);
  40. return true;
  41. }
  42. return false;
  43. }
  44. public function literal($what, $eatWhitespace = true) {
  45. // this is here mainly prevent notice from { } string accessor
  46. if ($this->count >= strlen($this->buffer)) return false;
  47. // shortcut on single letter
  48. if (!$eatWhitespace and strlen($what) === 1) {
  49. if ($this->buffer{$this->count} == $what) {
  50. $this->count++;
  51. return true;
  52. }
  53. return false;
  54. }
  55. return $this->match($this->preg_quote($what), $m, $eatWhitespace);
  56. }
  57. }
  58. class tagparse extends easyparse {
  59. static private $combinators = null;
  60. static private $match_opts = null;
  61. public function parse() {
  62. if (empty(self::$combinators)) {
  63. self::$combinators = '(' . implode('|', array_map(array($this, 'preg_quote'),
  64. array('+', '>', '~'))).')';
  65. self::$match_opts = '(' . implode('|', array_map(array($this, 'preg_quote'),
  66. array('=', '~=', '|=', '$=', '*='))) . ')';
  67. }
  68. // crush whitespace
  69. $this->buffer = preg_replace('/\s+/', ' ', $this->buffer) . ' ';
  70. $tags = array();
  71. while ($this->tag($t)) {
  72. $tags[] = $t;
  73. }
  74. return $tags;
  75. }
  76. public static function compileString($string) {
  77. list(, $delim, $str) = $string;
  78. $str = str_replace($delim, "\\" . $delim, $str);
  79. $str = str_replace("\n", "\\\n", $str);
  80. return $delim . $str . $delim;
  81. }
  82. public static function compilePaths($paths) {
  83. return implode(', ', array_map(array('self', 'compilePath'), $paths));
  84. }
  85. // array of tags
  86. public static function compilePath($path) {
  87. return implode(' ', array_map(array('self', 'compileTag'), $path));
  88. }
  89. public static function compileTag($tag) {
  90. ob_start();
  91. if (isset($tag['comb'])) echo $tag['comb'] . " ";
  92. if (isset($tag['front'])) echo $tag['front'];
  93. if (isset($tag['attr'])) {
  94. echo '[' . $tag['attr'];
  95. if (isset($tag['op'])) {
  96. echo $tag['op'] . $tag['op_value'];
  97. }
  98. echo ']';
  99. }
  100. return ob_get_clean();
  101. }
  102. public function string(&$out) {
  103. $s = $this->seek();
  104. if ($this->literal('"')) {
  105. $delim = '"';
  106. } elseif ($this->literal("'")) {
  107. $delim = "'";
  108. } else {
  109. return false;
  110. }
  111. while (true) {
  112. // step through letters looking for either end or escape
  113. $buff = "";
  114. $escapeNext = false;
  115. $finished = false;
  116. for ($i = $this->count; $i < strlen($this->buffer); $i++) {
  117. $char = $this->buffer[$i];
  118. switch ($char) {
  119. case $delim:
  120. if ($escapeNext) {
  121. $buff .= $char;
  122. $escapeNext = false;
  123. break;
  124. }
  125. $finished = true;
  126. break 2;
  127. case "\\":
  128. if ($escapeNext) {
  129. $buff .= $char;
  130. $escapeNext = false;
  131. } else {
  132. $escapeNext = true;
  133. }
  134. break;
  135. case "\n":
  136. if (!$escapeNext) {
  137. break 3;
  138. }
  139. $buff .= $char;
  140. $escapeNext = false;
  141. break;
  142. default:
  143. if ($escapeNext) {
  144. $buff .= "\\";
  145. $escapeNext = false;
  146. }
  147. $buff .= $char;
  148. }
  149. }
  150. if (!$finished) break;
  151. $out = array('string', $delim, $buff);
  152. $this->seek($i+1);
  153. return true;
  154. }
  155. $this->seek($s);
  156. return false;
  157. }
  158. public function tag(&$out) {
  159. $s = $this->seek();
  160. $tag = array();
  161. if ($this->combinator($op)) $tag['comb'] = $op;
  162. if (!$this->match('(.*?)( |$|\[|'.self::$combinators.')', $match)) {
  163. $this->seek($s);
  164. return false;
  165. }
  166. if (!empty($match[3])) {
  167. // give back combinator
  168. $this->count-=strlen($match[3]);
  169. }
  170. if (!empty($match[1])) $tag['front'] = $match[1];
  171. if ($match[2] == '[') {
  172. if ($this->ident($i)) {
  173. $tag['attr'] = $i;
  174. if ($this->match(self::$match_opts, $m) && $this->value($v)) {
  175. $tag['op'] = $m[1];
  176. $tag['op_value'] = $v;
  177. }
  178. if ($this->literal(']')) {
  179. $out = $tag;
  180. return true;
  181. }
  182. }
  183. } elseif (isset($tag['front'])) {
  184. $out = $tag;
  185. return true;
  186. }
  187. $this->seek($s);
  188. return false;
  189. }
  190. public function ident(&$out) {
  191. // [-]?{nmstart}{nmchar}*
  192. // nmstart: [_a-z]|{nonascii}|{escape}
  193. // nmchar: [_a-z0-9-]|{nonascii}|{escape}
  194. if ($this->match('(-?[_a-z][_\w]*)', $m)) {
  195. $out = $m[1];
  196. return true;
  197. }
  198. return false;
  199. }
  200. public function value(&$out) {
  201. if ($this->string($str)) {
  202. $out = $this->compileString($str);
  203. return true;
  204. } elseif ($this->ident($id)) {
  205. $out = $id;
  206. return true;
  207. }
  208. return false;
  209. }
  210. public function combinator(&$op) {
  211. if ($this->match(self::$combinators, $m)) {
  212. $op = $m[1];
  213. return true;
  214. }
  215. return false;
  216. }
  217. }
  218. class nodecounter {
  219. public $count = 0;
  220. public $children = array();
  221. public $name;
  222. public $child_blocks;
  223. public $the_block;
  224. public function __construct($name) {
  225. $this->name = $name;
  226. }
  227. public function dump($stack = null) {
  228. if (is_null($stack)) $stack = array();
  229. $stack[] = $this->getName();
  230. echo implode(' -> ', $stack) . " ($this->count)\n";
  231. foreach ($this->children as $child) {
  232. $child->dump($stack);
  233. }
  234. }
  235. public static function compileProperties($c, $block) {
  236. foreach ($block as $name => $value) {
  237. if ($c->isProperty($name, $value)) {
  238. echo $c->compileProperty($name, $value) . "\n";
  239. }
  240. }
  241. }
  242. public function compile($c, $path = null) {
  243. if (is_null($path)) $path = array();
  244. $path[] = $this->name;
  245. $isVisible = !is_null($this->the_block) || !is_null($this->child_blocks);
  246. if ($isVisible) {
  247. echo $c->indent(implode(' ', $path) . ' {');
  248. $c->indentLevel++;
  249. $path = array();
  250. if ($this->the_block) {
  251. $this->compileProperties($c, $this->the_block);
  252. }
  253. if ($this->child_blocks) {
  254. foreach ($this->child_blocks as $block) {
  255. echo $c->indent(tagparse::compilePaths($block['__tags']).' {');
  256. $c->indentLevel++;
  257. $this->compileProperties($c, $block);
  258. $c->indentLevel--;
  259. echo $c->indent('}');
  260. }
  261. }
  262. }
  263. // compile child nodes
  264. foreach ($this->children as $node) {
  265. $node->compile($c, $path);
  266. }
  267. if ($isVisible) {
  268. $c->indentLevel--;
  269. echo $c->indent('}');
  270. }
  271. }
  272. public function getName() {
  273. if (is_null($this->name)) return "[root]";
  274. else return $this->name;
  275. }
  276. public function getNode($name) {
  277. if (!isset($this->children[$name])) {
  278. $this->children[$name] = new nodecounter($name);
  279. }
  280. return $this->children[$name];
  281. }
  282. public function findNode($path) {
  283. $current = $this;
  284. for ($i = 0; $i < count($path); $i++) {
  285. $t = tagparse::compileTag($path[$i]);
  286. $current = $current->getNode($t);
  287. }
  288. return $current;
  289. }
  290. public function addBlock($path, $block) {
  291. $node = $this->findNode($path);
  292. if (!is_null($node->the_block)) throw new exception("can this happen?");
  293. unset($block['__tags']);
  294. $node->the_block = $block;
  295. }
  296. public function addToNode($path, $block) {
  297. $node = $this->findNode($path);
  298. $node->child_blocks[] = $block;
  299. }
  300. }
  301. /**
  302. * create a less file from a css file by combining blocks where appropriate
  303. */
  304. class lessify extends lessc {
  305. public function dump() {
  306. print_r($this->env);
  307. }
  308. public function parse($str = null) {
  309. $this->prepareParser($str ? $str : $this->buffer);
  310. while (false !== $this->parseChunk());
  311. $root = new nodecounter(null);
  312. // attempt to preserve some of the block order
  313. $order = array();
  314. $visitedTags = array();
  315. foreach (end($this->env) as $name => $block) {
  316. if (!$this->isBlock($name, $block)) continue;
  317. if (isset($visitedTags[$name])) continue;
  318. foreach ($block['__tags'] as $t) {
  319. $visitedTags[$t] = true;
  320. }
  321. // skip those with more than 1
  322. if (count($block['__tags']) == 1) {
  323. $p = new tagparse(end($block['__tags']));
  324. $path = $p->parse();
  325. $root->addBlock($path, $block);
  326. $order[] = array('compressed', $path, $block);
  327. continue;
  328. } else {
  329. $common = null;
  330. $paths = array();
  331. foreach ($block['__tags'] as $rawtag) {
  332. $p = new tagparse($rawtag);
  333. $paths[] = $path = $p->parse();
  334. if (is_null($common)) $common = $path;
  335. else {
  336. $new_common = array();
  337. foreach ($path as $tag) {
  338. $head = array_shift($common);
  339. if ($tag == $head) {
  340. $new_common[] = $head;
  341. } else break;
  342. }
  343. $common = $new_common;
  344. if (empty($common)) {
  345. // nothing in common
  346. break;
  347. }
  348. }
  349. }
  350. if (!empty($common)) {
  351. $new_paths = array();
  352. foreach ($paths as $p) $new_paths[] = array_slice($p, count($common));
  353. $block['__tags'] = $new_paths;
  354. $root->addToNode($common, $block);
  355. $order[] = array('compressed', $common, $block);
  356. continue;
  357. }
  358. }
  359. $order[] = array('none', $block['__tags'], $block);
  360. }
  361. $compressed = $root->children;
  362. foreach ($order as $item) {
  363. list($type, $tags, $block) = $item;
  364. if ($type == 'compressed') {
  365. $top = tagparse::compileTag(reset($tags));
  366. if (isset($compressed[$top])) {
  367. $compressed[$top]->compile($this);
  368. unset($compressed[$top]);
  369. }
  370. } else {
  371. echo $this->indent(implode(', ', $tags).' {');
  372. $this->indentLevel++;
  373. nodecounter::compileProperties($this, $block);
  374. $this->indentLevel--;
  375. echo $this->indent('}');
  376. }
  377. }
  378. }
  379. }