/modules/custom-css/custom-css/preprocessors/scss.inc.php

  1. <?php 
  2. /** 
  3. * SCSS compiler written in PHP 
  4. * 
  5. * @copyright 2012-2013 Leaf Corcoran 
  6. * 
  7. * @license http://opensource.org/licenses/gpl-license GPL-3.0 
  8. * @license http://opensource.org/licenses/MIT MIT 
  9. * 
  10. * @link http://leafo.net/scssphp 
  11. */ 
  12.  
  13. /** 
  14. * The scss compiler and parser. 
  15. * 
  16. * Converting SCSS to CSS is a three stage process. The incoming file is parsed 
  17. * by `scssc_parser` into a syntax tree, then it is compiled into another tree 
  18. * representing the CSS structure by `scssc`. The CSS tree is fed into a 
  19. * formatter, like `scssc_formatter` which then outputs CSS as a string. 
  20. * 
  21. * During the first compile, all values are *reduced*, which means that their 
  22. * types are brought to the lowest form before being dump as strings. This 
  23. * handles math equations, variable dereferences, and the like. 
  24. * 
  25. * The `parse` function of `scssc` is the entry point. 
  26. * 
  27. * In summary: 
  28. * 
  29. * The `scssc` class creates an instance of the parser, feeds it SCSS code,  
  30. * then transforms the resulting tree to a CSS tree. This class also holds the 
  31. * evaluation context, such as all available mixins and variables at any given 
  32. * time. 
  33. * 
  34. * The `scssc_parser` class is only concerned with parsing its input. 
  35. * 
  36. * The `scssc_formatter` takes a CSS tree, and dumps it to a formatted string,  
  37. * handling things like indentation. 
  38. */ 
  39.  
  40. /** 
  41. * SCSS compiler 
  42. * 
  43. * @author Leaf Corcoran <leafot@gmail.com> 
  44. */ 
  45. class scssc { 
  46. static public $VERSION = "v0.0.9"; 
  47.  
  48. static protected $operatorNames = array( 
  49. '+' => "add",  
  50. '-' => "sub",  
  51. '*' => "mul",  
  52. '/' => "div",  
  53. '%' => "mod",  
  54.  
  55. '==' => "eq",  
  56. '!=' => "neq",  
  57. '<' => "lt",  
  58. '>' => "gt",  
  59.  
  60. '<=' => "lte",  
  61. '>=' => "gte",  
  62. ); 
  63.  
  64. static protected $namespaces = array( 
  65. "special" => "%",  
  66. "mixin" => "@",  
  67. "function" => "^",  
  68. ); 
  69.  
  70. static protected $unitTable = array( 
  71. "in" => array( 
  72. "in" => 1,  
  73. "pt" => 72,  
  74. "pc" => 6,  
  75. "cm" => 2.54,  
  76. "mm" => 25.4,  
  77. "px" => 96,  
  78. ); 
  79.  
  80. static public $true = array("keyword", "true"); 
  81. static public $false = array("keyword", "false"); 
  82. static public $null = array("null"); 
  83.  
  84. static public $defaultValue = array("keyword", ""); 
  85. static public $selfSelector = array("self"); 
  86.  
  87. protected $importPaths = array(""); 
  88. protected $importCache = array(); 
  89.  
  90. protected $userFunctions = array(); 
  91.  
  92. protected $numberPrecision = 5; 
  93.  
  94. protected $formatter = "scss_formatter_nested"; 
  95.  
  96. public function compile($code, $name=null) { 
  97. $this->indentLevel = -1; 
  98. $this->commentsSeen = array(); 
  99. $this->extends = array(); 
  100. $this->extendsMap = array(); 
  101.  
  102. $locale = setlocale(LC_NUMERIC, 0); 
  103. setlocale(LC_NUMERIC, "C"); 
  104.  
  105. $this->parsedFiles = array(); 
  106. $this->parser = new scss_parser($name); 
  107. $tree = $this->parser->parse($code); 
  108.  
  109. $this->formatter = new $this->formatter(); 
  110.  
  111. $this->env = null; 
  112. $this->scope = null; 
  113.  
  114. $this->compileRoot($tree); 
  115.  
  116. $out = $this->formatter->format($this->scope); 
  117.  
  118. setlocale(LC_NUMERIC, $locale); 
  119. return $out; 
  120.  
  121. protected function isSelfExtend($target, $origin) { 
  122. foreach ($origin as $sel) { 
  123. if (in_array($target, $sel)) { 
  124. return true; 
  125.  
  126. return false; 
  127.  
  128. protected function pushExtends($target, $origin) { 
  129. if ($this->isSelfExtend($target, $origin)) { 
  130. return; 
  131.  
  132. $i = count($this->extends); 
  133. $this->extends[] = array($target, $origin); 
  134.  
  135. foreach ($target as $part) { 
  136. if (isset($this->extendsMap[$part])) { 
  137. $this->extendsMap[$part][] = $i; 
  138. } else { 
  139. $this->extendsMap[$part] = array($i); 
  140.  
  141. protected function makeOutputBlock($type, $selectors = null) { 
  142. $out = new stdClass; 
  143. $out->type = $type; 
  144. $out->lines = array(); 
  145. $out->children = array(); 
  146. $out->parent = $this->scope; 
  147. $out->selectors = $selectors; 
  148. $out->depth = $this->env->depth; 
  149.  
  150. return $out; 
  151.  
  152. protected function matchExtendsSingle($single, &$outOrigin) { 
  153. $counts = array(); 
  154. foreach ($single as $part) { 
  155. if (!is_string($part)) return false; // hmm 
  156.  
  157. if (isset($this->extendsMap[$part])) { 
  158. foreach ($this->extendsMap[$part] as $idx) { 
  159. $counts[$idx] = 
  160. isset($counts[$idx]) ? $counts[$idx] + 1 : 1; 
  161.  
  162. $outOrigin = array(); 
  163. $found = false; 
  164.  
  165. foreach ($counts as $idx => $count) { 
  166. list($target, $origin) = $this->extends[$idx]; 
  167.  
  168. // check count 
  169. if ($count != count($target)) continue; 
  170.  
  171. // check if target is subset of single 
  172. if (array_diff(array_intersect($single, $target), $target)) continue; 
  173.  
  174. $rem = array_diff($single, $target); 
  175.  
  176. foreach ($origin as $j => $new) { 
  177. // prevent infinite loop when target extends itself 
  178. foreach ($new as $new_selector) { 
  179. if (!array_diff($single, $new_selector)) { 
  180. continue 2; 
  181.  
  182. $origin[$j][count($origin[$j]) - 1] = $this->combineSelectorSingle(end($new), $rem); 
  183.  
  184. $outOrigin = array_merge($outOrigin, $origin); 
  185.  
  186. $found = true; 
  187.  
  188. return $found; 
  189.  
  190. protected function combineSelectorSingle($base, $other) { 
  191. $tag = null; 
  192. $out = array(); 
  193.  
  194. foreach (array($base, $other) as $single) { 
  195. foreach ($single as $part) { 
  196. if (preg_match('/^[^\[.#:]/', $part)) { 
  197. $tag = $part; 
  198. } else { 
  199. $out[] = $part; 
  200.  
  201. if ($tag) { 
  202. array_unshift($out, $tag); 
  203.  
  204. return $out; 
  205.  
  206. protected function matchExtends($selector, &$out, $from = 0, $initial=true) { 
  207. foreach ($selector as $i => $part) { 
  208. if ($i < $from) continue; 
  209.  
  210. if ($this->matchExtendsSingle($part, $origin)) { 
  211. $before = array_slice($selector, 0, $i); 
  212. $after = array_slice($selector, $i + 1); 
  213.  
  214. foreach ($origin as $new) { 
  215. $k = 0; 
  216.  
  217. // remove shared parts 
  218. if ($initial) { 
  219. foreach ($before as $k => $val) { 
  220. if (!isset($new[$k]) || $val != $new[$k]) { 
  221. break; 
  222.  
  223. $result = array_merge( 
  224. $before,  
  225. $k > 0 ? array_slice($new, $k) : $new,  
  226. $after); 
  227.  
  228.  
  229. if ($result == $selector) continue; 
  230. $out[] = $result; 
  231.  
  232. // recursively check for more matches 
  233. $this->matchExtends($result, $out, $i, false); 
  234.  
  235. // selector sequence merging 
  236. if (!empty($before) && count($new) > 1) { 
  237. $result2 = array_merge( 
  238. array_slice($new, 0, -1),  
  239. $k > 0 ? array_slice($before, $k) : $before,  
  240. array_slice($new, -1),  
  241. $after); 
  242.  
  243. $out[] = $result2; 
  244.  
  245. protected function flattenSelectors($block, $parentKey = null) { 
  246. if ($block->selectors) { 
  247. $selectors = array(); 
  248. foreach ($block->selectors as $s) { 
  249. $selectors[] = $s; 
  250. if (!is_array($s)) continue; 
  251. // check extends 
  252. if (!empty($this->extendsMap)) { 
  253. $this->matchExtends($s, $selectors); 
  254.  
  255. $block->selectors = array(); 
  256. $placeholderSelector = false; 
  257. foreach ($selectors as $selector) { 
  258. if ($this->hasSelectorPlaceholder($selector)) { 
  259. $placeholderSelector = true; 
  260. continue; 
  261. $block->selectors[] = $this->compileSelector($selector); 
  262.  
  263. if ($placeholderSelector && 0 == count($block->selectors) && null !== $parentKey) { 
  264. unset($block->parent->children[$parentKey]); 
  265. return; 
  266.  
  267. foreach ($block->children as $key => $child) { 
  268. $this->flattenSelectors($child, $key); 
  269.  
  270. protected function compileRoot($rootBlock) { 
  271. $this->pushEnv($rootBlock); 
  272. $this->scope = $this->makeOutputBlock("root"); 
  273.  
  274. $this->compileChildren($rootBlock->children, $this->scope); 
  275. $this->flattenSelectors($this->scope); 
  276.  
  277. $this->popEnv(); 
  278.  
  279. protected function compileMedia($media) { 
  280. $this->pushEnv($media); 
  281. $parentScope = $this->mediaParent($this->scope); 
  282.  
  283. $this->scope = $this->makeOutputBlock("media", array( 
  284. $this->compileMediaQuery($this->multiplyMedia($this->env))) 
  285. ); 
  286.  
  287. $parentScope->children[] = $this->scope; 
  288.  
  289. // top level properties in a media cause it to be wrapped 
  290. $needsWrap = false; 
  291. foreach ($media->children as $child) { 
  292. $type = $child[0]; 
  293. if ($type !== 'block' && $type !== 'media' && $type !== 'directive') { 
  294. $needsWrap = true; 
  295. break; 
  296.  
  297. if ($needsWrap) { 
  298. $wrapped = (object)array( 
  299. "selectors" => array(),  
  300. "children" => $media->children 
  301. ); 
  302. $media->children = array(array("block", $wrapped)); 
  303.  
  304. $this->compileChildren($media->children, $this->scope); 
  305.  
  306. $this->scope = $this->scope->parent; 
  307. $this->popEnv(); 
  308.  
  309. protected function mediaParent($scope) { 
  310. while (!empty($scope->parent)) { 
  311. if (!empty($scope->type) && $scope->type != "media") { 
  312. break; 
  313. $scope = $scope->parent; 
  314.  
  315. return $scope; 
  316.  
  317. // TODO refactor compileNestedBlock and compileMedia into same thing 
  318. protected function compileNestedBlock($block, $selectors) { 
  319. $this->pushEnv($block); 
  320.  
  321. $this->scope = $this->makeOutputBlock($block->type, $selectors); 
  322. $this->scope->parent->children[] = $this->scope; 
  323. $this->compileChildren($block->children, $this->scope); 
  324.  
  325. $this->scope = $this->scope->parent; 
  326. $this->popEnv(); 
  327.  
  328. /** 
  329. * Recursively compiles a block. 
  330. * 
  331. * A block is analogous to a CSS block in most cases. A single SCSS document 
  332. * is encapsulated in a block when parsed, but it does not have parent tags 
  333. * so all of its children appear on the root level when compiled. 
  334. * 
  335. * Blocks are made up of selectors and children. 
  336. * 
  337. * The children of a block are just all the blocks that are defined within. 
  338. * 
  339. * Compiling the block involves pushing a fresh environment on the stack,  
  340. * and iterating through the props, compiling each one. 
  341. * 
  342. * @see scss::compileChild() 
  343. * 
  344. * @param \StdClass $block 
  345. */ 
  346. protected function compileBlock($block) { 
  347. $env = $this->pushEnv($block); 
  348.  
  349. $env->selectors = 
  350. array_map(array($this, "evalSelector"), $block->selectors); 
  351.  
  352. $out = $this->makeOutputBlock(null, $this->multiplySelectors($env)); 
  353. $this->scope->children[] = $out; 
  354. $this->compileChildren($block->children, $out); 
  355.  
  356. $this->popEnv(); 
  357.  
  358. // joins together .classes and #ids 
  359. protected function flattenSelectorSingle($single) { 
  360. $joined = array(); 
  361. foreach ($single as $part) { 
  362. if (empty($joined) || 
  363. !is_string($part) || 
  364. preg_match('/[\[.:#%]/', $part)) 
  365. $joined[] = $part; 
  366. continue; 
  367.  
  368. if (is_array(end($joined))) { 
  369. $joined[] = $part; 
  370. } else { 
  371. $joined[count($joined) - 1] .= $part; 
  372.  
  373. return $joined; 
  374.  
  375. // replaces all the interpolates 
  376. protected function evalSelector($selector) { 
  377. return array_map(array($this, "evalSelectorPart"), $selector); 
  378.  
  379. protected function evalSelectorPart($piece) { 
  380. foreach ($piece as &$p) { 
  381. if (!is_array($p)) continue; 
  382.  
  383. switch ($p[0]) { 
  384. case "interpolate": 
  385. $p = $this->compileValue($p); 
  386. break; 
  387. case "string": 
  388. $p = $this->compileValue($p); 
  389. break; 
  390.  
  391. return $this->flattenSelectorSingle($piece); 
  392.  
  393. // compiles to string 
  394. // self(&) should have been replaced by now 
  395. protected function compileSelector($selector) { 
  396. if (!is_array($selector)) return $selector; // media and the like 
  397.  
  398. return implode(" ", array_map( 
  399. array($this, "compileSelectorPart"), $selector)); 
  400.  
  401. protected function compileSelectorPart($piece) { 
  402. foreach ($piece as &$p) { 
  403. if (!is_array($p)) continue; 
  404.  
  405. switch ($p[0]) { 
  406. case "self": 
  407. $p = "&"; 
  408. break; 
  409. default: 
  410. $p = $this->compileValue($p); 
  411. break; 
  412.  
  413. return implode($piece); 
  414.  
  415. protected function hasSelectorPlaceholder($selector) 
  416. if (!is_array($selector)) return false; 
  417.  
  418. foreach ($selector as $parts) { 
  419. foreach ($parts as $part) { 
  420. if ('%' == $part[0]) { 
  421. return true; 
  422.  
  423. return false; 
  424.  
  425. protected function compileChildren($stms, $out) { 
  426. foreach ($stms as $stm) { 
  427. $ret = $this->compileChild($stm, $out); 
  428. if (!is_null($ret)) return $ret; 
  429.  
  430. protected function compileMediaQuery($queryList) { 
  431. $out = "@media"; 
  432. $first = true; 
  433. foreach ($queryList as $query) { 
  434. $parts = array(); 
  435. foreach ($query as $q) { 
  436. switch ($q[0]) { 
  437. case "mediaType": 
  438. $parts[] = implode(" ", array_map(array($this, "compileValue"), array_slice($q, 1))); 
  439. break; 
  440. case "mediaExp": 
  441. if (isset($q[2])) { 
  442. $parts[] = "(". $this->compileValue($q[1]) . $this->formatter->assignSeparator . $this->compileValue($q[2]) . ")"; 
  443. } else { 
  444. $parts[] = "(" . $this->compileValue($q[1]) . ")"; 
  445. break; 
  446. if (!empty($parts)) { 
  447. if ($first) { 
  448. $first = false; 
  449. $out .= " "; 
  450. } else { 
  451. $out .= $this->formatter->tagSeparator; 
  452. $out .= implode(" and ", $parts); 
  453. return $out; 
  454.  
  455. // returns true if the value was something that could be imported 
  456. protected function compileImport($rawPath, $out) { 
  457. if ($rawPath[0] == "string") { 
  458. $path = $this->compileStringContent($rawPath); 
  459. if ($path = $this->findImport($path)) { 
  460. $this->importFile($path, $out); 
  461. return true; 
  462. return false; 
  463. if ($rawPath[0] == "list") { 
  464. // handle a list of strings 
  465. if (count($rawPath[2]) == 0) return false; 
  466. foreach ($rawPath[2] as $path) { 
  467. if ($path[0] != "string") return false; 
  468.  
  469. foreach ($rawPath[2] as $path) { 
  470. $this->compileImport($path, $out); 
  471.  
  472. return true; 
  473.  
  474. return false; 
  475.  
  476. // return a value to halt execution 
  477. protected function compileChild($child, $out) { 
  478. $this->sourcePos = isset($child[-1]) ? $child[-1] : -1; 
  479. $this->sourceParser = isset($child[-2]) ? $child[-2] : $this->parser; 
  480.  
  481. switch ($child[0]) { 
  482. case "import": 
  483. list(, $rawPath) = $child; 
  484. $rawPath = $this->reduce($rawPath); 
  485. if (!$this->compileImport($rawPath, $out)) { 
  486. $out->lines[] = "@import " . $this->compileValue($rawPath) . ";"; 
  487. break; 
  488. case "directive": 
  489. list(, $directive) = $child; 
  490. $s = "@" . $directive->name; 
  491. if (!empty($directive->value)) { 
  492. $s .= " " . $this->compileValue($directive->value); 
  493. $this->compileNestedBlock($directive, array($s)); 
  494. break; 
  495. case "media": 
  496. $this->compileMedia($child[1]); 
  497. break; 
  498. case "block": 
  499. $this->compileBlock($child[1]); 
  500. break; 
  501. case "charset": 
  502. $out->lines[] = "@charset ".$this->compileValue($child[1]).";"; 
  503. break; 
  504. case "assign": 
  505. list(, $name, $value) = $child; 
  506. if ($name[0] == "var") { 
  507. $isDefault = !empty($child[3]); 
  508.  
  509. if ($isDefault) { 
  510. $existingValue = $this->get($name[1], true); 
  511. $shouldSet = $existingValue === true || $existingValue == self::$null; 
  512.  
  513. if (!$isDefault || $shouldSet) { 
  514. $this->set($name[1], $this->reduce($value)); 
  515. break; 
  516.  
  517. // if the value reduces to null from something else then 
  518. // the property should be discarded 
  519. if ($value[0] != "null") { 
  520. $value = $this->reduce($value); 
  521. if ($value[0] == "null") { 
  522. break; 
  523.  
  524. $compiledValue = $this->compileValue($value); 
  525. $out->lines[] = $this->formatter->property( 
  526. $this->compileValue($name),  
  527. $compiledValue); 
  528. break; 
  529. case "comment": 
  530. $out->lines[] = $child[1]; 
  531. break; 
  532. case "mixin": 
  533. case "function": 
  534. list(, $block) = $child; 
  535. $this->set(self::$namespaces[$block->type] . $block->name, $block); 
  536. break; 
  537. case "extend": 
  538. list(, $selectors) = $child; 
  539. foreach ($selectors as $sel) { 
  540. // only use the first one 
  541. $sel = current($this->evalSelector($sel)); 
  542. $this->pushExtends($sel, $out->selectors); 
  543. break; 
  544. case "if": 
  545. list(, $if) = $child; 
  546. if ($this->isTruthy($this->reduce($if->cond, true))) { 
  547. return $this->compileChildren($if->children, $out); 
  548. } else { 
  549. foreach ($if->cases as $case) { 
  550. if ($case->type == "else" || 
  551. $case->type == "elseif" && $this->isTruthy($this->reduce($case->cond))) 
  552. return $this->compileChildren($case->children, $out); 
  553. break; 
  554. case "return": 
  555. return $this->reduce($child[1], true); 
  556. case "each": 
  557. list(, $each) = $child; 
  558. $list = $this->coerceList($this->reduce($each->list)); 
  559. foreach ($list[2] as $item) { 
  560. $this->pushEnv(); 
  561. $this->set($each->var, $item); 
  562. // TODO: allow return from here 
  563. $this->compileChildren($each->children, $out); 
  564. $this->popEnv(); 
  565. break; 
  566. case "while": 
  567. list(, $while) = $child; 
  568. while ($this->isTruthy($this->reduce($while->cond, true))) { 
  569. $ret = $this->compileChildren($while->children, $out); 
  570. if ($ret) return $ret; 
  571. break; 
  572. case "for": 
  573. list(, $for) = $child; 
  574. $start = $this->reduce($for->start, true); 
  575. $start = $start[1]; 
  576. $end = $this->reduce($for->end, true); 
  577. $end = $end[1]; 
  578. $d = $start < $end ? 1 : -1; 
  579.  
  580. while (true) { 
  581. if ((!$for->until && $start - $d == $end) || 
  582. ($for->until && $start == $end)) 
  583. break; 
  584.  
  585. $this->set($for->var, array("number", $start, "")); 
  586. $start += $d; 
  587.  
  588. $ret = $this->compileChildren($for->children, $out); 
  589. if ($ret) return $ret; 
  590.  
  591. break; 
  592. case "nestedprop": 
  593. list(, $prop) = $child; 
  594. $prefixed = array(); 
  595. $prefix = $this->compileValue($prop->prefix) . "-"; 
  596. foreach ($prop->children as $child) { 
  597. if ($child[0] == "assign") { 
  598. array_unshift($child[1][2], $prefix); 
  599. if ($child[0] == "nestedprop") { 
  600. array_unshift($child[1]->prefix[2], $prefix); 
  601. $prefixed[] = $child; 
  602. $this->compileChildren($prefixed, $out); 
  603. break; 
  604. case "include": // including a mixin 
  605. list(, $name, $argValues, $content) = $child; 
  606. $mixin = $this->get(self::$namespaces["mixin"] . $name, false); 
  607. if (!$mixin) { 
  608. $this->throwError("Undefined mixin $name"); 
  609.  
  610. $callingScope = $this->env; 
  611.  
  612. // push scope, apply args 
  613. $this->pushEnv(); 
  614. if ($this->env->depth > 0) { 
  615. $this->env->depth--; 
  616.  
  617. if (!is_null($content)) { 
  618. $content->scope = $callingScope; 
  619. $this->setRaw(self::$namespaces["special"] . "content", $content); 
  620.  
  621. if (!is_null($mixin->args)) { 
  622. $this->applyArguments($mixin->args, $argValues); 
  623.  
  624. foreach ($mixin->children as $child) { 
  625. $this->compileChild($child, $out); 
  626.  
  627. $this->popEnv(); 
  628.  
  629. break; 
  630. case "mixin_content": 
  631. $content = $this->get(self::$namespaces["special"] . "content"); 
  632. if (is_null($content)) { 
  633. $this->throwError("Expected @content inside of mixin"); 
  634.  
  635. $strongTypes = array('include', 'block', 'for', 'while'); 
  636. foreach ($content->children as $child) { 
  637. $this->storeEnv = (in_array($child[0], $strongTypes)) 
  638. ? null 
  639. : $content->scope; 
  640.  
  641. $this->compileChild($child, $out); 
  642.  
  643. unset($this->storeEnv); 
  644. break; 
  645. case "debug": 
  646. list(, $value, $pos) = $child; 
  647. $line = $this->parser->getLineNo($pos); 
  648. $value = $this->compileValue($this->reduce($value, true)); 
  649. fwrite(STDERR, "Line $line DEBUG: $value\n"); 
  650. break; 
  651. default: 
  652. $this->throwError("unknown child type: $child[0]"); 
  653.  
  654. protected function expToString($exp) { 
  655. list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp; 
  656. $content = array($this->reduce($left)); 
  657. if ($whiteLeft) $content[] = " "; 
  658. $content[] = $op; 
  659. if ($whiteRight) $content[] = " "; 
  660. $content[] = $this->reduce($right); 
  661. return array("string", "", $content); 
  662.  
  663. protected function isTruthy($value) { 
  664. return $value != self::$false && $value != self::$null; 
  665.  
  666. // should $value cause its operand to eval 
  667. protected function shouldEval($value) { 
  668. switch ($value[0]) { 
  669. case "exp": 
  670. if ($value[1] == "/") { 
  671. return $this->shouldEval($value[2], $value[3]); 
  672. case "var": 
  673. case "fncall": 
  674. return true; 
  675. return false; 
  676.  
  677. protected function reduce($value, $inExp = false) { 
  678. list($type) = $value; 
  679. switch ($type) { 
  680. case "exp": 
  681. list(, $op, $left, $right, $inParens) = $value; 
  682. $opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op; 
  683.  
  684. $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right); 
  685.  
  686. $left = $this->reduce($left, true); 
  687. $right = $this->reduce($right, true); 
  688.  
  689. // only do division in special cases 
  690. if ($opName == "div" && !$inParens && !$inExp) { 
  691. if ($left[0] != "color" && $right[0] != "color") { 
  692. return $this->expToString($value); 
  693.  
  694. $left = $this->coerceForExpression($left); 
  695. $right = $this->coerceForExpression($right); 
  696.  
  697. $ltype = $left[0]; 
  698. $rtype = $right[0]; 
  699.  
  700. // this tries: 
  701. // 1. op_[op name]_[left type]_[right type] 
  702. // 2. op_[left type]_[right type] (passing the op as first arg 
  703. // 3. op_[op name] 
  704. $fn = "op_${opName}_${ltype}_${rtype}"; 
  705. if (is_callable(array($this, $fn)) || 
  706. (($fn = "op_${ltype}_${rtype}") && 
  707. is_callable(array($this, $fn)) && 
  708. $passOp = true) || 
  709. (($fn = "op_${opName}") && 
  710. is_callable(array($this, $fn)) && 
  711. $genOp = true)) 
  712. $unitChange = false; 
  713. if (!isset($genOp) && 
  714. $left[0] == "number" && $right[0] == "number") 
  715. if ($opName == "mod" && $right[2] != "") { 
  716. $this->throwError("Cannot modulo by a number with units: $right[1]$right[2]."); 
  717.  
  718. $unitChange = true; 
  719. $emptyUnit = $left[2] == "" || $right[2] == ""; 
  720. $targetUnit = "" != $left[2] ? $left[2] : $right[2]; 
  721.  
  722. if ($opName != "mul") { 
  723. $left[2] = "" != $left[2] ? $left[2] : $targetUnit; 
  724. $right[2] = "" != $right[2] ? $right[2] : $targetUnit; 
  725.  
  726. if ($opName != "mod") { 
  727. $left = $this->normalizeNumber($left); 
  728. $right = $this->normalizeNumber($right); 
  729.  
  730. if ($opName == "div" && !$emptyUnit && $left[2] == $right[2]) { 
  731. $targetUnit = ""; 
  732.  
  733. if ($opName == "mul") { 
  734. $left[2] = "" != $left[2] ? $left[2] : $right[2]; 
  735. $right[2] = "" != $right[2] ? $right[2] : $left[2]; 
  736. } elseif ($opName == "div" && $left[2] == $right[2]) { 
  737. $left[2] = ""; 
  738. $right[2] = ""; 
  739.  
  740. $shouldEval = $inParens || $inExp; 
  741. if (isset($passOp)) { 
  742. $out = $this->$fn($op, $left, $right, $shouldEval); 
  743. } else { 
  744. $out = $this->$fn($left, $right, $shouldEval); 
  745.  
  746. if (!is_null($out)) { 
  747. if ($unitChange && $out[0] == "number") { 
  748. $out = $this->coerceUnit($out, $targetUnit); 
  749. return $out; 
  750.  
  751. return $this->expToString($value); 
  752. case "unary": 
  753. list(, $op, $exp, $inParens) = $value; 
  754. $inExp = $inExp || $this->shouldEval($exp); 
  755.  
  756. $exp = $this->reduce($exp); 
  757. if ($exp[0] == "number") { 
  758. switch ($op) { 
  759. case "+": 
  760. return $exp; 
  761. case "-": 
  762. $exp[1] *= -1; 
  763. return $exp; 
  764.  
  765. if ($op == "not") { 
  766. if ($inExp || $inParens) { 
  767. if ($exp == self::$false) { 
  768. return self::$true; 
  769. } else { 
  770. return self::$false; 
  771. } else { 
  772. $op = $op . " "; 
  773.  
  774. return array("string", "", array($op, $exp)); 
  775. case "var": 
  776. list(, $name) = $value; 
  777. return $this->reduce($this->get($name)); 
  778. case "list": 
  779. foreach ($value[2] as &$item) { 
  780. $item = $this->reduce($item); 
  781. return $value; 
  782. case "string": 
  783. foreach ($value[2] as &$item) { 
  784. if (is_array($item)) { 
  785. $item = $this->reduce($item); 
  786. return $value; 
  787. case "interpolate": 
  788. $value[1] = $this->reduce($value[1]); 
  789. return $value; 
  790. case "fncall": 
  791. list(, $name, $argValues) = $value; 
  792.  
  793. // user defined function? 
  794. $func = $this->get(self::$namespaces["function"] . $name, false); 
  795. if ($func) { 
  796. $this->pushEnv(); 
  797.  
  798. // set the args 
  799. if (isset($func->args)) { 
  800. $this->applyArguments($func->args, $argValues); 
  801.  
  802. // throw away lines and children 
  803. $tmp = (object)array( 
  804. "lines" => array(),  
  805. "children" => array() 
  806. ); 
  807. $ret = $this->compileChildren($func->children, $tmp); 
  808. $this->popEnv(); 
  809.  
  810. return is_null($ret) ? self::$defaultValue : $ret; 
  811.  
  812. // built in function 
  813. if ($this->callBuiltin($name, $argValues, $returnValue)) { 
  814. return $returnValue; 
  815.  
  816. // need to flatten the arguments into a list 
  817. $listArgs = array(); 
  818. foreach ((array)$argValues as $arg) { 
  819. if (empty($arg[0])) { 
  820. $listArgs[] = $this->reduce($arg[1]); 
  821. return array("function", $name, array("list", ", ", $listArgs)); 
  822. default: 
  823. return $value; 
  824.  
  825. public function normalizeValue($value) { 
  826. $value = $this->coerceForExpression($this->reduce($value)); 
  827. list($type) = $value; 
  828.  
  829. switch ($type) { 
  830. case "list": 
  831. $value = $this->extractInterpolation($value); 
  832. if ($value[0] != "list") { 
  833. return array("keyword", $this->compileValue($value)); 
  834. foreach ($value[2] as $key => $item) { 
  835. $value[2][$key] = $this->normalizeValue($item); 
  836. return $value; 
  837. case "number": 
  838. return $this->normalizeNumber($value); 
  839. default: 
  840. return $value; 
  841.  
  842. // just does physical lengths for now 
  843. protected function normalizeNumber($number) { 
  844. list(, $value, $unit) = $number; 
  845. if (isset(self::$unitTable["in"][$unit])) { 
  846. $conv = self::$unitTable["in"][$unit]; 
  847. return array("number", $value / $conv, "in"); 
  848. return $number; 
  849.  
  850. // $number should be normalized 
  851. protected function coerceUnit($number, $unit) { 
  852. list(, $value, $baseUnit) = $number; 
  853. if (isset(self::$unitTable[$baseUnit][$unit])) { 
  854. $value = $value * self::$unitTable[$baseUnit][$unit]; 
  855.  
  856. return array("number", $value, $unit); 
  857.  
  858. protected function op_add_number_number($left, $right) { 
  859. return array("number", $left[1] + $right[1], $left[2]); 
  860.  
  861. protected function op_mul_number_number($left, $right) { 
  862. return array("number", $left[1] * $right[1], $left[2]); 
  863.  
  864. protected function op_sub_number_number($left, $right) { 
  865. return array("number", $left[1] - $right[1], $left[2]); 
  866.  
  867. protected function op_div_number_number($left, $right) { 
  868. return array("number", $left[1] / $right[1], $left[2]); 
  869.  
  870. protected function op_mod_number_number($left, $right) { 
  871. return array("number", $left[1] % $right[1], $left[2]); 
  872.  
  873. // adding strings 
  874. protected function op_add($left, $right) { 
  875. if ($strLeft = $this->coerceString($left)) { 
  876. if ($right[0] == "string") { 
  877. $right[1] = ""; 
  878. $strLeft[2][] = $right; 
  879. return $strLeft; 
  880.  
  881. if ($strRight = $this->coerceString($right)) { 
  882. if ($left[0] == "string") { 
  883. $left[1] = ""; 
  884. array_unshift($strRight[2], $left); 
  885. return $strRight; 
  886.  
  887. protected function op_and($left, $right, $shouldEval) { 
  888. if (!$shouldEval) return; 
  889. if ($left != self::$false) return $right; 
  890. return $left; 
  891.  
  892. protected function op_or($left, $right, $shouldEval) { 
  893. if (!$shouldEval) return; 
  894. if ($left != self::$false) return $left; 
  895. return $right; 
  896.  
  897. protected function op_color_color($op, $left, $right) { 
  898. $out = array('color'); 
  899. foreach (range(1, 3) as $i) { 
  900. $lval = isset($left[$i]) ? $left[$i] : 0; 
  901. $rval = isset($right[$i]) ? $right[$i] : 0; 
  902. switch ($op) { 
  903. case '+': 
  904. $out[] = $lval + $rval; 
  905. break; 
  906. case '-': 
  907. $out[] = $lval - $rval; 
  908. break; 
  909. case '*': 
  910. $out[] = $lval * $rval; 
  911. break; 
  912. case '%': 
  913. $out[] = $lval % $rval; 
  914. break; 
  915. case '/': 
  916. if ($rval == 0) { 
  917. $this->throwError("color: Can't divide by zero"); 
  918. $out[] = $lval / $rval; 
  919. break; 
  920. case "==": 
  921. return $this->op_eq($left, $right); 
  922. case "!=": 
  923. return $this->op_neq($left, $right); 
  924. default: 
  925. $this->throwError("color: unknown op $op"); 
  926.  
  927. if (isset($left[4])) $out[4] = $left[4]; 
  928. elseif (isset($right[4])) $out[4] = $right[4]; 
  929.  
  930. return $this->fixColor($out); 
  931.  
  932. protected function op_color_number($op, $left, $right) { 
  933. $value = $right[1]; 
  934. return $this->op_color_color($op, $left,  
  935. array("color", $value, $value, $value)); 
  936.  
  937. protected function op_number_color($op, $left, $right) { 
  938. $value = $left[1]; 
  939. return $this->op_color_color($op,  
  940. array("color", $value, $value, $value), $right); 
  941.  
  942. protected function op_eq($left, $right) { 
  943. if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) { 
  944. $lStr[1] = ""; 
  945. $rStr[1] = ""; 
  946. return $this->toBool($this->compileValue($lStr) == $this->compileValue($rStr)); 
  947.  
  948. return $this->toBool($left == $right); 
  949.  
  950. protected function op_neq($left, $right) { 
  951. return $this->toBool($left != $right); 
  952.  
  953. protected function op_gte_number_number($left, $right) { 
  954. return $this->toBool($left[1] >= $right[1]); 
  955.  
  956. protected function op_gt_number_number($left, $right) { 
  957. return $this->toBool($left[1] > $right[1]); 
  958.  
  959. protected function op_lte_number_number($left, $right) { 
  960. return $this->toBool($left[1] <= $right[1]); 
  961.  
  962. protected function op_lt_number_number($left, $right) { 
  963. return $this->toBool($left[1] < $right[1]); 
  964.  
  965. public function toBool($thing) { 
  966. return $thing ? self::$true : self::$false; 
  967.  
  968. /** 
  969. * Compiles a primitive value into a CSS property value. 
  970. * 
  971. * Values in scssphp are typed by being wrapped in arrays, their format is 
  972. * typically: 
  973. * 
  974. * array(type, contents [, additional_contents]*) 
  975. * 
  976. * The input is expected to be reduced. This function will not work on 
  977. * things like expressions and variables. 
  978. * 
  979. * @param array $value 
  980. */ 
  981. protected function compileValue($value) { 
  982. $value = $this->reduce($value); 
  983.  
  984. list($type) = $value; 
  985. switch ($type) { 
  986. case "keyword": 
  987. return $value[1]; 
  988. case "color": 
  989. // [1] - red component (either number for a %) 
  990. // [2] - green component 
  991. // [3] - blue component 
  992. // [4] - optional alpha component 
  993. list(, $r, $g, $b) = $value; 
  994.  
  995. $r = round($r); 
  996. $g = round($g); 
  997. $b = round($b); 
  998.  
  999. if (count($value) == 5 && $value[4] != 1) { // rgba 
  1000. return 'rgba('.$r.', '.$g.', '.$b.', '.$value[4].')'; 
  1001.  
  1002. $h = sprintf("#%02x%02x%02x", $r, $g, $b); 
  1003.  
  1004. // Converting hex color to short notation (e.g. #003399 to #039) 
  1005. if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) { 
  1006. $h = '#' . $h[1] . $h[3] . $h[5]; 
  1007.  
  1008. return $h; 
  1009. case "number": 
  1010. return round($value[1], $this->numberPrecision) . $value[2]; 
  1011. case "string": 
  1012. return $value[1] . $this->compileStringContent($value) . $value[1]; 
  1013. case "function": 
  1014. $args = !empty($value[2]) ? $this->compileValue($value[2]) : ""; 
  1015. return "$value[1]($args)"; 
  1016. case "list": 
  1017. $value = $this->extractInterpolation($value); 
  1018. if ($value[0] != "list") return $this->compileValue($value); 
  1019.  
  1020. list(, $delim, $items) = $value; 
  1021.  
  1022. $filtered = array(); 
  1023. foreach ($items as $item) { 
  1024. if ($item[0] == "null") continue; 
  1025. $filtered[] = $this->compileValue($item); 
  1026.  
  1027. return implode("$delim ", $filtered); 
  1028. case "interpolated": # node created by extractInterpolation 
  1029. list(, $interpolate, $left, $right) = $value; 
  1030. list(, , $whiteLeft, $whiteRight) = $interpolate; 
  1031.  
  1032. $left = count($left[2]) > 0 ? 
  1033. $this->compileValue($left).$whiteLeft : ""; 
  1034.  
  1035. $right = count($right[2]) > 0 ? 
  1036. $whiteRight.$this->compileValue($right) : ""; 
  1037.  
  1038. return $left.$this->compileValue($interpolate).$right; 
  1039.  
  1040. case "interpolate": # raw parse node 
  1041. list(, $exp) = $value; 
  1042.  
  1043. // strip quotes if it's a string 
  1044. $reduced = $this->reduce($exp); 
  1045. switch ($reduced[0]) { 
  1046. case "string": 
  1047. $reduced = array("keyword",  
  1048. $this->compileStringContent($reduced)); 
  1049. break; 
  1050. case "null": 
  1051. $reduced = array("keyword", ""); 
  1052.  
  1053. return $this->compileValue($reduced); 
  1054. case "null": 
  1055. return "null"; 
  1056. default: 
  1057. $this->throwError("unknown value type: $type"); 
  1058.  
  1059. protected function compileStringContent($string) { 
  1060. $parts = array(); 
  1061. foreach ($string[2] as $part) { 
  1062. if (is_array($part)) { 
  1063. $parts[] = $this->compileValue($part); 
  1064. } else { 
  1065. $parts[] = $part; 
  1066.  
  1067. return implode($parts); 
  1068.  
  1069. // doesn't need to be recursive, compileValue will handle that 
  1070. protected function extractInterpolation($list) { 
  1071. $items = $list[2]; 
  1072. foreach ($items as $i => $item) { 
  1073. if ($item[0] == "interpolate") { 
  1074. $before = array("list", $list[1], array_slice($items, 0, $i)); 
  1075. $after = array("list", $list[1], array_slice($items, $i + 1)); 
  1076. return array("interpolated", $item, $before, $after); 
  1077. return $list; 
  1078.  
  1079. // find the final set of selectors 
  1080. protected function multiplySelectors($env) { 
  1081. $envs = array(); 
  1082. while (null !== $env) { 
  1083. if (!empty($env->selectors)) { 
  1084. $envs[] = $env; 
  1085. $env = $env->parent; 
  1086. }; 
  1087.  
  1088. $selectors = array(); 
  1089. $parentSelectors = array(array()); 
  1090. while ($env = array_pop($envs)) { 
  1091. $selectors = array(); 
  1092. foreach ($env->selectors as $selector) { 
  1093. foreach ($parentSelectors as $parent) { 
  1094. $selectors[] = $this->joinSelectors($parent, $selector); 
  1095. $parentSelectors = $selectors; 
  1096.  
  1097. return $selectors; 
  1098.  
  1099. // looks for & to replace, or append parent before child 
  1100. protected function joinSelectors($parent, $child) { 
  1101. $setSelf = false; 
  1102. $out = array(); 
  1103. foreach ($child as $part) { 
  1104. $newPart = array(); 
  1105. foreach ($part as $p) { 
  1106. if ($p == self::$selfSelector) { 
  1107. $setSelf = true; 
  1108. foreach ($parent as $i => $parentPart) { 
  1109. if ($i > 0) { 
  1110. $out[] = $newPart; 
  1111. $newPart = array(); 
  1112.  
  1113. foreach ($parentPart as $pp) { 
  1114. $newPart[] = $pp; 
  1115. } else { 
  1116. $newPart[] = $p; 
  1117.  
  1118. $out[] = $newPart; 
  1119.  
  1120. return $setSelf ? $out : array_merge($parent, $child); 
  1121.  
  1122. protected function multiplyMedia($env, $childQueries = null) { 
  1123. if (is_null($env) || 
  1124. !empty($env->block->type) && $env->block->type != "media") 
  1125. return $childQueries; 
  1126.  
  1127. // plain old block, skip 
  1128. if (empty($env->block->type)) { 
  1129. return $this->multiplyMedia($env->parent, $childQueries); 
  1130.  
  1131. $parentQueries = $env->block->queryList; 
  1132. if ($childQueries == null) { 
  1133. $childQueries = $parentQueries; 
  1134. } else { 
  1135. $originalQueries = $childQueries; 
  1136. $childQueries = array(); 
  1137.  
  1138. foreach ($parentQueries as $parentQuery) { 
  1139. foreach ($originalQueries as $childQuery) { 
  1140. $childQueries []= array_merge($parentQuery, $childQuery); 
  1141.  
  1142. return $this->multiplyMedia($env->parent, $childQueries); 
  1143.  
  1144. // convert something to list 
  1145. protected function coerceList($item, $delim = ", ") { 
  1146. if (!is_null($item) && $item[0] == "list") { 
  1147. return $item; 
  1148.  
  1149. return array("list", $delim, is_null($item) ? array(): array($item)); 
  1150.  
  1151. protected function applyArguments($argDef, $argValues) { 
  1152. $hasVariable = false; 
  1153. $args = array(); 
  1154. foreach ($argDef as $i => $arg) { 
  1155. list($name, $default, $isVariable) = $argDef[$i]; 
  1156. $args[$name] = array($i, $name, $default, $isVariable); 
  1157. $hasVariable |= $isVariable; 
  1158.  
  1159. $keywordArgs = array(); 
  1160. $deferredKeywordArgs = array(); 
  1161. $remaining = array(); 
  1162. // assign the keyword args 
  1163. foreach ((array) $argValues as $arg) { 
  1164. if (!empty($arg[0])) { 
  1165. if (!isset($args[$arg[0][1]])) { 
  1166. if ($hasVariable) { 
  1167. $deferredKeywordArgs[$arg[0][1]] = $arg[1]; 
  1168. } else { 
  1169. $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]); 
  1170. } elseif ($args[$arg[0][1]][0] < count($remaining)) { 
  1171. $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]); 
  1172. } else { 
  1173. $keywordArgs[$arg[0][1]] = $arg[1]; 
  1174. } elseif (count($keywordArgs)) { 
  1175. $this->throwError('Positional arguments must come before keyword arguments.'); 
  1176. } elseif ($arg[2] == true) { 
  1177. $val = $this->reduce($arg[1], true); 
  1178. if ($val[0] == "list") { 
  1179. foreach ($val[2] as $name => $item) { 
  1180. if (!is_numeric($name)) { 
  1181. $keywordArgs[$name] = $item; 
  1182. } else { 
  1183. $remaining[] = $item; 
  1184. } else { 
  1185. $remaining[] = $val; 
  1186. } else { 
  1187. $remaining[] = $arg[1]; 
  1188.  
  1189. foreach ($args as $arg) { 
  1190. list($i, $name, $default, $isVariable) = $arg; 
  1191. if ($isVariable) { 
  1192. $val = array("list", ", ", array()); 
  1193. for ($count = count($remaining); $i < $count; $i++) { 
  1194. $val[2][] = $remaining[$i]; 
  1195. foreach ($deferredKeywordArgs as $itemName => $item) { 
  1196. $val[2][$itemName] = $item; 
  1197. } elseif (isset($remaining[$i])) { 
  1198. $val = $remaining[$i]; 
  1199. } elseif (isset($keywordArgs[$name])) { 
  1200. $val = $keywordArgs[$name]; 
  1201. } elseif (!empty($default)) { 
  1202. $val = $default; 
  1203. } else { 
  1204. $this->throwError("Missing argument $name"); 
  1205.  
  1206. $this->set($name, $this->reduce($val, true), true); 
  1207.  
  1208. protected function pushEnv($block=null) { 
  1209. $env = new stdClass; 
  1210. $env->parent = $this->env; 
  1211. $env->store = array(); 
  1212. $env->block = $block; 
  1213. $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0; 
  1214.  
  1215. $this->env = $env; 
  1216. return $env; 
  1217.  
  1218. protected function normalizeName($name) { 
  1219. return str_replace("-", "_", $name); 
  1220.  
  1221. protected function getStoreEnv() { 
  1222. return isset($this->storeEnv) ? $this->storeEnv : $this->env; 
  1223.  
  1224. protected function set($name, $value, $shadow=false) { 
  1225. $name = $this->normalizeName($name); 
  1226.  
  1227. if ($shadow) { 
  1228. $this->setRaw($name, $value); 
  1229. } else { 
  1230. $this->setExisting($name, $value); 
  1231.  
  1232. protected function setExisting($name, $value, $env = null) { 
  1233. if (is_null($env)) $env = $this->getStoreEnv(); 
  1234.  
  1235. if (isset($env->store[$name]) || is_null($env->parent)) { 
  1236. $env->store[$name] = $value; 
  1237. } else { 
  1238. $this->setExisting($name, $value, $env->parent); 
  1239.  
  1240. protected function setRaw($name, $value) { 
  1241. $env = $this->getStoreEnv(); 
  1242. $env->store[$name] = $value; 
  1243.  
  1244. public function get($name, $defaultValue = null, $env = null) { 
  1245. $name = $this->normalizeName($name); 
  1246.  
  1247. if (is_null($env)) $env = $this->getStoreEnv(); 
  1248. if (is_null($defaultValue)) $defaultValue = self::$defaultValue; 
  1249.  
  1250. if (isset($env->store[$name])) { 
  1251. return $env->store[$name]; 
  1252. } elseif (isset($env->parent)) { 
  1253. return $this->get($name, $defaultValue, $env->parent); 
  1254.  
  1255. return $defaultValue; // found nothing 
  1256.  
  1257. protected function popEnv() { 
  1258. $env = $this->env; 
  1259. $this->env = $this->env->parent; 
  1260. return $env; 
  1261.  
  1262. public function getParsedFiles() { 
  1263. return $this->parsedFiles; 
  1264.  
  1265. public function addImportPath($path) { 
  1266. $this->importPaths[] = $path; 
  1267.  
  1268. public function setImportPaths($path) { 
  1269. $this->importPaths = (array)$path; 
  1270.  
  1271. public function setNumberPrecision($numberPrecision) { 
  1272. $this->numberPrecision = $numberPrecision; 
  1273.  
  1274. public function setFormatter($formatterName) { 
  1275. $this->formatter = $formatterName; 
  1276.  
  1277. public function registerFunction($name, $func) { 
  1278. $this->userFunctions[$this->normalizeName($name)] = $func; 
  1279.  
  1280. public function unregisterFunction($name) { 
  1281. unset($this->userFunctions[$this->normalizeName($name)]); 
  1282.  
  1283. protected function importFile($path, $out) { 
  1284. // see if tree is cached 
  1285. $realPath = realpath($path); 
  1286. if (isset($this->importCache[$realPath])) { 
  1287. $tree = $this->importCache[$realPath]; 
  1288. } else { 
  1289. $code = file_get_contents($path); 
  1290. $parser = new scss_parser($path, false); 
  1291. $tree = $parser->parse($code); 
  1292. $this->parsedFiles[] = $path; 
  1293.  
  1294. $this->importCache[$realPath] = $tree; 
  1295.  
  1296. $pi = pathinfo($path); 
  1297. array_unshift($this->importPaths, $pi['dirname']); 
  1298. $this->compileChildren($tree->children, $out); 
  1299. array_shift($this->importPaths); 
  1300.  
  1301. // results the file path for an import url if it exists 
  1302. public function findImport($url) { 
  1303. $urls = array(); 
  1304.  
  1305. // for "normal" scss imports (ignore vanilla css and external requests) 
  1306. if (!preg_match('/\.css|^http:\/\/$/', $url)) { 
  1307. // try both normal and the _partial filename 
  1308. $urls = array($url, preg_replace('/[^\/]+$/', '_\0', $url)); 
  1309.  
  1310. foreach ($this->importPaths as $dir) { 
  1311. if (is_string($dir)) { 
  1312. // check urls for normal import paths 
  1313. foreach ($urls as $full) { 
  1314. $full = $dir . 
  1315. (!empty($dir) && substr($dir, -1) != '/' ? '/' : '') . 
  1316. $full; 
  1317.  
  1318. if ($this->fileExists($file = $full.'.scss') || 
  1319. $this->fileExists($file = $full)) 
  1320. return $file; 
  1321. } else { 
  1322. // check custom callback for import path 
  1323. $file = call_user_func($dir, $url, $this); 
  1324. if ($file !== null) { 
  1325. return $file; 
  1326.  
  1327. return null; 
  1328.  
  1329. protected function fileExists($name) { 
  1330. return is_file($name); 
  1331.  
  1332. protected function callBuiltin($name, $args, &$returnValue) { 
  1333. // try a lib function 
  1334. $name = $this->normalizeName($name); 
  1335. $libName = "lib_".$name; 
  1336. $f = array($this, $libName); 
  1337. $prototype = isset(self::$$libName) ? self::$$libName : null; 
  1338.  
  1339. if (is_callable($f)) { 
  1340. $sorted = $this->sortArgs($prototype, $args); 
  1341. foreach ($sorted as &$val) { 
  1342. $val = $this->reduce($val, true); 
  1343. $returnValue = call_user_func($f, $sorted, $this); 
  1344. } elseif (isset($this->userFunctions[$name])) { 
  1345. // see if we can find a user function 
  1346. $fn = $this->userFunctions[$name]; 
  1347.  
  1348. foreach ($args as &$val) { 
  1349. $val = $this->reduce($val[1], true); 
  1350.  
  1351. $returnValue = call_user_func($fn, $args, $this); 
  1352.  
  1353. if (isset($returnValue)) { 
  1354. // coerce a php value into a scss one 
  1355. if (is_numeric($returnValue)) { 
  1356. $returnValue = array('number', $returnValue, ""); 
  1357. } elseif (is_bool($returnValue)) { 
  1358. $returnValue = $returnValue ? self::$true : self::$false; 
  1359. } elseif (!is_array($returnValue)) { 
  1360. $returnValue = array('keyword', $returnValue); 
  1361.  
  1362. return true; 
  1363.  
  1364. return false; 
  1365.  
  1366. // sorts any keyword arguments 
  1367. // TODO: merge with apply arguments 
  1368. protected function sortArgs($prototype, $args) { 
  1369. $keyArgs = array(); 
  1370. $posArgs = array(); 
  1371.  
  1372. foreach ($args as $arg) { 
  1373. list($key, $value) = $arg; 
  1374. $key = $key[1]; 
  1375. if (empty($key)) { 
  1376. $posArgs[] = $value; 
  1377. } else { 
  1378. $keyArgs[$key] = $value; 
  1379.  
  1380. if (is_null($prototype)) return $posArgs; 
  1381.  
  1382. $finalArgs = array(); 
  1383. foreach ($prototype as $i => $names) { 
  1384. if (isset($posArgs[$i])) { 
  1385. $finalArgs[] = $posArgs[$i]; 
  1386. continue; 
  1387.  
  1388. $set = false; 
  1389. foreach ((array)$names as $name) { 
  1390. if (isset($keyArgs[$name])) { 
  1391. $finalArgs[] = $keyArgs[$name]; 
  1392. $set = true; 
  1393. break; 
  1394.  
  1395. if (!$set) { 
  1396. $finalArgs[] = null; 
  1397.  
  1398. return $finalArgs; 
  1399.  
  1400. protected function coerceForExpression($value) { 
  1401. if ($color = $this->coerceColor($value)) { 
  1402. return $color; 
  1403.  
  1404. return $value; 
  1405.  
  1406. protected function coerceColor($value) { 
  1407. switch ($value[0]) { 
  1408. case "color": return $value; 
  1409. case "keyword": 
  1410. $name = $value[1]; 
  1411. if (isset(self::$cssColors[$name])) { 
  1412. @list($r, $g, $b, $a) = explode(', ', self::$cssColors[$name]); 
  1413. return isset($a) 
  1414. ? array('color', (int) $r, (int) $g, (int) $b, (int) $a) 
  1415. : array('color', (int) $r, (int) $g, (int) $b); 
  1416. return null; 
  1417.  
  1418. return null; 
  1419.  
  1420. protected function coerceString($value) { 
  1421. switch ($value[0]) { 
  1422. case "string": 
  1423. return $value; 
  1424. case "keyword": 
  1425. return array("string", "", array($value[1])); 
  1426. return null; 
  1427.  
  1428. public function assertList($value) { 
  1429. if ($value[0] != "list") 
  1430. $this->throwError("expecting list"); 
  1431. return $value; 
  1432.  
  1433. public function assertColor($value) { 
  1434. if ($color = $this->coerceColor($value)) return $color; 
  1435. $this->throwError("expecting color"); 
  1436.  
  1437. public function assertNumber($value) { 
  1438. if ($value[0] != "number") 
  1439. $this->throwError("expecting number"); 
  1440. return $value[1]; 
  1441.  
  1442. protected function coercePercent($value) { 
  1443. if ($value[0] == "number") { 
  1444. if ($value[2] == "%") { 
  1445. return $value[1] / 100; 
  1446. return $value[1]; 
  1447. return 0; 
  1448.  
  1449. // make sure a color's components don't go out of bounds 
  1450. protected function fixColor($c) { 
  1451. foreach (range(1, 3) as $i) { 
  1452. if ($c[$i] < 0) $c[$i] = 0; 
  1453. if ($c[$i] > 255) $c[$i] = 255; 
  1454.  
  1455. return $c; 
  1456.  
  1457. public function toHSL($red, $green, $blue) { 
  1458. $r = $red / 255; 
  1459. $g = $green / 255; 
  1460. $b = $blue / 255; 
  1461.  
  1462. $min = min($r, $g, $b); 
  1463. $max = max($r, $g, $b); 
  1464. $d = $max - $min; 
  1465. $l = ($min + $max) / 2; 
  1466.  
  1467. if ($min == $max) { 
  1468. $s = $h = 0; 
  1469. } else { 
  1470. if ($l < 0.5) 
  1471. $s = $d / (2 * $l); 
  1472. else 
  1473. $s = $d / (2 - 2 * $l); 
  1474.  
  1475. if ($r == $max) 
  1476. $h = 60 * ($g - $b) / $d; 
  1477. elseif ($g == $max) 
  1478. $h = 60 * ($b - $r) / $d + 120; 
  1479. elseif ($b == $max) 
  1480. $h = 60 * ($r - $g) / $d + 240; 
  1481.  
  1482. return array('hsl', fmod($h, 360), $s * 100, $l * 100); 
  1483.  
  1484. public function hueToRGB($m1, $m2, $h) { 
  1485. if ($h < 0) 
  1486. $h += 1; 
  1487. elseif ($h > 1) 
  1488. $h -= 1; 
  1489.  
  1490. if ($h * 6 < 1) 
  1491. return $m1 + ($m2 - $m1) * $h * 6; 
  1492.  
  1493. if ($h * 2 < 1) 
  1494. return $m2; 
  1495.  
  1496. if ($h * 3 < 2) 
  1497. return $m1 + ($m2 - $m1) * (2/3 - $h) * 6; 
  1498.  
  1499. return $m1; 
  1500.  
  1501. // H from 0 to 360, S and L from 0 to 100 
  1502. public function toRGB($hue, $saturation, $lightness) { 
  1503. if ($hue < 0) { 
  1504. $hue += 360; 
  1505.  
  1506. $h = $hue / 360; 
  1507. $s = min(100, max(0, $saturation)) / 100; 
  1508. $l = min(100, max(0, $lightness)) / 100; 
  1509.  
  1510. $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s; 
  1511. $m1 = $l * 2 - $m2; 
  1512.  
  1513. $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255; 
  1514. $g = $this->hueToRGB($m1, $m2, $h) * 255; 
  1515. $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255; 
  1516.  
  1517. $out = array('color', $r, $g, $b); 
  1518. return $out; 
  1519.  
  1520. // Built in functions 
  1521.  
  1522. protected static $lib_if = array("condition", "if-true", "if-false"); 
  1523. protected function lib_if($args) { 
  1524. list($cond, $t, $f) = $args; 
  1525. if ($cond == self::$false) return $f; 
  1526. return $t; 
  1527.  
  1528. protected static $lib_index = array("list", "value"); 
  1529. protected function lib_index($args) { 
  1530. list($list, $value) = $args; 
  1531. $list = $this->assertList($list); 
  1532.  
  1533. $values = array(); 
  1534. foreach ($list[2] as $item) { 
  1535. $values[] = $this->normalizeValue($item); 
  1536. $key = array_search($this->normalizeValue($value), $values); 
  1537.  
  1538. return false === $key ? false : $key + 1; 
  1539.  
  1540. protected static $lib_rgb = array("red", "green", "blue"); 
  1541. protected function lib_rgb($args) { 
  1542. list($r, $g, $b) = $args; 
  1543. return array("color", $r[1], $g[1], $b[1]); 
  1544.  
  1545. protected static $lib_rgba = array( 
  1546. array("red", "color"),  
  1547. "green", "blue", "alpha"); 
  1548. protected function lib_rgba($args) { 
  1549. if ($color = $this->coerceColor($args[0])) { 
  1550. $num = is_null($args[1]) ? $args[3] : $args[1]; 
  1551. $alpha = $this->assertNumber($num); 
  1552. $color[4] = $alpha; 
  1553. return $color; 
  1554.  
  1555. list($r, $g, $b, $a) = $args; 
  1556. return array("color", $r[1], $g[1], $b[1], $a[1]); 
  1557.  
  1558. // helper function for adjust_color, change_color, and scale_color 
  1559. protected function alter_color($args, $fn) { 
  1560. $color = $this->assertColor($args[0]); 
  1561.  
  1562. foreach (array(1, 2, 3, 7) as $i) { 
  1563. if (!is_null($args[$i])) { 
  1564. $val = $this->assertNumber($args[$i]); 
  1565. $ii = $i == 7 ? 4 : $i; // alpha 
  1566. $color[$ii] = 
  1567. $this->$fn(isset($color[$ii]) ? $color[$ii] : 0, $val, $i); 
  1568.  
  1569. if (!is_null($args[4]) || !is_null($args[5]) || !is_null($args[6])) { 
  1570. $hsl = $this->toHSL($color[1], $color[2], $color[3]); 
  1571. foreach (array(4, 5, 6) as $i) { 
  1572. if (!is_null($args[$i])) { 
  1573. $val = $this->assertNumber($args[$i]); 
  1574. $hsl[$i - 3] = $this->$fn($hsl[$i - 3], $val, $i); 
  1575.  
  1576. $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]); 
  1577. if (isset($color[4])) $rgb[4] = $color[4]; 
  1578. $color = $rgb; 
  1579.  
  1580. return $color; 
  1581.  
  1582. protected static $lib_adjust_color = array( 
  1583. "color", "red", "green", "blue",  
  1584. "hue", "saturation", "lightness", "alpha" 
  1585. ); 
  1586. protected function adjust_color_helper($base, $alter, $i) { 
  1587. return $base += $alter; 
  1588. protected function lib_adjust_color($args) { 
  1589. return $this->alter_color($args, "adjust_color_helper"); 
  1590.  
  1591. protected static $lib_change_color = array( 
  1592. "color", "red", "green", "blue",  
  1593. "hue", "saturation", "lightness", "alpha" 
  1594. ); 
  1595. protected function change_color_helper($base, $alter, $i) { 
  1596. return $alter; 
  1597. protected function lib_change_color($args) { 
  1598. return $this->alter_color($args, "change_color_helper"); 
  1599.  
  1600. protected static $lib_scale_color = array( 
  1601. "color", "red", "green", "blue",  
  1602. "hue", "saturation", "lightness", "alpha" 
  1603. ); 
  1604. protected function scale_color_helper($base, $scale, $i) { 
  1605. // 1, 2, 3 - rgb 
  1606. // 4, 5, 6 - hsl 
  1607. // 7 - a 
  1608. switch ($i) { 
  1609. case 1: 
  1610. case 2: 
  1611. case 3: 
  1612. $max = 255; break; 
  1613. case 4: 
  1614. $max = 360; break; 
  1615. case 7: 
  1616. $max = 1; break; 
  1617. default: 
  1618. $max = 100; 
  1619.  
  1620. $scale = $scale / 100; 
  1621. if ($scale < 0) { 
  1622. return $base * $scale + $base; 
  1623. } else { 
  1624. return ($max - $base) * $scale + $base; 
  1625. protected function lib_scale_color($args) { 
  1626. return $this->alter_color($args, "scale_color_helper"); 
  1627.  
  1628. protected static $lib_ie_hex_str = array("color"); 
  1629. protected function lib_ie_hex_str($args) { 
  1630. $color = $this->coerceColor($args[0]); 
  1631. $color[4] = isset($color[4]) ? round(255*$color[4]) : 255; 
  1632.  
  1633. return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]); 
  1634.  
  1635. protected static $lib_red = array("color"); 
  1636. protected function lib_red($args) { 
  1637. $color = $this->coerceColor($args[0]); 
  1638. return $color[1]; 
  1639.  
  1640. protected static $lib_green = array("color"); 
  1641. protected function lib_green($args) { 
  1642. $color = $this->coerceColor($args[0]); 
  1643. return $color[2]; 
  1644.  
  1645. protected static $lib_blue = array("color"); 
  1646. protected function lib_blue($args) { 
  1647. $color = $this->coerceColor($args[0]); 
  1648. return $color[3]; 
  1649.  
  1650. protected static $lib_alpha = array("color"); 
  1651. protected function lib_alpha($args) { 
  1652. if ($color = $this->coerceColor($args[0])) { 
  1653. return isset($color[4]) ? $color[4] : 1; 
  1654.  
  1655. // this might be the IE function, so return value unchanged 
  1656. return null; 
  1657.  
  1658. protected static $lib_opacity = array("color"); 
  1659. protected function lib_opacity($args) { 
  1660. $value = $args[0]; 
  1661. if ($value[0] === 'number') return null; 
  1662. return $this->lib_alpha($args); 
  1663.  
  1664. // mix two colors 
  1665. protected static $lib_mix = array("color-1", "color-2", "weight"); 
  1666. protected function lib_mix($args) { 
  1667. list($first, $second, $weight) = $args; 
  1668. $first = $this->assertColor($first); 
  1669. $second = $this->assertColor($second); 
  1670.  
  1671. if (is_null($weight)) { 
  1672. $weight = 0.5; 
  1673. } else { 
  1674. $weight = $this->coercePercent($weight); 
  1675.  
  1676. $firstAlpha = isset($first[4]) ? $first[4] : 1; 
  1677. $secondAlpha = isset($second[4]) ? $second[4] : 1; 
  1678.  
  1679. $w = $weight * 2 - 1; 
  1680. $a = $firstAlpha - $secondAlpha; 
  1681.  
  1682. $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0; 
  1683. $w2 = 1.0 - $w1; 
  1684.  
  1685. $new = array('color',  
  1686. $w1 * $first[1] + $w2 * $second[1],  
  1687. $w1 * $first[2] + $w2 * $second[2],  
  1688. $w1 * $first[3] + $w2 * $second[3],  
  1689. ); 
  1690.  
  1691. if ($firstAlpha != 1.0 || $secondAlpha != 1.0) { 
  1692. $new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1); 
  1693.  
  1694. return $this->fixColor($new); 
  1695.  
  1696. protected static $lib_hsl = array("hue", "saturation", "lightness"); 
  1697. protected function lib_hsl($args) { 
  1698. list($h, $s, $l) = $args; 
  1699. return $this->toRGB($h[1], $s[1], $l[1]); 
  1700.  
  1701. protected static $lib_hsla = array("hue", "saturation",  
  1702. "lightness", "alpha"); 
  1703. protected function lib_hsla($args) { 
  1704. list($h, $s, $l, $a) = $args; 
  1705. $color = $this->toRGB($h[1], $s[1], $l[1]); 
  1706. $color[4] = $a[1]; 
  1707. return $color; 
  1708.  
  1709. protected static $lib_hue = array("color"); 
  1710. protected function lib_hue($args) { 
  1711. $color = $this->assertColor($args[0]); 
  1712. $hsl = $this->toHSL($color[1], $color[2], $color[3]); 
  1713. return array("number", $hsl[1], "deg"); 
  1714.  
  1715. protected static $lib_saturation = array("color"); 
  1716. protected function lib_saturation($args) { 
  1717. $color = $this->assertColor($args[0]); 
  1718. $hsl = $this->toHSL($color[1], $color[2], $color[3]); 
  1719. return array("number", $hsl[2], "%"); 
  1720.  
  1721. protected static $lib_lightness = array("color"); 
  1722. protected function lib_lightness($args) { 
  1723. $color = $this->assertColor($args[0]); 
  1724. $hsl = $this->toHSL($color[1], $color[2], $color[3]); 
  1725. return array("number", $hsl[3], "%"); 
  1726.  
  1727. protected function adjustHsl($color, $idx, $amount) { 
  1728. $hsl = $this->toHSL($color[1], $color[2], $color[3]); 
  1729. $hsl[$idx] += $amount; 
  1730. $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]); 
  1731. if (isset($color[4])) $out[4] = $color[4]; 
  1732. return $out; 
  1733.  
  1734. protected static $lib_adjust_hue = array("color", "degrees"); 
  1735. protected function lib_adjust_hue($args) { 
  1736. $color = $this->assertColor($args[0]); 
  1737. $degrees = $this->assertNumber($args[1]); 
  1738. return $this->adjustHsl($color, 1, $degrees); 
  1739.  
  1740. protected static $lib_lighten = array("color", "amount"); 
  1741. protected function lib_lighten($args) { 
  1742. $color = $this->assertColor($args[0]); 
  1743. $amount = 100*$this->coercePercent($args[1]); 
  1744. return $this->adjustHsl($color, 3, $amount); 
  1745.  
  1746. protected static $lib_darken = array("color", "amount"); 
  1747. protected function lib_darken($args) { 
  1748. $color = $this->assertColor($args[0]); 
  1749. $amount = 100*$this->coercePercent($args[1]); 
  1750. return $this->adjustHsl($color, 3, -$amount); 
  1751.  
  1752. protected static $lib_saturate = array("color", "amount"); 
  1753. protected function lib_saturate($args) { 
  1754. $value = $args[0]; 
  1755. if ($value[0] === 'number') return null; 
  1756. $color = $this->assertColor($value); 
  1757. $amount = 100*$this->coercePercent($args[1]); 
  1758. return $this->adjustHsl($color, 2, $amount); 
  1759.  
  1760. protected static $lib_desaturate = array("color", "amount"); 
  1761. protected function lib_desaturate($args) { 
  1762. $color = $this->assertColor($args[0]); 
  1763. $amount = 100*$this->coercePercent($args[1]); 
  1764. return $this->adjustHsl($color, 2, -$amount); 
  1765.  
  1766. protected static $lib_grayscale = array("color"); 
  1767. protected function lib_grayscale($args) { 
  1768. $value = $args[0]; 
  1769. if ($value[0] === 'number') return null; 
  1770. return $this->adjustHsl($this->assertColor($value), 2, -100); 
  1771.  
  1772. protected static $lib_complement = array("color"); 
  1773. protected function lib_complement($args) { 
  1774. return $this->adjustHsl($this->assertColor($args[0]), 1, 180); 
  1775.  
  1776. protected static $lib_invert = array("color"); 
  1777. protected function lib_invert($args) { 
  1778. $value = $args[0]; 
  1779. if ($value[0] === 'number') return null; 
  1780. $color = $this->assertColor($value); 
  1781. $color[1] = 255 - $color[1]; 
  1782. $color[2] = 255 - $color[2]; 
  1783. $color[3] = 255 - $color[3]; 
  1784. return $color; 
  1785.  
  1786. // increases opacity by amount 
  1787. protected static $lib_opacify = array("color", "amount"); 
  1788. protected function lib_opacify($args) { 
  1789. $color = $this->assertColor($args[0]); 
  1790. $amount = $this->coercePercent($args[1]); 
  1791.  
  1792. $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount; 
  1793. $color[4] = min(1, max(0, $color[4])); 
  1794. return $color; 
  1795.  
  1796. protected static $lib_fade_in = array("color", "amount"); 
  1797. protected function lib_fade_in($args) { 
  1798. return $this->lib_opacify($args); 
  1799.  
  1800. // decreases opacity by amount 
  1801. protected static $lib_transparentize = array("color", "amount"); 
  1802. protected function lib_transparentize($args) { 
  1803. $color = $this->assertColor($args[0]); 
  1804. $amount = $this->coercePercent($args[1]); 
  1805.  
  1806. $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount; 
  1807. $color[4] = min(1, max(0, $color[4])); 
  1808. return $color; 
  1809.  
  1810. protected static $lib_fade_out = array("color", "amount"); 
  1811. protected function lib_fade_out($args) { 
  1812. return $this->lib_transparentize($args); 
  1813.  
  1814. protected static $lib_unquote = array("string"); 
  1815. protected function lib_unquote($args) { 
  1816. $str = $args[0]; 
  1817. if ($str[0] == "string") $str[1] = ""; 
  1818. return $str; 
  1819.  
  1820. protected static $lib_quote = array("string"); 
  1821. protected function lib_quote($args) { 
  1822. $value = $args[0]; 
  1823. if ($value[0] == "string" && !empty($value[1])) 
  1824. return $value; 
  1825. return array("string", '"', array($value)); 
  1826.  
  1827. protected static $lib_percentage = array("value"); 
  1828. protected function lib_percentage($args) { 
  1829. return array("number",  
  1830. $this->coercePercent($args[0]) * 100,  
  1831. "%"); 
  1832.  
  1833. protected static $lib_round = array("value"); 
  1834. protected function lib_round($args) { 
  1835. $num = $args[0]; 
  1836. $num[1] = round($num[1]); 
  1837. return $num; 
  1838.  
  1839. protected static $lib_floor = array("value"); 
  1840. protected function lib_floor($args) { 
  1841. $num = $args[0]; 
  1842. $num[1] = floor($num[1]); 
  1843. return $num; 
  1844.  
  1845. protected static $lib_ceil = array("value"); 
  1846. protected function lib_ceil($args) { 
  1847. $num = $args[0]; 
  1848. $num[1] = ceil($num[1]); 
  1849. return $num; 
  1850.  
  1851. protected static $lib_abs = array("value"); 
  1852. protected function lib_abs($args) { 
  1853. $num = $args[0]; 
  1854. $num[1] = abs($num[1]); 
  1855. return $num; 
  1856.  
  1857. protected function lib_min($args) { 
  1858. $numbers = $this->getNormalizedNumbers($args); 
  1859. $min = null; 
  1860. foreach ($numbers as $key => $number) { 
  1861. if (null === $min || $number[1] <= $min[1]) { 
  1862. $min = array($key, $number[1]); 
  1863.  
  1864. return $args[$min[0]]; 
  1865.  
  1866. protected function lib_max($args) { 
  1867. $numbers = $this->getNormalizedNumbers($args); 
  1868. $max = null; 
  1869. foreach ($numbers as $key => $number) { 
  1870. if (null === $max || $number[1] >= $max[1]) { 
  1871. $max = array($key, $number[1]); 
  1872.  
  1873. return $args[$max[0]]; 
  1874.  
  1875. protected function getNormalizedNumbers($args) { 
  1876. $unit = null; 
  1877. $originalUnit = null; 
  1878. $numbers = array(); 
  1879. foreach ($args as $key => $item) { 
  1880. if ('number' != $item[0]) { 
  1881. $this->throwError("%s is not a number", $item[0]); 
  1882. $number = $this->normalizeNumber($item); 
  1883.  
  1884. if (null === $unit) { 
  1885. $unit = $number[2]; 
  1886. $originalUnit = $item[2]; 
  1887. } elseif ($unit !== $number[2]) { 
  1888. $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item[2]); 
  1889.  
  1890. $numbers[$key] = $number; 
  1891.  
  1892. return $numbers; 
  1893.  
  1894. protected static $lib_length = array("list"); 
  1895. protected function lib_length($args) { 
  1896. $list = $this->coerceList($args[0]); 
  1897. return count($list[2]); 
  1898.  
  1899. protected static $lib_nth = array("list", "n"); 
  1900. protected function lib_nth($args) { 
  1901. $list = $this->coerceList($args[0]); 
  1902. $n = $this->assertNumber($args[1]) - 1; 
  1903. return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue; 
  1904.  
  1905. protected function listSeparatorForJoin($list1, $sep) { 
  1906. if (is_null($sep)) return $list1[1]; 
  1907. switch ($this->compileValue($sep)) { 
  1908. case "comma": 
  1909. return ", "; 
  1910. case "space": 
  1911. return ""; 
  1912. default: 
  1913. return $list1[1]; 
  1914.  
  1915. protected static $lib_join = array("list1", "list2", "separator"); 
  1916. protected function lib_join($args) { 
  1917. list($list1, $list2, $sep) = $args; 
  1918. $list1 = $this->coerceList($list1, " "); 
  1919. $list2 = $this->coerceList($list2, " "); 
  1920. $sep = $this->listSeparatorForJoin($list1, $sep); 
  1921. return array("list", $sep, array_merge($list1[2], $list2[2])); 
  1922.  
  1923. protected static $lib_append = array("list", "val", "separator"); 
  1924. protected function lib_append($args) { 
  1925. list($list1, $value, $sep) = $args; 
  1926. $list1 = $this->coerceList($list1, " "); 
  1927. $sep = $this->listSeparatorForJoin($list1, $sep); 
  1928. return array("list", $sep, array_merge($list1[2], array($value))); 
  1929.  
  1930. protected function lib_zip($args) { 
  1931. foreach ($args as $arg) { 
  1932. $this->assertList($arg); 
  1933.  
  1934. $lists = array(); 
  1935. $firstList = array_shift($args); 
  1936. foreach ($firstList[2] as $key => $item) { 
  1937. $list = array("list", "", array($item)); 
  1938. foreach ($args as $arg) { 
  1939. if (isset($arg[2][$key])) { 
  1940. $list[2][] = $arg[2][$key]; 
  1941. } else { 
  1942. break 2; 
  1943. $lists[] = $list; 
  1944.  
  1945. return array("list", ", ", $lists); 
  1946.  
  1947. protected static $lib_type_of = array("value"); 
  1948. protected function lib_type_of($args) { 
  1949. $value = $args[0]; 
  1950. switch ($value[0]) { 
  1951. case "keyword": 
  1952. if ($value == self::$true || $value == self::$false) { 
  1953. return "bool"; 
  1954.  
  1955. if ($this->coerceColor($value)) { 
  1956. return "color"; 
  1957.  
  1958. return "string"; 
  1959. default: 
  1960. return $value[0]; 
  1961.  
  1962. protected static $lib_unit = array("number"); 
  1963. protected function lib_unit($args) { 
  1964. $num = $args[0]; 
  1965. if ($num[0] == "number") { 
  1966. return array("string", '"', array($num[2])); 
  1967. return ""; 
  1968.  
  1969. protected static $lib_unitless = array("number"); 
  1970. protected function lib_unitless($args) { 
  1971. $value = $args[0]; 
  1972. return $value[0] == "number" && empty($value[2]); 
  1973.  
  1974. protected static $lib_comparable = array("number-1", "number-2"); 
  1975. protected function lib_comparable($args) { 
  1976. list($number1, $number2) = $args; 
  1977. if (!isset($number1[0]) || $number1[0] != "number" || !isset($number2[0]) || $number2[0] != "number") { 
  1978. $this->throwError('Invalid argument(s) for "comparable"'); 
  1979.  
  1980. $number1 = $this->normalizeNumber($number1); 
  1981. $number2 = $this->normalizeNumber($number2); 
  1982.  
  1983. return $number1[2] == $number2[2] || $number1[2] == "" || $number2[2] == ""; 
  1984.  
  1985. /** 
  1986. * Workaround IE7's content counter bug. 
  1987. * 
  1988. * @param array $args 
  1989. */ 
  1990. protected function lib_counter($args) { 
  1991. $list = array_map(array($this, 'compileValue'), $args); 
  1992. return array('string', '', array('counter(' . implode(', ', $list) . ')')); 
  1993.  
  1994. public function throwError($msg = null) { 
  1995. if (func_num_args() > 1) { 
  1996. $msg = call_user_func_array("sprintf", func_get_args()); 
  1997.  
  1998. if ($this->sourcePos >= 0 && isset($this->sourceParser)) { 
  1999. $this->sourceParser->throwParseError($msg, $this->sourcePos); 
  2000.  
  2001. throw new Exception($msg); 
  2002.  
  2003. /** 
  2004. * CSS Colors 
  2005. * 
  2006. * @see http://www.w3.org/TR/css3-color 
  2007. */ 
  2008. static protected $cssColors = array( 
  2009. 'aliceblue' => '240, 248, 255',  
  2010. 'antiquewhite' => '250, 235, 215',  
  2011. 'aqua' => '0, 255, 255',  
  2012. 'aquamarine' => '127, 255, 212',  
  2013. 'azure' => '240, 255, 255',  
  2014. 'beige' => '245, 245, 220',  
  2015. 'bisque' => '255, 228, 196',  
  2016. 'black' => '0, 0, 0',  
  2017. 'blanchedalmond' => '255, 235, 205',  
  2018. 'blue' => '0, 0, 255',  
  2019. 'blueviolet' => '138, 43, 226',  
  2020. 'brown' => '165, 42, 42',  
  2021. 'burlywood' => '222, 184, 135',  
  2022. 'cadetblue' => '95, 158, 160',  
  2023. 'chartreuse' => '127, 255, 0',  
  2024. 'chocolate' => '210, 105, 30',  
  2025. 'coral' => '255, 127, 80',  
  2026. 'cornflowerblue' => '100, 149, 237',  
  2027. 'cornsilk' => '255, 248, 220',  
  2028. 'crimson' => '220, 20, 60',  
  2029. 'cyan' => '0, 255, 255',  
  2030. 'darkblue' => '0, 0, 139',  
  2031. 'darkcyan' => '0, 139, 139',  
  2032. 'darkgoldenrod' => '184, 134, 11',  
  2033. 'darkgray' => '169, 169, 169',  
  2034. 'darkgreen' => '0, 100, 0',  
  2035. 'darkgrey' => '169, 169, 169',  
  2036. 'darkkhaki' => '189, 183, 107',  
  2037. 'darkmagenta' => '139, 0, 139',  
  2038. 'darkolivegreen' => '85, 107, 47',  
  2039. 'darkorange' => '255, 140, 0',  
  2040. 'darkorchid' => '153, 50, 204',  
  2041. 'darkred' => '139, 0, 0',  
  2042. 'darksalmon' => '233, 150, 122',  
  2043. 'darkseagreen' => '143, 188, 143',  
  2044. 'darkslateblue' => '72, 61, 139',  
  2045. 'darkslategray' => '47, 79, 79',  
  2046. 'darkslategrey' => '47, 79, 79',  
  2047. 'darkturquoise' => '0, 206, 209',  
  2048. 'darkviolet' => '148, 0, 211',  
  2049. 'deeppink' => '255, 20, 147',  
  2050. 'deepskyblue' => '0, 191, 255',  
  2051. 'dimgray' => '105, 105, 105',  
  2052. 'dimgrey' => '105, 105, 105',  
  2053. 'dodgerblue' => '30, 144, 255',  
  2054. 'firebrick' => '178, 34, 34',  
  2055. 'floralwhite' => '255, 250, 240',  
  2056. 'forestgreen' => '34, 139, 34',  
  2057. 'fuchsia' => '255, 0, 255',  
  2058. 'gainsboro' => '220, 220, 220',  
  2059. 'ghostwhite' => '248, 248, 255',  
  2060. 'gold' => '255, 215, 0',  
  2061. 'goldenrod' => '218, 165, 32',  
  2062. 'gray' => '128, 128, 128',  
  2063. 'green' => '0, 128, 0',  
  2064. 'greenyellow' => '173, 255, 47',  
  2065. 'grey' => '128, 128, 128',  
  2066. 'honeydew' => '240, 255, 240',  
  2067. 'hotpink' => '255, 105, 180',  
  2068. 'indianred' => '205, 92, 92',  
  2069. 'indigo' => '75, 0, 130',  
  2070. 'ivory' => '255, 255, 240',  
  2071. 'khaki' => '240, 230, 140',  
  2072. 'lavender' => '230, 230, 250',  
  2073. 'lavenderblush' => '255, 240, 245',  
  2074. 'lawngreen' => '124, 252, 0',  
  2075. 'lemonchiffon' => '255, 250, 205',  
  2076. 'lightblue' => '173, 216, 230',  
  2077. 'lightcoral' => '240, 128, 128',  
  2078. 'lightcyan' => '224, 255, 255',  
  2079. 'lightgoldenrodyellow' => '250, 250, 210',  
  2080. 'lightgray' => '211, 211, 211',  
  2081. 'lightgreen' => '144, 238, 144',  
  2082. 'lightgrey' => '211, 211, 211',  
  2083. 'lightpink' => '255, 182, 193',  
  2084. 'lightsalmon' => '255, 160, 122',  
  2085. 'lightseagreen' => '32, 178, 170',  
  2086. 'lightskyblue' => '135, 206, 250',  
  2087. 'lightslategray' => '119, 136, 153',  
  2088. 'lightslategrey' => '119, 136, 153',  
  2089. 'lightsteelblue' => '176, 196, 222',  
  2090. 'lightyellow' => '255, 255, 224',  
  2091. 'lime' => '0, 255, 0',  
  2092. 'limegreen' => '50, 205, 50',  
  2093. 'linen' => '250, 240, 230',  
  2094. 'magenta' => '255, 0, 255',  
  2095. 'maroon' => '128, 0, 0',  
  2096. 'mediumaquamarine' => '102, 205, 170',  
  2097. 'mediumblue' => '0, 0, 205',  
  2098. 'mediumorchid' => '186, 85, 211',  
  2099. 'mediumpurple' => '147, 112, 219',  
  2100. 'mediumseagreen' => '60, 179, 113',  
  2101. 'mediumslateblue' => '123, 104, 238',  
  2102. 'mediumspringgreen' => '0, 250, 154',  
  2103. 'mediumturquoise' => '72, 209, 204',  
  2104. 'mediumvioletred' => '199, 21, 133',  
  2105. 'midnightblue' => '25, 25, 112',  
  2106. 'mintcream' => '245, 255, 250',  
  2107. 'mistyrose' => '255, 228, 225',  
  2108. 'moccasin' => '255, 228, 181',  
  2109. 'navajowhite' => '255, 222, 173',  
  2110. 'navy' => '0, 0, 128',  
  2111. 'oldlace' => '253, 245, 230',  
  2112. 'olive' => '128, 128, 0',  
  2113. 'olivedrab' => '107, 142, 35',  
  2114. 'orange' => '255, 165, 0',  
  2115. 'orangered' => '255, 69, 0',  
  2116. 'orchid' => '218, 112, 214',  
  2117. 'palegoldenrod' => '238, 232, 170',  
  2118. 'palegreen' => '152, 251, 152',  
  2119. 'paleturquoise' => '175, 238, 238',  
  2120. 'palevioletred' => '219, 112, 147',  
  2121. 'papayawhip' => '255, 239, 213',  
  2122. 'peachpuff' => '255, 218, 185',  
  2123. 'peru' => '205, 133, 63',  
  2124. 'pink' => '255, 192, 203',  
  2125. 'plum' => '221, 160, 221',  
  2126. 'powderblue' => '176, 224, 230',  
  2127. 'purple' => '128, 0, 128',  
  2128. 'red' => '255, 0, 0',  
  2129. 'rosybrown' => '188, 143, 143',  
  2130. 'royalblue' => '65, 105, 225',  
  2131. 'saddlebrown' => '139, 69, 19',  
  2132. 'salmon' => '250, 128, 114',  
  2133. 'sandybrown' => '244, 164, 96',  
  2134. 'seagreen' => '46, 139, 87',  
  2135. 'seashell' => '255, 245, 238',  
  2136. 'sienna' => '160, 82, 45',  
  2137. 'silver' => '192, 192, 192',  
  2138. 'skyblue' => '135, 206, 235',  
  2139. 'slateblue' => '106, 90, 205',  
  2140. 'slategray' => '112, 128, 144',  
  2141. 'slategrey' => '112, 128, 144',  
  2142. 'snow' => '255, 250, 250',  
  2143. 'springgreen' => '0, 255, 127',  
  2144. 'steelblue' => '70, 130, 180',  
  2145. 'tan' => '210, 180, 140',  
  2146. 'teal' => '0, 128, 128',  
  2147. 'thistle' => '216, 191, 216',  
  2148. 'tomato' => '255, 99, 71',  
  2149. 'transparent' => '0, 0, 0, 0',  
  2150. 'turquoise' => '64, 224, 208',  
  2151. 'violet' => '238, 130, 238',  
  2152. 'wheat' => '245, 222, 179',  
  2153. 'white' => '255, 255, 255',  
  2154. 'whitesmoke' => '245, 245, 245',  
  2155. 'yellow' => '255, 255, 0',  
  2156. 'yellowgreen' => '154, 205, 50' 
  2157. ); 
  2158.  
  2159. /** 
  2160. * SCSS parser 
  2161. * 
  2162. * @author Leaf Corcoran <leafot@gmail.com> 
  2163. */ 
  2164. class scss_parser { 
  2165. static protected $precedence = array( 
  2166. "or" => 0,  
  2167. "and" => 1,  
  2168.  
  2169. '==' => 2,  
  2170. '!=' => 2,  
  2171. '<=' => 2,  
  2172. '>=' => 2,  
  2173. '=' => 2,  
  2174. '<' => 3,  
  2175. '>' => 2,  
  2176.  
  2177. '+' => 3,  
  2178. '-' => 3,  
  2179. '*' => 4,  
  2180. '/' => 4,  
  2181. '%' => 4,  
  2182. ); 
  2183.  
  2184. static protected $operators = array("+", "-", "*", "/", "%",  
  2185. "==", "!=", "<=", ">=", "<", ">", "and", "or"); 
  2186.  
  2187. static protected $operatorStr; 
  2188. static protected $whitePattern; 
  2189. static protected $commentMulti; 
  2190.  
  2191. static protected $commentSingle = "//"; 
  2192. static protected $commentMultiLeft = "/*"; 
  2193. static protected $commentMultiRight = "*/"; 
  2194.  
  2195. public function __construct($sourceName = null, $rootParser = true) { 
  2196. $this->sourceName = $sourceName; 
  2197. $this->rootParser = $rootParser; 
  2198.  
  2199. if (empty(self::$operatorStr)) { 
  2200. self::$operatorStr = $this->makeOperatorStr(self::$operators); 
  2201.  
  2202. $commentSingle = $this->preg_quote(self::$commentSingle); 
  2203. $commentMultiLeft = $this->preg_quote(self::$commentMultiLeft); 
  2204. $commentMultiRight = $this->preg_quote(self::$commentMultiRight); 
  2205. self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight; 
  2206. self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais'; 
  2207.  
  2208. static protected function makeOperatorStr($operators) { 
  2209. return '('.implode('|', array_map(array('scss_parser', 'preg_quote'),  
  2210. $operators)).')'; 
  2211.  
  2212. public function parse($buffer) { 
  2213. $this->count = 0; 
  2214. $this->env = null; 
  2215. $this->inParens = false; 
  2216. $this->pushBlock(null); // root block 
  2217. $this->eatWhiteDefault = true; 
  2218. $this->insertComments = true; 
  2219.  
  2220. $this->buffer = $buffer; 
  2221.  
  2222. $this->whitespace(); 
  2223. while (false !== $this->parseChunk()); 
  2224.  
  2225. if ($this->count != strlen($this->buffer)) 
  2226. $this->throwParseError(); 
  2227.  
  2228. if (!empty($this->env->parent)) { 
  2229. $this->throwParseError("unclosed block"); 
  2230.  
  2231. $this->env->isRoot = true; 
  2232. return $this->env; 
  2233.  
  2234. /** 
  2235. * Parse a single chunk off the head of the buffer and append it to the 
  2236. * current parse environment. 
  2237. * 
  2238. * Returns false when the buffer is empty, or when there is an error. 
  2239. * 
  2240. * This function is called repeatedly until the entire document is 
  2241. * parsed. 
  2242. * 
  2243. * This parser is most similar to a recursive descent parser. Single 
  2244. * functions represent discrete grammatical rules for the language, and 
  2245. * they are able to capture the text that represents those rules. 
  2246. * 
  2247. * Consider the function scssc::keyword(). (All parse functions are 
  2248. * structured the same.) 
  2249. * 
  2250. * The function takes a single reference argument. When calling the 
  2251. * function it will attempt to match a keyword on the head of the buffer. 
  2252. * If it is successful, it will place the keyword in the referenced 
  2253. * argument, advance the position in the buffer, and return true. If it 
  2254. * fails then it won't advance the buffer and it will return false. 
  2255. * 
  2256. * All of these parse functions are powered by scssc::match(), which behaves 
  2257. * the same way, but takes a literal regular expression. Sometimes it is 
  2258. * more convenient to use match instead of creating a new function. 
  2259. * 
  2260. * Because of the format of the functions, to parse an entire string of 
  2261. * grammatical rules, you can chain them together using &&. 
  2262. * 
  2263. * But, if some of the rules in the chain succeed before one fails, then 
  2264. * the buffer position will be left at an invalid state. In order to 
  2265. * avoid this, scssc::seek() is used to remember and set buffer positions. 
  2266. * 
  2267. * Before parsing a chain, use $s = $this->seek() to remember the current 
  2268. * position into $s. Then if a chain fails, use $this->seek($s) to 
  2269. * go back where we started. 
  2270. * 
  2271. * @return boolean 
  2272. */ 
  2273. protected function parseChunk() { 
  2274. $s = $this->seek(); 
  2275.  
  2276. // the directives 
  2277. if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") { 
  2278. if ($this->literal("@media") && $this->mediaQueryList($mediaQueryList) && $this->literal("{")) { 
  2279. $media = $this->pushSpecialBlock("media"); 
  2280. $media->queryList = $mediaQueryList[2]; 
  2281. return true; 
  2282. } else { 
  2283. $this->seek($s); 
  2284.  
  2285. if ($this->literal("@mixin") && 
  2286. $this->keyword($mixinName) && 
  2287. ($this->argumentDef($args) || true) && 
  2288. $this->literal("{")) 
  2289. $mixin = $this->pushSpecialBlock("mixin"); 
  2290. $mixin->name = $mixinName; 
  2291. $mixin->args = $args; 
  2292. return true; 
  2293. } else { 
  2294. $this->seek($s); 
  2295.  
  2296. if ($this->literal("@include") && 
  2297. $this->keyword($mixinName) && 
  2298. ($this->literal("(") && 
  2299. ($this->argValues($argValues) || true) && 
  2300. $this->literal(")") || true) && 
  2301. ($this->end() || 
  2302. $this->literal("{") && $hasBlock = true)) 
  2303. $child = array("include",  
  2304. $mixinName, isset($argValues) ? $argValues : null, null); 
  2305.  
  2306. if (!empty($hasBlock)) { 
  2307. $include = $this->pushSpecialBlock("include"); 
  2308. $include->child = $child; 
  2309. } else { 
  2310. $this->append($child, $s); 
  2311.  
  2312. return true; 
  2313. } else { 
  2314. $this->seek($s); 
  2315.  
  2316. if ($this->literal("@import") && 
  2317. $this->valueList($importPath) && 
  2318. $this->end()) 
  2319. $this->append(array("import", $importPath), $s); 
  2320. return true; 
  2321. } else { 
  2322. $this->seek($s); 
  2323.  
  2324. if ($this->literal("@extend") && 
  2325. $this->selectors($selector) && 
  2326. $this->end()) 
  2327. $this->append(array("extend", $selector), $s); 
  2328. return true; 
  2329. } else { 
  2330. $this->seek($s); 
  2331.  
  2332. if ($this->literal("@function") && 
  2333. $this->keyword($fnName) && 
  2334. $this->argumentDef($args) && 
  2335. $this->literal("{")) 
  2336. $func = $this->pushSpecialBlock("function"); 
  2337. $func->name = $fnName; 
  2338. $func->args = $args; 
  2339. return true; 
  2340. } else { 
  2341. $this->seek($s); 
  2342.  
  2343. if ($this->literal("@return") && $this->valueList($retVal) && $this->end()) { 
  2344. $this->append(array("return", $retVal), $s); 
  2345. return true; 
  2346. } else { 
  2347. $this->seek($s); 
  2348.  
  2349. if ($this->literal("@each") && 
  2350. $this->variable($varName) && 
  2351. $this->literal("in") && 
  2352. $this->valueList($list) && 
  2353. $this->literal("{")) 
  2354. $each = $this->pushSpecialBlock("each"); 
  2355. $each->var = $varName[1]; 
  2356. $each->list = $list; 
  2357. return true; 
  2358. } else { 
  2359. $this->seek($s); 
  2360.  
  2361. if ($this->literal("@while") && 
  2362. $this->expression($cond) && 
  2363. $this->literal("{")) 
  2364. $while = $this->pushSpecialBlock("while"); 
  2365. $while->cond = $cond; 
  2366. return true; 
  2367. } else { 
  2368. $this->seek($s); 
  2369.  
  2370. if ($this->literal("@for") && 
  2371. $this->variable($varName) && 
  2372. $this->literal("from") && 
  2373. $this->expression($start) && 
  2374. ($this->literal("through") || 
  2375. ($forUntil = true && $this->literal("to"))) && 
  2376. $this->expression($end) && 
  2377. $this->literal("{")) 
  2378. $for = $this->pushSpecialBlock("for"); 
  2379. $for->var = $varName[1]; 
  2380. $for->start = $start; 
  2381. $for->end = $end; 
  2382. $for->until = isset($forUntil); 
  2383. return true; 
  2384. } else { 
  2385. $this->seek($s); 
  2386.  
  2387. if ($this->literal("@if") && $this->valueList($cond) && $this->literal("{")) { 
  2388. $if = $this->pushSpecialBlock("if"); 
  2389. $if->cond = $cond; 
  2390. $if->cases = array(); 
  2391. return true; 
  2392. } else { 
  2393. $this->seek($s); 
  2394.  
  2395. if (($this->literal("@debug") || $this->literal("@warn")) && 
  2396. $this->valueList($value) && 
  2397. $this->end()) { 
  2398. $this->append(array("debug", $value, $s), $s); 
  2399. return true; 
  2400. } else { 
  2401. $this->seek($s); 
  2402.  
  2403. if ($this->literal("@content") && $this->end()) { 
  2404. $this->append(array("mixin_content"), $s); 
  2405. return true; 
  2406. } else { 
  2407. $this->seek($s); 
  2408.  
  2409. $last = $this->last(); 
  2410. if (!is_null($last) && $last[0] == "if") { 
  2411. list(, $if) = $last; 
  2412. if ($this->literal("@else")) { 
  2413. if ($this->literal("{")) { 
  2414. $else = $this->pushSpecialBlock("else"); 
  2415. } elseif ($this->literal("if") && $this->valueList($cond) && $this->literal("{")) { 
  2416. $else = $this->pushSpecialBlock("elseif"); 
  2417. $else->cond = $cond; 
  2418.  
  2419. if (isset($else)) { 
  2420. $else->dontAppend = true; 
  2421. $if->cases[] = $else; 
  2422. return true; 
  2423.  
  2424. $this->seek($s); 
  2425.  
  2426. if ($this->literal("@charset") && 
  2427. $this->valueList($charset) && $this->end()) 
  2428. $this->append(array("charset", $charset), $s); 
  2429. return true; 
  2430. } else { 
  2431. $this->seek($s); 
  2432.  
  2433. // doesn't match built in directive, do generic one 
  2434. if ($this->literal("@", false) && $this->keyword($dirName) && 
  2435. ($this->openString("{", $dirValue) || true) && 
  2436. $this->literal("{")) 
  2437. $directive = $this->pushSpecialBlock("directive"); 
  2438. $directive->name = $dirName; 
  2439. if (isset($dirValue)) $directive->value = $dirValue; 
  2440. return true; 
  2441.  
  2442. $this->seek($s); 
  2443. return false; 
  2444.  
  2445. // property shortcut 
  2446. // captures most properties before having to parse a selector 
  2447. if ($this->keyword($name, false) && 
  2448. $this->literal(": ") && 
  2449. $this->valueList($value) && 
  2450. $this->end()) 
  2451. $name = array("string", "", array($name)); 
  2452. $this->append(array("assign", $name, $value), $s); 
  2453. return true; 
  2454. } else { 
  2455. $this->seek($s); 
  2456.  
  2457. // variable assigns 
  2458. if ($this->variable($name) && 
  2459. $this->literal(":") && 
  2460. $this->valueList($value) && $this->end()) 
  2461. // check for !default 
  2462. $defaultVar = $value[0] == "list" && $this->stripDefault($value); 
  2463. $this->append(array("assign", $name, $value, $defaultVar), $s); 
  2464. return true; 
  2465. } else { 
  2466. $this->seek($s); 
  2467.  
  2468. // misc 
  2469. if ($this->literal("-->")) { 
  2470. return true; 
  2471.  
  2472. // opening css block 
  2473. $oldComments = $this->insertComments; 
  2474. $this->insertComments = false; 
  2475. if ($this->selectors($selectors) && $this->literal("{")) { 
  2476. $this->pushBlock($selectors); 
  2477. $this->insertComments = $oldComments; 
  2478. return true; 
  2479. } else { 
  2480. $this->seek($s); 
  2481. $this->insertComments = $oldComments; 
  2482.  
  2483. // property assign, or nested assign 
  2484. if ($this->propertyName($name) && $this->literal(":")) { 
  2485. $foundSomething = false; 
  2486. if ($this->valueList($value)) { 
  2487. $this->append(array("assign", $name, $value), $s); 
  2488. $foundSomething = true; 
  2489.  
  2490. if ($this->literal("{")) { 
  2491. $propBlock = $this->pushSpecialBlock("nestedprop"); 
  2492. $propBlock->prefix = $name; 
  2493. $foundSomething = true; 
  2494. } elseif ($foundSomething) { 
  2495. $foundSomething = $this->end(); 
  2496.  
  2497. if ($foundSomething) { 
  2498. return true; 
  2499.  
  2500. $this->seek($s); 
  2501. } else { 
  2502. $this->seek($s); 
  2503.  
  2504. // closing a block 
  2505. if ($this->literal("}")) { 
  2506. $block = $this->popBlock(); 
  2507. if (isset($block->type) && $block->type == "include") { 
  2508. $include = $block->child; 
  2509. unset($block->child); 
  2510. $include[3] = $block; 
  2511. $this->append($include, $s); 
  2512. } elseif (empty($block->dontAppend)) { 
  2513. $type = isset($block->type) ? $block->type : "block"; 
  2514. $this->append(array($type, $block), $s); 
  2515. return true; 
  2516.  
  2517. // extra stuff 
  2518. if ($this->literal(";") || 
  2519. $this->literal("<!--")) 
  2520. return true; 
  2521.  
  2522. return false; 
  2523.  
  2524. protected function stripDefault(&$value) { 
  2525. $def = end($value[2]); 
  2526. if ($def[0] == "keyword" && $def[1] == "!default") { 
  2527. array_pop($value[2]); 
  2528. $value = $this->flattenList($value); 
  2529. return true; 
  2530.  
  2531. if ($def[0] == "list") { 
  2532. return $this->stripDefault($value[2][count($value[2]) - 1]); 
  2533.  
  2534. return false; 
  2535.  
  2536. protected function literal($what, $eatWhitespace = null) { 
  2537. if (is_null($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault; 
  2538.  
  2539. // shortcut on single letter 
  2540. if (!isset($what[1]) && isset($this->buffer[$this->count])) { 
  2541. if ($this->buffer[$this->count] == $what) { 
  2542. if (!$eatWhitespace) { 
  2543. $this->count++; 
  2544. return true; 
  2545. // goes below... 
  2546. } else { 
  2547. return false; 
  2548.  
  2549. return $this->match($this->preg_quote($what), $m, $eatWhitespace); 
  2550.  
  2551. // tree builders 
  2552.  
  2553. protected function pushBlock($selectors) { 
  2554. $b = new stdClass; 
  2555. $b->parent = $this->env; // not sure if we need this yet 
  2556.  
  2557. $b->selectors = $selectors; 
  2558. $b->children = array(); 
  2559.  
  2560. $this->env = $b; 
  2561. return $b; 
  2562.  
  2563. protected function pushSpecialBlock($type) { 
  2564. $block = $this->pushBlock(null); 
  2565. $block->type = $type; 
  2566. return $block; 
  2567.  
  2568. protected function popBlock() { 
  2569. if (empty($this->env->parent)) { 
  2570. $this->throwParseError("unexpected }"); 
  2571.  
  2572. $old = $this->env; 
  2573. $this->env = $this->env->parent; 
  2574. unset($old->parent); 
  2575. return $old; 
  2576.  
  2577. protected function append($statement, $pos=null) { 
  2578. if ($pos !== null) { 
  2579. $statement[-1] = $pos; 
  2580. if (!$this->rootParser) $statement[-2] = $this; 
  2581. $this->env->children[] = $statement; 
  2582.  
  2583. // last child that was appended 
  2584. protected function last() { 
  2585. $i = count($this->env->children) - 1; 
  2586. if (isset($this->env->children[$i])) 
  2587. return $this->env->children[$i]; 
  2588.  
  2589. // high level parsers (they return parts of ast) 
  2590.  
  2591. protected function mediaQueryList(&$out) { 
  2592. return $this->genericList($out, "mediaQuery", ", ", false); 
  2593.  
  2594. protected function mediaQuery(&$out) { 
  2595. $s = $this->seek(); 
  2596.  
  2597. $expressions = null; 
  2598. $parts = array(); 
  2599.  
  2600. if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->mixedKeyword($mediaType)) { 
  2601. $prop = array("mediaType"); 
  2602. if (isset($only)) $prop[] = array("keyword", "only"); 
  2603. if (isset($not)) $prop[] = array("keyword", "not"); 
  2604. $media = array("list", "", array()); 
  2605. foreach ((array)$mediaType as $type) { 
  2606. if (is_array($type)) { 
  2607. $media[2][] = $type; 
  2608. } else { 
  2609. $media[2][] = array("keyword", $type); 
  2610. $prop[] = $media; 
  2611. $parts[] = $prop; 
  2612.  
  2613. if (empty($parts) || $this->literal("and")) { 
  2614. $this->genericList($expressions, "mediaExpression", "and", false); 
  2615. if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]); 
  2616.  
  2617. $out = $parts; 
  2618. return true; 
  2619.  
  2620. protected function mediaExpression(&$out) { 
  2621. $s = $this->seek(); 
  2622. $value = null; 
  2623. if ($this->literal("(") && 
  2624. $this->expression($feature) && 
  2625. ($this->literal(":") && $this->expression($value) || true) && 
  2626. $this->literal(")")) 
  2627. $out = array("mediaExp", $feature); 
  2628. if ($value) $out[] = $value; 
  2629. return true; 
  2630.  
  2631. $this->seek($s); 
  2632. return false; 
  2633.  
  2634. protected function argValues(&$out) { 
  2635. if ($this->genericList($list, "argValue", ", ", false)) { 
  2636. $out = $list[2]; 
  2637. return true; 
  2638. return false; 
  2639.  
  2640. protected function argValue(&$out) { 
  2641. $s = $this->seek(); 
  2642.  
  2643. $keyword = null; 
  2644. if (!$this->variable($keyword) || !$this->literal(":")) { 
  2645. $this->seek($s); 
  2646. $keyword = null; 
  2647.  
  2648. if ($this->genericList($value, "expression")) { 
  2649. $out = array($keyword, $value, false); 
  2650. $s = $this->seek(); 
  2651. if ($this->literal("...")) { 
  2652. $out[2] = true; 
  2653. } else { 
  2654. $this->seek($s); 
  2655. return true; 
  2656.  
  2657. return false; 
  2658.  
  2659.  
  2660. protected function valueList(&$out) { 
  2661. return $this->genericList($out, "spaceList", ", "); 
  2662.  
  2663. protected function spaceList(&$out) { 
  2664. return $this->genericList($out, "expression"); 
  2665.  
  2666. protected function genericList(&$out, $parseItem, $delim="", $flatten=true) { 
  2667. $s = $this->seek(); 
  2668. $items = array(); 
  2669. while ($this->$parseItem($value)) { 
  2670. $items[] = $value; 
  2671. if ($delim) { 
  2672. if (!$this->literal($delim)) break; 
  2673.  
  2674. if (count($items) == 0) { 
  2675. $this->seek($s); 
  2676. return false; 
  2677.  
  2678. if ($flatten && count($items) == 1) { 
  2679. $out = $items[0]; 
  2680. } else { 
  2681. $out = array("list", $delim, $items); 
  2682.  
  2683. return true; 
  2684.  
  2685. protected function expression(&$out) { 
  2686. $s = $this->seek(); 
  2687.  
  2688. if ($this->literal("(")) { 
  2689. if ($this->literal(")")) { 
  2690. $out = array("list", "", array()); 
  2691. return true; 
  2692.  
  2693. if ($this->valueList($out) && $this->literal(')') && $out[0] == "list") { 
  2694. return true; 
  2695.  
  2696. $this->seek($s); 
  2697.  
  2698. if ($this->value($lhs)) { 
  2699. $out = $this->expHelper($lhs, 0); 
  2700. return true; 
  2701.  
  2702. return false; 
  2703.  
  2704. protected function expHelper($lhs, $minP) { 
  2705. $opstr = self::$operatorStr; 
  2706.  
  2707. $ss = $this->seek(); 
  2708. $whiteBefore = isset($this->buffer[$this->count - 1]) && 
  2709. ctype_space($this->buffer[$this->count - 1]); 
  2710. while ($this->match($opstr, $m) && self::$precedence[$m[1]] >= $minP) { 
  2711. $whiteAfter = isset($this->buffer[$this->count - 1]) && 
  2712. ctype_space($this->buffer[$this->count - 1]); 
  2713.  
  2714. $op = $m[1]; 
  2715.  
  2716. // don't turn negative numbers into expressions 
  2717. if ($op == "-" && $whiteBefore) { 
  2718. if (!$whiteAfter) break; 
  2719.  
  2720. if (!$this->value($rhs)) break; 
  2721.  
  2722. // peek and see if rhs belongs to next operator 
  2723. if ($this->peek($opstr, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) { 
  2724. $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]); 
  2725.  
  2726. $lhs = array("exp", $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter); 
  2727. $ss = $this->seek(); 
  2728. $whiteBefore = isset($this->buffer[$this->count - 1]) && 
  2729. ctype_space($this->buffer[$this->count - 1]); 
  2730.  
  2731. $this->seek($ss); 
  2732. return $lhs; 
  2733.  
  2734. protected function value(&$out) { 
  2735. $s = $this->seek(); 
  2736.  
  2737. if ($this->literal("not", false) && $this->whitespace() && $this->value($inner)) { 
  2738. $out = array("unary", "not", $inner, $this->inParens); 
  2739. return true; 
  2740. } else { 
  2741. $this->seek($s); 
  2742.  
  2743. if ($this->literal("+") && $this->value($inner)) { 
  2744. $out = array("unary", "+", $inner, $this->inParens); 
  2745. return true; 
  2746. } else { 
  2747. $this->seek($s); 
  2748.  
  2749. // negation 
  2750. if ($this->literal("-", false) && 
  2751. ($this->variable($inner) || 
  2752. $this->unit($inner) || 
  2753. $this->parenValue($inner))) 
  2754. $out = array("unary", "-", $inner, $this->inParens); 
  2755. return true; 
  2756. } else { 
  2757. $this->seek($s); 
  2758.  
  2759. if ($this->parenValue($out)) return true; 
  2760. if ($this->interpolation($out)) return true; 
  2761. if ($this->variable($out)) return true; 
  2762. if ($this->color($out)) return true; 
  2763. if ($this->unit($out)) return true; 
  2764. if ($this->string($out)) return true; 
  2765. if ($this->func($out)) return true; 
  2766. if ($this->progid($out)) return true; 
  2767.  
  2768. if ($this->keyword($keyword)) { 
  2769. if ($keyword == "null") { 
  2770. $out = array("null"); 
  2771. } else { 
  2772. $out = array("keyword", $keyword); 
  2773. return true; 
  2774.  
  2775. return false; 
  2776.  
  2777. // value wrappen in parentheses 
  2778. protected function parenValue(&$out) { 
  2779. $s = $this->seek(); 
  2780.  
  2781. $inParens = $this->inParens; 
  2782. if ($this->literal("(") && 
  2783. ($this->inParens = true) && $this->expression($exp) && 
  2784. $this->literal(")")) 
  2785. $out = $exp; 
  2786. $this->inParens = $inParens; 
  2787. return true; 
  2788. } else { 
  2789. $this->inParens = $inParens; 
  2790. $this->seek($s); 
  2791.  
  2792. return false; 
  2793.  
  2794. protected function progid(&$out) { 
  2795. $s = $this->seek(); 
  2796. if ($this->literal("progid:", false) && 
  2797. $this->openString("(", $fn) && 
  2798. $this->literal("(")) 
  2799. $this->openString(")", $args, "("); 
  2800. if ($this->literal(")")) { 
  2801. $out = array("string", "", array( 
  2802. "progid:", $fn, "(", $args, ")" 
  2803. )); 
  2804. return true; 
  2805.  
  2806. $this->seek($s); 
  2807. return false; 
  2808.  
  2809. protected function func(&$func) { 
  2810. $s = $this->seek(); 
  2811.  
  2812. if ($this->keyword($name, false) && 
  2813. $this->literal("(")) 
  2814. if ($name == "alpha" && $this->argumentList($args)) { 
  2815. $func = array("function", $name, array("string", "", $args)); 
  2816. return true; 
  2817.  
  2818. if ($name != "expression" && !preg_match("/^(-[a-z]+-)?calc$/", $name)) { 
  2819. $ss = $this->seek(); 
  2820. if ($this->argValues($args) && $this->literal(")")) { 
  2821. $func = array("fncall", $name, $args); 
  2822. return true; 
  2823. $this->seek($ss); 
  2824.  
  2825. if (($this->openString(")", $str, "(") || true ) && 
  2826. $this->literal(")")) 
  2827. $args = array(); 
  2828. if (!empty($str)) { 
  2829. $args[] = array(null, array("string", "", array($str))); 
  2830.  
  2831. $func = array("fncall", $name, $args); 
  2832. return true; 
  2833.  
  2834. $this->seek($s); 
  2835. return false; 
  2836.  
  2837. protected function argumentList(&$out) { 
  2838. $s = $this->seek(); 
  2839. $this->literal("("); 
  2840.  
  2841. $args = array(); 
  2842. while ($this->keyword($var)) { 
  2843. $ss = $this->seek(); 
  2844.  
  2845. if ($this->literal("=") && $this->expression($exp)) { 
  2846. $args[] = array("string", "", array($var."=")); 
  2847. $arg = $exp; 
  2848. } else { 
  2849. break; 
  2850.  
  2851. $args[] = $arg; 
  2852.  
  2853. if (!$this->literal(", ")) break; 
  2854.  
  2855. $args[] = array("string", "", array(", ")); 
  2856.  
  2857. if (!$this->literal(")") || !count($args)) { 
  2858. $this->seek($s); 
  2859. return false; 
  2860.  
  2861. $out = $args; 
  2862. return true; 
  2863.  
  2864. protected function argumentDef(&$out) { 
  2865. $s = $this->seek(); 
  2866. $this->literal("("); 
  2867.  
  2868. $args = array(); 
  2869. while ($this->variable($var)) { 
  2870. $arg = array($var[1], null, false); 
  2871.  
  2872. $ss = $this->seek(); 
  2873. if ($this->literal(":") && $this->genericList($defaultVal, "expression")) { 
  2874. $arg[1] = $defaultVal; 
  2875. } else { 
  2876. $this->seek($ss); 
  2877.  
  2878. $ss = $this->seek(); 
  2879. if ($this->literal("...")) { 
  2880. $sss = $this->seek(); 
  2881. if (!$this->literal(")")) { 
  2882. $this->throwParseError("... has to be after the final argument"); 
  2883. $arg[2] = true; 
  2884. $this->seek($sss); 
  2885. } else { 
  2886. $this->seek($ss); 
  2887.  
  2888. $args[] = $arg; 
  2889. if (!$this->literal(", ")) break; 
  2890.  
  2891. if (!$this->literal(")")) { 
  2892. $this->seek($s); 
  2893. return false; 
  2894.  
  2895. $out = $args; 
  2896. return true; 
  2897.  
  2898. protected function color(&$out) { 
  2899. $color = array('color'); 
  2900.  
  2901. if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) { 
  2902. if (isset($m[3])) { 
  2903. $num = $m[3]; 
  2904. $width = 16; 
  2905. } else { 
  2906. $num = $m[2]; 
  2907. $width = 256; 
  2908.  
  2909. $num = hexdec($num); 
  2910. foreach (array(3, 2, 1) as $i) { 
  2911. $t = $num % $width; 
  2912. $num /= $width; 
  2913.  
  2914. $color[$i] = $t * (256/$width) + $t * floor(16/$width); 
  2915.  
  2916. $out = $color; 
  2917. return true; 
  2918.  
  2919. return false; 
  2920.  
  2921. protected function unit(&$unit) { 
  2922. if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) { 
  2923. $unit = array("number", $m[1], empty($m[3]) ? "" : $m[3]); 
  2924. return true; 
  2925. return false; 
  2926.  
  2927. protected function string(&$out) { 
  2928. $s = $this->seek(); 
  2929. if ($this->literal('"', false)) { 
  2930. $delim = '"'; 
  2931. } elseif ($this->literal("'", false)) { 
  2932. $delim = "'"; 
  2933. } else { 
  2934. return false; 
  2935.  
  2936. $content = array(); 
  2937. $oldWhite = $this->eatWhiteDefault; 
  2938. $this->eatWhiteDefault = false; 
  2939.  
  2940. while ($this->matchString($m, $delim)) { 
  2941. $content[] = $m[1]; 
  2942. if ($m[2] == "#{") { 
  2943. $this->count -= strlen($m[2]); 
  2944. if ($this->interpolation($inter, false)) { 
  2945. $content[] = $inter; 
  2946. } else { 
  2947. $this->count += strlen($m[2]); 
  2948. $content[] = "#{"; // ignore it 
  2949. } elseif ($m[2] == '\\') { 
  2950. $content[] = $m[2]; 
  2951. if ($this->literal($delim, false)) { 
  2952. $content[] = $delim; 
  2953. } else { 
  2954. $this->count -= strlen($delim); 
  2955. break; // delim 
  2956.  
  2957. $this->eatWhiteDefault = $oldWhite; 
  2958.  
  2959. if ($this->literal($delim)) { 
  2960. $out = array("string", $delim, $content); 
  2961. return true; 
  2962.  
  2963. $this->seek($s); 
  2964. return false; 
  2965.  
  2966. protected function mixedKeyword(&$out) { 
  2967. $s = $this->seek(); 
  2968.  
  2969. $parts = array(); 
  2970.  
  2971. $oldWhite = $this->eatWhiteDefault; 
  2972. $this->eatWhiteDefault = false; 
  2973.  
  2974. while (true) { 
  2975. if ($this->keyword($key)) { 
  2976. $parts[] = $key; 
  2977. continue; 
  2978.  
  2979. if ($this->interpolation($inter)) { 
  2980. $parts[] = $inter; 
  2981. continue; 
  2982.  
  2983. break; 
  2984.  
  2985. $this->eatWhiteDefault = $oldWhite; 
  2986.  
  2987. if (count($parts) == 0) return false; 
  2988.  
  2989. if ($this->eatWhiteDefault) { 
  2990. $this->whitespace(); 
  2991.  
  2992. $out = $parts; 
  2993. return true; 
  2994.  
  2995. // an unbounded string stopped by $end 
  2996. protected function openString($end, &$out, $nestingOpen=null) { 
  2997. $oldWhite = $this->eatWhiteDefault; 
  2998. $this->eatWhiteDefault = false; 
  2999.  
  3000. $stop = array("'", '"', "#{", $end); 
  3001. $stop = array_map(array($this, "preg_quote"), $stop); 
  3002. $stop[] = self::$commentMulti; 
  3003.  
  3004. $patt = '(.*?)('.implode("|", $stop).')'; 
  3005.  
  3006. $nestingLevel = 0; 
  3007.  
  3008. $content = array(); 
  3009. while ($this->match($patt, $m, false)) { 
  3010. if (isset($m[1]) && $m[1] !== '') { 
  3011. $content[] = $m[1]; 
  3012. if ($nestingOpen) { 
  3013. $nestingLevel += substr_count($m[1], $nestingOpen); 
  3014.  
  3015. $tok = $m[2]; 
  3016.  
  3017. $this->count-= strlen($tok); 
  3018. if ($tok == $end) { 
  3019. if ($nestingLevel == 0) { 
  3020. break; 
  3021. } else { 
  3022. $nestingLevel--; 
  3023.  
  3024. if (($tok == "'" || $tok == '"') && $this->string($str)) { 
  3025. $content[] = $str; 
  3026. continue; 
  3027.  
  3028. if ($tok == "#{" && $this->interpolation($inter)) { 
  3029. $content[] = $inter; 
  3030. continue; 
  3031.  
  3032. $content[] = $tok; 
  3033. $this->count+= strlen($tok); 
  3034.  
  3035. $this->eatWhiteDefault = $oldWhite; 
  3036.  
  3037. if (count($content) == 0) return false; 
  3038.  
  3039. // trim the end 
  3040. if (is_string(end($content))) { 
  3041. $content[count($content) - 1] = rtrim(end($content)); 
  3042.  
  3043. $out = array("string", "", $content); 
  3044. return true; 
  3045.  
  3046. // $lookWhite: save information about whitespace before and after 
  3047. protected function interpolation(&$out, $lookWhite=true) { 
  3048. $oldWhite = $this->eatWhiteDefault; 
  3049. $this->eatWhiteDefault = true; 
  3050.  
  3051. $s = $this->seek(); 
  3052. if ($this->literal("#{") && $this->valueList($value) && $this->literal("}", false)) { 
  3053.  
  3054. // TODO: don't error if out of bounds 
  3055.  
  3056. if ($lookWhite) { 
  3057. $left = preg_match('/\s/', $this->buffer[$s - 1]) ? " " : ""; 
  3058. $right = preg_match('/\s/', $this->buffer[$this->count]) ? " ": ""; 
  3059. } else { 
  3060. $left = $right = false; 
  3061.  
  3062. $out = array("interpolate", $value, $left, $right); 
  3063. $this->eatWhiteDefault = $oldWhite; 
  3064. if ($this->eatWhiteDefault) $this->whitespace(); 
  3065. return true; 
  3066.  
  3067. $this->seek($s); 
  3068. $this->eatWhiteDefault = $oldWhite; 
  3069. return false; 
  3070.  
  3071. // low level parsers 
  3072.  
  3073. // returns an array of parts or a string 
  3074. protected function propertyName(&$out) { 
  3075. $s = $this->seek(); 
  3076. $parts = array(); 
  3077.  
  3078. $oldWhite = $this->eatWhiteDefault; 
  3079. $this->eatWhiteDefault = false; 
  3080.  
  3081. while (true) { 
  3082. if ($this->interpolation($inter)) { 
  3083. $parts[] = $inter; 
  3084. } elseif ($this->keyword($text)) { 
  3085. $parts[] = $text; 
  3086. } elseif (count($parts) == 0 && $this->match('[:.#]', $m, false)) { 
  3087. // css hacks 
  3088. $parts[] = $m[0]; 
  3089. } else { 
  3090. break; 
  3091.  
  3092. $this->eatWhiteDefault = $oldWhite; 
  3093. if (count($parts) == 0) return false; 
  3094.  
  3095. // match comment hack 
  3096. if (preg_match(self::$whitePattern,  
  3097. $this->buffer, $m, null, $this->count)) 
  3098. if (!empty($m[0])) { 
  3099. $parts[] = $m[0]; 
  3100. $this->count += strlen($m[0]); 
  3101.  
  3102. $this->whitespace(); // get any extra whitespace 
  3103.  
  3104. $out = array("string", "", $parts); 
  3105. return true; 
  3106.  
  3107. // comma separated list of selectors 
  3108. protected function selectors(&$out) { 
  3109. $s = $this->seek(); 
  3110. $selectors = array(); 
  3111. while ($this->selector($sel)) { 
  3112. $selectors[] = $sel; 
  3113. if (!$this->literal(", ")) break; 
  3114. while ($this->literal(", ")); // ignore extra 
  3115.  
  3116. if (count($selectors) == 0) { 
  3117. $this->seek($s); 
  3118. return false; 
  3119.  
  3120. $out = $selectors; 
  3121. return true; 
  3122.  
  3123. // whitespace separated list of selectorSingle 
  3124. protected function selector(&$out) { 
  3125. $selector = array(); 
  3126.  
  3127. while (true) { 
  3128. if ($this->match('[>+~]+', $m)) { 
  3129. $selector[] = array($m[0]); 
  3130. } elseif ($this->selectorSingle($part)) { 
  3131. $selector[] = $part; 
  3132. $this->whitespace(); 
  3133. } elseif ($this->match('\/[^\/]+\/', $m)) { 
  3134. $selector[] = array($m[0]); 
  3135. } else { 
  3136. break; 
  3137.  
  3138.  
  3139. if (count($selector) == 0) { 
  3140. return false; 
  3141.  
  3142. $out = $selector; 
  3143. return true; 
  3144.  
  3145. // the parts that make up 
  3146. // div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder 
  3147. protected function selectorSingle(&$out) { 
  3148. $oldWhite = $this->eatWhiteDefault; 
  3149. $this->eatWhiteDefault = false; 
  3150.  
  3151. $parts = array(); 
  3152.  
  3153. if ($this->literal("*", false)) { 
  3154. $parts[] = "*"; 
  3155.  
  3156. while (true) { 
  3157. // see if we can stop early 
  3158. if ($this->match("\s*[{, ]", $m)) { 
  3159. $this->count--; 
  3160. break; 
  3161.  
  3162. $s = $this->seek(); 
  3163. // self 
  3164. if ($this->literal("&", false)) { 
  3165. $parts[] = scssc::$selfSelector; 
  3166. continue; 
  3167.  
  3168. if ($this->literal(".", false)) { 
  3169. $parts[] = "."; 
  3170. continue; 
  3171.  
  3172. if ($this->literal("|", false)) { 
  3173. $parts[] = "|"; 
  3174. continue; 
  3175.  
  3176. // for keyframes 
  3177. if ($this->unit($unit)) { 
  3178. $parts[] = $unit; 
  3179. continue; 
  3180.  
  3181. if ($this->keyword($name)) { 
  3182. $parts[] = $name; 
  3183. continue; 
  3184.  
  3185. if ($this->interpolation($inter)) { 
  3186. $parts[] = $inter; 
  3187. continue; 
  3188.  
  3189. if ($this->literal('%', false) && $this->placeholder($placeholder)) { 
  3190. $parts[] = '%'; 
  3191. $parts[] = $placeholder; 
  3192. continue; 
  3193.  
  3194. if ($this->literal("#", false)) { 
  3195. $parts[] = "#"; 
  3196. continue; 
  3197.  
  3198. // a pseudo selector 
  3199. if ($this->match("::?", $m) && $this->mixedKeyword($nameParts)) { 
  3200. $parts[] = $m[0]; 
  3201. foreach ($nameParts as $sub) { 
  3202. $parts[] = $sub; 
  3203.  
  3204. $ss = $this->seek(); 
  3205. if ($this->literal("(") && 
  3206. ($this->openString(")", $str, "(") || true ) && 
  3207. $this->literal(")")) 
  3208. $parts[] = "("; 
  3209. if (!empty($str)) $parts[] = $str; 
  3210. $parts[] = ")"; 
  3211. } else { 
  3212. $this->seek($ss); 
  3213.  
  3214. continue; 
  3215. } else { 
  3216. $this->seek($s); 
  3217.  
  3218. // attribute selector 
  3219. // TODO: replace with open string? 
  3220. if ($this->literal("[", false)) { 
  3221. $attrParts = array("["); 
  3222. // keyword, string, operator 
  3223. while (true) { 
  3224. if ($this->literal("]", false)) { 
  3225. $this->count--; 
  3226. break; // get out early 
  3227.  
  3228. if ($this->match('\s+', $m)) { 
  3229. $attrParts[] = " "; 
  3230. continue; 
  3231. if ($this->string($str)) { 
  3232. $attrParts[] = $str; 
  3233. continue; 
  3234.  
  3235. if ($this->keyword($word)) { 
  3236. $attrParts[] = $word; 
  3237. continue; 
  3238.  
  3239. if ($this->interpolation($inter, false)) { 
  3240. $attrParts[] = $inter; 
  3241. continue; 
  3242.  
  3243. // operator, handles attr namespace too 
  3244. if ($this->match('[|-~\$\*\^=]+', $m)) { 
  3245. $attrParts[] = $m[0]; 
  3246. continue; 
  3247.  
  3248. break; 
  3249.  
  3250. if ($this->literal("]", false)) { 
  3251. $attrParts[] = "]"; 
  3252. foreach ($attrParts as $part) { 
  3253. $parts[] = $part; 
  3254. continue; 
  3255. $this->seek($s); 
  3256. // should just break here? 
  3257.  
  3258. break; 
  3259.  
  3260. $this->eatWhiteDefault = $oldWhite; 
  3261.  
  3262. if (count($parts) == 0) return false; 
  3263.  
  3264. $out = $parts; 
  3265. return true; 
  3266.  
  3267. protected function variable(&$out) { 
  3268. $s = $this->seek(); 
  3269. if ($this->literal("$", false) && $this->keyword($name)) { 
  3270. $out = array("var", $name); 
  3271. return true; 
  3272. $this->seek($s); 
  3273. return false; 
  3274.  
  3275. protected function keyword(&$word, $eatWhitespace = null) { 
  3276. if ($this->match('([\w_\-\*!"\'\\\\][\w\-_"\'\\\\]*)',  
  3277. $m, $eatWhitespace)) 
  3278. $word = $m[1]; 
  3279. return true; 
  3280. return false; 
  3281.  
  3282. protected function placeholder(&$placeholder) { 
  3283. if ($this->match('([\w\-_]+)', $m)) { 
  3284. $placeholder = $m[1]; 
  3285. return true; 
  3286. return false; 
  3287.  
  3288. // consume an end of statement delimiter 
  3289. protected function end() { 
  3290. if ($this->literal(';')) { 
  3291. return true; 
  3292. } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') { 
  3293. // if there is end of file or a closing block next then we don't need a ; 
  3294. return true; 
  3295. return false; 
  3296.  
  3297. // advance counter to next occurrence of $what 
  3298. // $until - don't include $what in advance 
  3299. // $allowNewline, if string, will be used as valid char set 
  3300. protected function to($what, &$out, $until = false, $allowNewline = false) { 
  3301. if (is_string($allowNewline)) { 
  3302. $validChars = $allowNewline; 
  3303. } else { 
  3304. $validChars = $allowNewline ? "." : "[^\n]"; 
  3305. if (!$this->match('('.$validChars.'*?)'.$this->preg_quote($what), $m, !$until)) return false; 
  3306. if ($until) $this->count -= strlen($what); // give back $what 
  3307. $out = $m[1]; 
  3308. return true; 
  3309.  
  3310. public function throwParseError($msg = "parse error", $count = null) { 
  3311. $count = is_null($count) ? $this->count : $count; 
  3312.  
  3313. $line = $this->getLineNo($count); 
  3314.  
  3315. if (!empty($this->sourceName)) { 
  3316. $loc = "$this->sourceName on line $line"; 
  3317. } else { 
  3318. $loc = "line: $line"; 
  3319.  
  3320. if ($this->peek("(.*?)(\n|$)", $m, $count)) { 
  3321. throw new Exception("$msg: failed at `$m[1]` $loc"); 
  3322. } else { 
  3323. throw new Exception("$msg: $loc"); 
  3324.  
  3325. public function getLineNo($pos) { 
  3326. return 1 + substr_count(substr($this->buffer, 0, $pos), "\n"); 
  3327.  
  3328. /** 
  3329. * Match string looking for either ending delim, escape, or string interpolation 
  3330. * 
  3331. * {@internal This is a workaround for preg_match's 250K string match limit. }} 
  3332. * 
  3333. * @param array $m Matches (passed by reference) 
  3334. * @param string $delim Delimeter 
  3335. * 
  3336. * @return boolean True if match; false otherwise 
  3337. */ 
  3338. protected function matchString(&$m, $delim) { 
  3339. $token = null; 
  3340.  
  3341. $end = strpos($this->buffer, "\n", $this->count); 
  3342. if ($end === false) { 
  3343. $end = strlen($this->buffer); 
  3344.  
  3345. // look for either ending delim, escape, or string interpolation 
  3346. foreach (array('#{', '\\', $delim) as $lookahead) { 
  3347. $pos = strpos($this->buffer, $lookahead, $this->count); 
  3348. if ($pos !== false && $pos < $end) { 
  3349. $end = $pos; 
  3350. $token = $lookahead; 
  3351.  
  3352. if (!isset($token)) { 
  3353. return false; 
  3354.  
  3355. $match = substr($this->buffer, $this->count, $end - $this->count); 
  3356. $m = array( 
  3357. $match . $token,  
  3358. $match,  
  3359. $token 
  3360. ); 
  3361. $this->count = $end + strlen($token); 
  3362.  
  3363. return true; 
  3364.  
  3365. // try to match something on head of buffer 
  3366. protected function match($regex, &$out, $eatWhitespace = null) { 
  3367. if (is_null($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault; 
  3368.  
  3369. $r = '/'.$regex.'/Ais'; 
  3370. if (preg_match($r, $this->buffer, $out, null, $this->count)) { 
  3371. $this->count += strlen($out[0]); 
  3372. if ($eatWhitespace) $this->whitespace(); 
  3373. return true; 
  3374. return false; 
  3375.  
  3376. // match some whitespace 
  3377. protected function whitespace() { 
  3378. $gotWhite = false; 
  3379. while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) { 
  3380. if ($this->insertComments) { 
  3381. if (isset($m[1]) && empty($this->commentsSeen[$this->count])) { 
  3382. $this->append(array("comment", $m[1])); 
  3383. $this->commentsSeen[$this->count] = true; 
  3384. $this->count += strlen($m[0]); 
  3385. $gotWhite = true; 
  3386. return $gotWhite; 
  3387.  
  3388. protected function peek($regex, &$out, $from=null) { 
  3389. if (is_null($from)) $from = $this->count; 
  3390.  
  3391. $r = '/'.$regex.'/Ais'; 
  3392. $result = preg_match($r, $this->buffer, $out, null, $from); 
  3393.  
  3394. return $result; 
  3395.  
  3396. protected function seek($where = null) { 
  3397. if ($where === null) return $this->count; 
  3398. else $this->count = $where; 
  3399. return true; 
  3400.  
  3401. static function preg_quote($what) { 
  3402. return preg_quote($what, '/'); 
  3403.  
  3404. protected function show() { 
  3405. if ($this->peek("(.*?)(\n|$)", $m, $this->count)) { 
  3406. return $m[1]; 
  3407. return ""; 
  3408.  
  3409. // turn list of length 1 into value type 
  3410. protected function flattenList($value) { 
  3411. if ($value[0] == "list" && count($value[2]) == 1) { 
  3412. return $this->flattenList($value[2][0]); 
  3413. return $value; 
  3414.  
  3415. /** 
  3416. * SCSS base formatter 
  3417. * 
  3418. * @author Leaf Corcoran <leafot@gmail.com> 
  3419. */ 
  3420. class scss_formatter { 
  3421. public $indentChar = " "; 
  3422.  
  3423. public $break = "\n"; 
  3424. public $open = " {"; 
  3425. public $close = "}"; 
  3426. public $tagSeparator = ", "; 
  3427. public $assignSeparator = ": "; 
  3428.  
  3429. public function __construct() { 
  3430. $this->indentLevel = 0; 
  3431.  
  3432. public function indentStr($n = 0) { 
  3433. return str_repeat($this->indentChar, max($this->indentLevel + $n, 0)); 
  3434.  
  3435. public function property($name, $value) { 
  3436. return $name . $this->assignSeparator . $value . ";"; 
  3437.  
  3438. protected function block($block) { 
  3439. if (empty($block->lines) && empty($block->children)) return; 
  3440.  
  3441. $inner = $pre = $this->indentStr(); 
  3442.  
  3443. if (!empty($block->selectors)) { 
  3444. echo $pre . 
  3445. implode($this->tagSeparator, $block->selectors) . 
  3446. $this->open . $this->break; 
  3447. $this->indentLevel++; 
  3448. $inner = $this->indentStr(); 
  3449.  
  3450. if (!empty($block->lines)) { 
  3451. $glue = $this->break.$inner; 
  3452. echo $inner . implode($glue, $block->lines); 
  3453. if (!empty($block->children)) { 
  3454. echo $this->break; 
  3455.  
  3456. foreach ($block->children as $child) { 
  3457. $this->block($child); 
  3458.  
  3459. if (!empty($block->selectors)) { 
  3460. $this->indentLevel--; 
  3461. if (empty($block->children)) echo $this->break; 
  3462. echo $pre . $this->close . $this->break; 
  3463.  
  3464. public function format($block) { 
  3465. ob_start(); 
  3466. $this->block($block); 
  3467. $out = ob_get_clean(); 
  3468.  
  3469. return $out; 
  3470.  
  3471. /** 
  3472. * SCSS nested formatter 
  3473. * 
  3474. * @author Leaf Corcoran <leafot@gmail.com> 
  3475. */ 
  3476. class scss_formatter_nested extends scss_formatter { 
  3477. public $close = " }"; 
  3478.  
  3479. // adjust the depths of all children, depth first 
  3480. public function adjustAllChildren($block) { 
  3481. // flatten empty nested blocks 
  3482. $children = array(); 
  3483. foreach ($block->children as $i => $child) { 
  3484. if (empty($child->lines) && empty($child->children)) { 
  3485. if (isset($block->children[$i + 1])) { 
  3486. $block->children[$i + 1]->depth = $child->depth; 
  3487. continue; 
  3488. $children[] = $child; 
  3489.  
  3490. $count = count($children); 
  3491. for ($i = 0; $i < $count; $i++) { 
  3492. $depth = $children[$i]->depth; 
  3493. $j = $i + 1; 
  3494. if (isset($children[$j]) && $depth < $children[$j]->depth) { 
  3495. $childDepth = $children[$j]->depth; 
  3496. for (; $j < $count; $j++) { 
  3497. if ($depth < $children[$j]->depth && $childDepth >= $children[$j]->depth) { 
  3498. $children[$j]->depth = $depth + 1; 
  3499.  
  3500. $block->children = $children; 
  3501.  
  3502. // make relative to parent 
  3503. foreach ($block->children as $child) { 
  3504. $this->adjustAllChildren($child); 
  3505. $child->depth = $child->depth - $block->depth; 
  3506.  
  3507. protected function block($block) { 
  3508. if ($block->type == "root") { 
  3509. $this->adjustAllChildren($block); 
  3510.  
  3511. $inner = $pre = $this->indentStr($block->depth - 1); 
  3512. if (!empty($block->selectors)) { 
  3513. echo $pre . 
  3514. implode($this->tagSeparator, $block->selectors) . 
  3515. $this->open . $this->break; 
  3516. $this->indentLevel++; 
  3517. $inner = $this->indentStr($block->depth - 1); 
  3518.  
  3519. if (!empty($block->lines)) { 
  3520. $glue = $this->break.$inner; 
  3521. echo $inner . implode($glue, $block->lines); 
  3522. if (!empty($block->children)) echo $this->break; 
  3523.  
  3524. foreach ($block->children as $i => $child) { 
  3525. // echo "*** block: ".$block->depth." child: ".$child->depth."\n"; 
  3526. $this->block($child); 
  3527. if ($i < count($block->children) - 1) { 
  3528. echo $this->break; 
  3529.  
  3530. if (isset($block->children[$i + 1])) { 
  3531. $next = $block->children[$i + 1]; 
  3532. if ($next->depth == max($block->depth, 1) && $child->depth >= $next->depth) { 
  3533. echo $this->break; 
  3534.  
  3535. if (!empty($block->selectors)) { 
  3536. $this->indentLevel--; 
  3537. echo $this->close; 
  3538.  
  3539. if ($block->type == "root") { 
  3540. echo $this->break; 
  3541.  
  3542. /** 
  3543. * SCSS compressed formatter 
  3544. * 
  3545. * @author Leaf Corcoran <leafot@gmail.com> 
  3546. */ 
  3547. class scss_formatter_compressed extends scss_formatter { 
  3548. public $open = "{"; 
  3549. public $tagSeparator = ", "; 
  3550. public $assignSeparator = ":"; 
  3551. public $break = ""; 
  3552.  
  3553. public function indentStr($n = 0) { 
  3554. return ""; 
  3555.  
  3556. /** 
  3557. * SCSS server 
  3558. * 
  3559. * @author Leaf Corcoran <leafot@gmail.com> 
  3560. */ 
  3561. class scss_server { 
  3562. /** 
  3563. * Join path components 
  3564. * 
  3565. * @param string $left Path component, left of the directory separator 
  3566. * @param string $right Path component, right of the directory separator 
  3567. * 
  3568. * @return string 
  3569. */ 
  3570. protected function join($left, $right) { 
  3571. return rtrim($left, '/\\') . DIRECTORY_SEPARATOR . ltrim($right, '/\\'); 
  3572.  
  3573. /** 
  3574. * Get name of requested .scss file 
  3575. * 
  3576. * @return string|null 
  3577. */ 
  3578. protected function inputName() { 
  3579. switch (true) { 
  3580. case isset($_GET['p']): 
  3581. return $_GET['p']; 
  3582. case isset($_SERVER['PATH_INFO']): 
  3583. return $_SERVER['PATH_INFO']; 
  3584. case isset($_SERVER['DOCUMENT_URI']): 
  3585. return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME'])); 
  3586.  
  3587. /** 
  3588. * Get path to requested .scss file 
  3589. * 
  3590. * @return string 
  3591. */ 
  3592. protected function findInput() { 
  3593. if (($input = $this->inputName()) 
  3594. && strpos($input, '..') === false 
  3595. && substr($input, -5) === '.scss' 
  3596. ) { 
  3597. $name = $this->join($this->dir, $input); 
  3598.  
  3599. if (is_file($name) && is_readable($name)) { 
  3600. return $name; 
  3601.  
  3602. return false; 
  3603.  
  3604. /** 
  3605. * Get path to cached .css file 
  3606. * 
  3607. * @return string 
  3608. */ 
  3609. protected function cacheName($fname) { 
  3610. return $this->join($this->cacheDir, md5($fname) . '.css'); 
  3611.  
  3612. /** 
  3613. * Get path to cached imports 
  3614. * 
  3615. * @return string 
  3616. */ 
  3617. protected function importsCacheName($out) { 
  3618. return $out . '.imports'; 
  3619.  
  3620. /** 
  3621. * Determine whether .scss file needs to be re-compiled. 
  3622. * 
  3623. * @param string $in Input path 
  3624. * @param string $out Output path 
  3625. * 
  3626. * @return boolean True if compile required. 
  3627. */ 
  3628. protected function needsCompile($in, $out) { 
  3629. if (!is_file($out)) return true; 
  3630.  
  3631. $mtime = filemtime($out); 
  3632. if (filemtime($in) > $mtime) return true; 
  3633.  
  3634. // look for modified imports 
  3635. $icache = $this->importsCacheName($out); 
  3636. if (is_readable($icache)) { 
  3637. $imports = unserialize(file_get_contents($icache)); 
  3638. foreach ($imports as $import) { 
  3639. if (filemtime($import) > $mtime) return true; 
  3640. return false; 
  3641.  
  3642. /** 
  3643. * Compile .scss file 
  3644. * 
  3645. * @param string $in Input path (.scss) 
  3646. * @param string $out Output path (.css) 
  3647. * 
  3648. * @return string 
  3649. */ 
  3650. protected function compile($in, $out) { 
  3651. $start = microtime(true); 
  3652. $css = $this->scss->compile(file_get_contents($in), $in); 
  3653. $elapsed = round((microtime(true) - $start), 4); 
  3654.  
  3655. $v = scssc::$VERSION; 
  3656. $t = date('r'); 
  3657. $css = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css; 
  3658.  
  3659. file_put_contents($out, $css); 
  3660. file_put_contents($this->importsCacheName($out),  
  3661. serialize($this->scss->getParsedFiles())); 
  3662. return $css; 
  3663.  
  3664. /** 
  3665. * Compile requested scss and serve css. Outputs HTTP response. 
  3666. * 
  3667. * @param string $salt Prefix a string to the filename for creating the cache name hash 
  3668. */ 
  3669. public function serve($salt = '') { 
  3670. if ($input = $this->findInput()) { 
  3671. $output = $this->cacheName($salt . $input); 
  3672. header('Content-type: text/css'); 
  3673.  
  3674. if ($this->needsCompile($input, $output)) { 
  3675. try { 
  3676. echo $this->compile($input, $output); 
  3677. } catch (Exception $e) { 
  3678. header('HTTP/1.1 500 Internal Server Error'); 
  3679. echo 'Parse error: ' . $e->getMessage() . "\n"; 
  3680. } else { 
  3681. header('X-SCSS-Cache: true'); 
  3682. echo file_get_contents($output); 
  3683.  
  3684. return; 
  3685.  
  3686. header('HTTP/1.0 404 Not Found'); 
  3687. header('Content-type: text'); 
  3688. $v = scssc::$VERSION; 
  3689. echo "/* INPUT NOT FOUND scss $v */\n"; 
  3690.  
  3691. /** 
  3692. * Constructor 
  3693. * 
  3694. * @param string $dir Root directory to .scss files 
  3695. * @param string $cacheDir Cache directory 
  3696. * @param \scssc|null $scss SCSS compiler instance 
  3697. */ 
  3698. public function __construct($dir, $cacheDir=null, $scss=null) { 
  3699. $this->dir = $dir; 
  3700.  
  3701. if (is_null($cacheDir)) { 
  3702. $cacheDir = $this->join($dir, 'scss_cache'); 
  3703.  
  3704. $this->cacheDir = $cacheDir; 
  3705. if (!is_dir($this->cacheDir)) mkdir($this->cacheDir, 0755, true); 
  3706.  
  3707. if (is_null($scss)) { 
  3708. $scss = new scssc(); 
  3709. $scss->setImportPaths($this->dir); 
  3710. $this->scss = $scss; 
  3711.  
  3712. /** 
  3713. * Helper method to serve compiled scss 
  3714. * 
  3715. * @param string $path Root path 
  3716. */ 
  3717. static public function serveFrom($path) { 
  3718. $server = new self($path); 
  3719. $server->serve(); 
.