diff options
Diffstat (limited to 'public/system/storage/vendor/scss.inc.php')
-rw-r--r-- | public/system/storage/vendor/scss.inc.php | 4574 |
1 files changed, 4574 insertions, 0 deletions
diff --git a/public/system/storage/vendor/scss.inc.php b/public/system/storage/vendor/scss.inc.php new file mode 100644 index 0000000..e60349c --- /dev/null +++ b/public/system/storage/vendor/scss.inc.php @@ -0,0 +1,4574 @@ +<?php +/** + * SCSS compiler written in PHP + * + * @copyright 2012-2013 Leaf Corcoran + * + * @license http://opensource.org/licenses/gpl-license GPL-3.0 + * @license http://opensource.org/licenses/MIT MIT + * + * @link http://leafo.net/scssphp + */ + +/** + * The scss compiler and parser. + * + * Converting SCSS to CSS is a three stage process. The incoming file is parsed + * by `scss_parser` into a syntax tree, then it is compiled into another tree + * representing the CSS structure by `scssc`. The CSS tree is fed into a + * formatter, like `scss_formatter` which then outputs CSS as a string. + * + * During the first compile, all values are *reduced*, which means that their + * types are brought to the lowest form before being dump as strings. This + * handles math equations, variable dereferences, and the like. + * + * The `parse` function of `scssc` is the entry point. + * + * In summary: + * + * The `scssc` class creates an instance of the parser, feeds it SCSS code, + * then transforms the resulting tree to a CSS tree. This class also holds the + * evaluation context, such as all available mixins and variables at any given + * time. + * + * The `scss_parser` class is only concerned with parsing its input. + * + * The `scss_formatter` takes a CSS tree, and dumps it to a formatted string, + * handling things like indentation. + */ + +/** + * SCSS compiler + * + * @author Leaf Corcoran <leafot@gmail.com> + */ +class scssc { + static public $VERSION = 'v0.0.12'; + + static protected $operatorNames = array( + '+' => "add", + '-' => "sub", + '*' => "mul", + '/' => "div", + '%' => "mod", + + '==' => "eq", + '!=' => "neq", + '<' => "lt", + '>' => "gt", + + '<=' => "lte", + '>=' => "gte", + ); + + static protected $namespaces = array( + "special" => "%", + "mixin" => "@", + "function" => "^", + ); + + static protected $unitTable = array( + "in" => array( + "in" => 1, + "pt" => 72, + "pc" => 6, + "cm" => 2.54, + "mm" => 25.4, + "px" => 96, + ) + ); + + static public $true = array("keyword", "true"); + static public $false = array("keyword", "false"); + static public $null = array("null"); + + static public $defaultValue = array("keyword", ""); + static public $selfSelector = array("self"); + + protected $importPaths = array(""); + protected $importCache = array(); + + protected $userFunctions = array(); + protected $registeredVars = array(); + + protected $numberPrecision = 5; + + protected $formatter = "scss_formatter_nested"; + + /** + * Compile scss + * + * @param string $code + * @param string $name + * + * @return string + */ + public function compile($code, $name = null) + { + $this->indentLevel = -1; + $this->commentsSeen = array(); + $this->extends = array(); + $this->extendsMap = array(); + $this->parsedFiles = array(); + $this->env = null; + $this->scope = null; + + $locale = setlocale(LC_NUMERIC, 0); + setlocale(LC_NUMERIC, "C"); + + $this->parser = new scss_parser($name); + + $tree = $this->parser->parse($code); + + $this->formatter = new $this->formatter(); + + $this->pushEnv($tree); + $this->injectVariables($this->registeredVars); + $this->compileRoot($tree); + $this->popEnv(); + + $out = $this->formatter->format($this->scope); + + setlocale(LC_NUMERIC, $locale); + + return $out; + } + + protected function isSelfExtend($target, $origin) { + foreach ($origin as $sel) { + if (in_array($target, $sel)) { + return true; + } + } + + return false; + } + + protected function pushExtends($target, $origin) { + if ($this->isSelfExtend($target, $origin)) { + return; + } + + $i = count($this->extends); + $this->extends[] = array($target, $origin); + + foreach ($target as $part) { + if (isset($this->extendsMap[$part])) { + $this->extendsMap[$part][] = $i; + } else { + $this->extendsMap[$part] = array($i); + } + } + } + + protected function makeOutputBlock($type, $selectors = null) { + $out = new stdClass; + $out->type = $type; + $out->lines = array(); + $out->children = array(); + $out->parent = $this->scope; + $out->selectors = $selectors; + $out->depth = $this->env->depth; + + return $out; + } + + protected function matchExtendsSingle($single, &$outOrigin) { + $counts = array(); + foreach ($single as $part) { + if (!is_string($part)) return false; // hmm + + if (isset($this->extendsMap[$part])) { + foreach ($this->extendsMap[$part] as $idx) { + $counts[$idx] = + isset($counts[$idx]) ? $counts[$idx] + 1 : 1; + } + } + } + + $outOrigin = array(); + $found = false; + + foreach ($counts as $idx => $count) { + list($target, $origin) = $this->extends[$idx]; + + // check count + if ($count != count($target)) continue; + + // check if target is subset of single + if (array_diff(array_intersect($single, $target), $target)) continue; + + $rem = array_diff($single, $target); + + foreach ($origin as $j => $new) { + // prevent infinite loop when target extends itself + foreach ($new as $new_selector) { + if (!array_diff($single, $new_selector)) { + continue 2; + } + } + + $origin[$j][count($origin[$j]) - 1] = $this->combineSelectorSingle(end($new), $rem); + } + + $outOrigin = array_merge($outOrigin, $origin); + + $found = true; + } + + return $found; + } + + protected function combineSelectorSingle($base, $other) { + $tag = null; + $out = array(); + + foreach (array($base, $other) as $single) { + foreach ($single as $part) { + if (preg_match('/^[^\[.#:]/', $part)) { + $tag = $part; + } else { + $out[] = $part; + } + } + } + + if ($tag) { + array_unshift($out, $tag); + } + + return $out; + } + + protected function matchExtends($selector, &$out, $from = 0, $initial=true) { + foreach ($selector as $i => $part) { + if ($i < $from) continue; + + if ($this->matchExtendsSingle($part, $origin)) { + $before = array_slice($selector, 0, $i); + $after = array_slice($selector, $i + 1); + + foreach ($origin as $new) { + $k = 0; + + // remove shared parts + if ($initial) { + foreach ($before as $k => $val) { + if (!isset($new[$k]) || $val != $new[$k]) { + break; + } + } + } + + $result = array_merge( + $before, + $k > 0 ? array_slice($new, $k) : $new, + $after); + + + if ($result == $selector) continue; + $out[] = $result; + + // recursively check for more matches + $this->matchExtends($result, $out, $i, false); + + // selector sequence merging + if (!empty($before) && count($new) > 1) { + $result2 = array_merge( + array_slice($new, 0, -1), + $k > 0 ? array_slice($before, $k) : $before, + array_slice($new, -1), + $after); + + $out[] = $result2; + } + } + } + } + } + + protected function flattenSelectors($block, $parentKey = null) { + if ($block->selectors) { + $selectors = array(); + foreach ($block->selectors as $s) { + $selectors[] = $s; + if (!is_array($s)) continue; + // check extends + if (!empty($this->extendsMap)) { + $this->matchExtends($s, $selectors); + } + } + + $block->selectors = array(); + $placeholderSelector = false; + foreach ($selectors as $selector) { + if ($this->hasSelectorPlaceholder($selector)) { + $placeholderSelector = true; + continue; + } + $block->selectors[] = $this->compileSelector($selector); + } + + if ($placeholderSelector && 0 == count($block->selectors) && null !== $parentKey) { + unset($block->parent->children[$parentKey]); + return; + } + } + + foreach ($block->children as $key => $child) { + $this->flattenSelectors($child, $key); + } + } + + protected function compileRoot($rootBlock) + { + $this->scope = $this->makeOutputBlock('root'); + + $this->compileChildren($rootBlock->children, $this->scope); + $this->flattenSelectors($this->scope); + } + + protected function compileMedia($media) { + $this->pushEnv($media); + + $mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env)); + + if (!empty($mediaQuery)) { + + $this->scope = $this->makeOutputBlock("media", array($mediaQuery)); + + $parentScope = $this->mediaParent($this->scope); + + $parentScope->children[] = $this->scope; + + // top level properties in a media cause it to be wrapped + $needsWrap = false; + foreach ($media->children as $child) { + $type = $child[0]; + if ($type !== 'block' && $type !== 'media' && $type !== 'directive') { + $needsWrap = true; + break; + } + } + + if ($needsWrap) { + $wrapped = (object)array( + "selectors" => array(), + "children" => $media->children + ); + $media->children = array(array("block", $wrapped)); + } + + $this->compileChildren($media->children, $this->scope); + + $this->scope = $this->scope->parent; + } + + $this->popEnv(); + } + + protected function mediaParent($scope) { + while (!empty($scope->parent)) { + if (!empty($scope->type) && $scope->type != "media") { + break; + } + $scope = $scope->parent; + } + + return $scope; + } + + // TODO refactor compileNestedBlock and compileMedia into same thing + protected function compileNestedBlock($block, $selectors) { + $this->pushEnv($block); + + $this->scope = $this->makeOutputBlock($block->type, $selectors); + $this->scope->parent->children[] = $this->scope; + $this->compileChildren($block->children, $this->scope); + + $this->scope = $this->scope->parent; + $this->popEnv(); + } + + /** + * Recursively compiles a block. + * + * A block is analogous to a CSS block in most cases. A single SCSS document + * is encapsulated in a block when parsed, but it does not have parent tags + * so all of its children appear on the root level when compiled. + * + * Blocks are made up of selectors and children. + * + * The children of a block are just all the blocks that are defined within. + * + * Compiling the block involves pushing a fresh environment on the stack, + * and iterating through the props, compiling each one. + * + * @see scss::compileChild() + * + * @param \StdClass $block + */ + protected function compileBlock($block) { + $env = $this->pushEnv($block); + + $env->selectors = + array_map(array($this, "evalSelector"), $block->selectors); + + $out = $this->makeOutputBlock(null, $this->multiplySelectors($env)); + $this->scope->children[] = $out; + $this->compileChildren($block->children, $out); + + $this->popEnv(); + } + + // joins together .classes and #ids + protected function flattenSelectorSingle($single) { + $joined = array(); + foreach ($single as $part) { + if (empty($joined) || + !is_string($part) || + preg_match('/[\[.:#%]/', $part)) + { + $joined[] = $part; + continue; + } + + if (is_array(end($joined))) { + $joined[] = $part; + } else { + $joined[count($joined) - 1] .= $part; + } + } + + return $joined; + } + + // replaces all the interpolates + protected function evalSelector($selector) { + return array_map(array($this, "evalSelectorPart"), $selector); + } + + protected function evalSelectorPart($piece) { + foreach ($piece as &$p) { + if (!is_array($p)) continue; + + switch ($p[0]) { + case "interpolate": + $p = $this->compileValue($p); + break; + case "string": + $p = $this->compileValue($p); + break; + } + } + + return $this->flattenSelectorSingle($piece); + } + + // compiles to string + // self(&) should have been replaced by now + protected function compileSelector($selector) { + if (!is_array($selector)) return $selector; // media and the like + + return implode(" ", array_map( + array($this, "compileSelectorPart"), $selector)); + } + + protected function compileSelectorPart($piece) { + foreach ($piece as &$p) { + if (!is_array($p)) continue; + + switch ($p[0]) { + case "self": + $p = "&"; + break; + default: + $p = $this->compileValue($p); + break; + } + } + + return implode($piece); + } + + protected function hasSelectorPlaceholder($selector) + { + if (!is_array($selector)) return false; + + foreach ($selector as $parts) { + foreach ($parts as $part) { + if ('%' == $part[0]) { + return true; + } + } + } + + return false; + } + + protected function compileChildren($stms, $out) { + foreach ($stms as $stm) { + $ret = $this->compileChild($stm, $out); + if (isset($ret)) return $ret; + } + } + + protected function compileMediaQuery($queryList) { + $out = "@media"; + $first = true; + foreach ($queryList as $query){ + $type = null; + $parts = array(); + foreach ($query as $q) { + switch ($q[0]) { + case "mediaType": + if ($type) { + $type = $this->mergeMediaTypes($type, array_map(array($this, "compileValue"), array_slice($q, 1))); + if (empty($type)) { // merge failed + return null; + } + } else { + $type = array_map(array($this, "compileValue"), array_slice($q, 1)); + } + break; + case "mediaExp": + if (isset($q[2])) { + $parts[] = "(". $this->compileValue($q[1]) . $this->formatter->assignSeparator . $this->compileValue($q[2]) . ")"; + } else { + $parts[] = "(" . $this->compileValue($q[1]) . ")"; + } + break; + } + } + if ($type) { + array_unshift($parts, implode(' ', array_filter($type))); + } + if (!empty($parts)) { + if ($first) { + $first = false; + $out .= " "; + } else { + $out .= $this->formatter->tagSeparator; + } + $out .= implode(" and ", $parts); + } + } + return $out; + } + + protected function mergeMediaTypes($type1, $type2) { + if (empty($type1)) { + return $type2; + } + if (empty($type2)) { + return $type1; + } + $m1 = ''; + $t1 = ''; + if (count($type1) > 1) { + $m1= strtolower($type1[0]); + $t1= strtolower($type1[1]); + } else { + $t1 = strtolower($type1[0]); + } + $m2 = ''; + $t2 = ''; + if (count($type2) > 1) { + $m2 = strtolower($type2[0]); + $t2 = strtolower($type2[1]); + } else { + $t2 = strtolower($type2[0]); + } + if (($m1 == 'not') ^ ($m2 == 'not')) { + if ($t1 == $t2) { + return null; + } + return array( + $m1 == 'not' ? $m2 : $m1, + $m1 == 'not' ? $t2 : $t1 + ); + } elseif ($m1 == 'not' && $m2 == 'not') { + # CSS has no way of representing "neither screen nor print" + if ($t1 != $t2) { + return null; + } + return array('not', $t1); + } elseif ($t1 != $t2) { + return null; + } else { // t1 == t2, neither m1 nor m2 are "not" + return array(empty($m1)? $m2 : $m1, $t1); + } + } + + // returns true if the value was something that could be imported + protected function compileImport($rawPath, $out) { + if ($rawPath[0] == "string") { + $path = $this->compileStringContent($rawPath); + if ($path = $this->findImport($path)) { + $this->importFile($path, $out); + return true; + } + return false; + } + if ($rawPath[0] == "list") { + // handle a list of strings + if (count($rawPath[2]) == 0) return false; + foreach ($rawPath[2] as $path) { + if ($path[0] != "string") return false; + } + + foreach ($rawPath[2] as $path) { + $this->compileImport($path, $out); + } + + return true; + } + + return false; + } + + // return a value to halt execution + protected function compileChild($child, $out) { + $this->sourcePos = isset($child[-1]) ? $child[-1] : -1; + $this->sourceParser = isset($child[-2]) ? $child[-2] : $this->parser; + + switch ($child[0]) { + case "import": + list(,$rawPath) = $child; + $rawPath = $this->reduce($rawPath); + if (!$this->compileImport($rawPath, $out)) { + $out->lines[] = "@import " . $this->compileValue($rawPath) . ";"; + } + break; + case "directive": + list(, $directive) = $child; + $s = "@" . $directive->name; + if (!empty($directive->value)) { + $s .= " " . $this->compileValue($directive->value); + } + $this->compileNestedBlock($directive, array($s)); + break; + case "media": + $this->compileMedia($child[1]); + break; + case "block": + $this->compileBlock($child[1]); + break; + case "charset": + $out->lines[] = "@charset ".$this->compileValue($child[1]).";"; + break; + case "assign": + list(,$name, $value) = $child; + if ($name[0] == "var") { + $isDefault = !empty($child[3]); + + if ($isDefault) { + $existingValue = $this->get($name[1], true); + $shouldSet = $existingValue === true || $existingValue == self::$null; + } + + if (!$isDefault || $shouldSet) { + $this->set($name[1], $this->reduce($value)); + } + break; + } + + // if the value reduces to null from something else then + // the property should be discarded + if ($value[0] != "null") { + $value = $this->reduce($value); + if ($value[0] == "null") { + break; + } + } + + $compiledValue = $this->compileValue($value); + $out->lines[] = $this->formatter->property( + $this->compileValue($name), + $compiledValue); + break; + case "comment": + $out->lines[] = $child[1]; + break; + case "mixin": + case "function": + list(,$block) = $child; + $this->set(self::$namespaces[$block->type] . $block->name, $block); + break; + case "extend": + list(, $selectors) = $child; + foreach ($selectors as $sel) { + // only use the first one + $sel = current($this->evalSelector($sel)); + $this->pushExtends($sel, $out->selectors); + } + break; + case "if": + list(, $if) = $child; + if ($this->isTruthy($this->reduce($if->cond, true))) { + return $this->compileChildren($if->children, $out); + } else { + foreach ($if->cases as $case) { + if ($case->type == "else" || + $case->type == "elseif" && $this->isTruthy($this->reduce($case->cond))) + { + return $this->compileChildren($case->children, $out); + } + } + } + break; + case "return": + return $this->reduce($child[1], true); + case "each": + list(,$each) = $child; + $list = $this->coerceList($this->reduce($each->list)); + foreach ($list[2] as $item) { + $this->pushEnv(); + $this->set($each->var, $item); + // TODO: allow return from here + $this->compileChildren($each->children, $out); + $this->popEnv(); + } + break; + case "while": + list(,$while) = $child; + while ($this->isTruthy($this->reduce($while->cond, true))) { + $ret = $this->compileChildren($while->children, $out); + if ($ret) return $ret; + } + break; + case "for": + list(,$for) = $child; + $start = $this->reduce($for->start, true); + $start = $start[1]; + $end = $this->reduce($for->end, true); + $end = $end[1]; + $d = $start < $end ? 1 : -1; + + while (true) { + if ((!$for->until && $start - $d == $end) || + ($for->until && $start == $end)) + { + break; + } + + $this->set($for->var, array("number", $start, "")); + $start += $d; + + $ret = $this->compileChildren($for->children, $out); + if ($ret) return $ret; + } + + break; + case "nestedprop": + list(,$prop) = $child; + $prefixed = array(); + $prefix = $this->compileValue($prop->prefix) . "-"; + foreach ($prop->children as $child) { + if ($child[0] == "assign") { + array_unshift($child[1][2], $prefix); + } + if ($child[0] == "nestedprop") { + array_unshift($child[1]->prefix[2], $prefix); + } + $prefixed[] = $child; + } + $this->compileChildren($prefixed, $out); + break; + case "include": // including a mixin + list(,$name, $argValues, $content) = $child; + $mixin = $this->get(self::$namespaces["mixin"] . $name, false); + if (!$mixin) { + $this->throwError("Undefined mixin $name"); + } + + $callingScope = $this->env; + + // push scope, apply args + $this->pushEnv(); + if ($this->env->depth > 0) { + $this->env->depth--; + } + + if (isset($content)) { + $content->scope = $callingScope; + $this->setRaw(self::$namespaces["special"] . "content", $content); + } + + if (isset($mixin->args)) { + $this->applyArguments($mixin->args, $argValues); + } + + foreach ($mixin->children as $child) { + $this->compileChild($child, $out); + } + + $this->popEnv(); + + break; + case "mixin_content": + $content = $this->get(self::$namespaces["special"] . "content"); + if (!isset($content)) { + $this->throwError("Expected @content inside of mixin"); + } + + $strongTypes = array('include', 'block', 'for', 'while'); + foreach ($content->children as $child) { + $this->storeEnv = (in_array($child[0], $strongTypes)) + ? null + : $content->scope; + + $this->compileChild($child, $out); + } + + unset($this->storeEnv); + break; + case "debug": + list(,$value, $pos) = $child; + $line = $this->parser->getLineNo($pos); + $value = $this->compileValue($this->reduce($value, true)); + fwrite(STDERR, "Line $line DEBUG: $value\n"); + break; + default: + $this->throwError("unknown child type: $child[0]"); + } + } + + protected function expToString($exp) { + list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp; + $content = array($this->reduce($left)); + if ($whiteLeft) $content[] = " "; + $content[] = $op; + if ($whiteRight) $content[] = " "; + $content[] = $this->reduce($right); + return array("string", "", $content); + } + + protected function isTruthy($value) { + return $value != self::$false && $value != self::$null; + } + + // should $value cause its operand to eval + protected function shouldEval($value) { + switch ($value[0]) { + case "exp": + if ($value[1] == "/") { + return $this->shouldEval($value[2], $value[3]); + } + case "var": + case "fncall": + return true; + } + return false; + } + + protected function reduce($value, $inExp = false) { + list($type) = $value; + switch ($type) { + case "exp": + list(, $op, $left, $right, $inParens) = $value; + $opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op; + + $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right); + + $left = $this->reduce($left, true); + $right = $this->reduce($right, true); + + // only do division in special cases + if ($opName == "div" && !$inParens && !$inExp) { + if ($left[0] != "color" && $right[0] != "color") { + return $this->expToString($value); + } + } + + $left = $this->coerceForExpression($left); + $right = $this->coerceForExpression($right); + + $ltype = $left[0]; + $rtype = $right[0]; + + // this tries: + // 1. op_[op name]_[left type]_[right type] + // 2. op_[left type]_[right type] (passing the op as first arg + // 3. op_[op name] + $fn = "op_${opName}_${ltype}_${rtype}"; + if (is_callable(array($this, $fn)) || + (($fn = "op_${ltype}_${rtype}") && + is_callable(array($this, $fn)) && + $passOp = true) || + (($fn = "op_${opName}") && + is_callable(array($this, $fn)) && + $genOp = true)) + { + $unitChange = false; + if (!isset($genOp) && + $left[0] == "number" && $right[0] == "number") + { + if ($opName == "mod" && $right[2] != "") { + $this->throwError("Cannot modulo by a number with units: $right[1]$right[2]."); + } + + $unitChange = true; + $emptyUnit = $left[2] == "" || $right[2] == ""; + $targetUnit = "" != $left[2] ? $left[2] : $right[2]; + + if ($opName != "mul") { + $left[2] = "" != $left[2] ? $left[2] : $targetUnit; + $right[2] = "" != $right[2] ? $right[2] : $targetUnit; + } + + if ($opName != "mod") { + $left = $this->normalizeNumber($left); + $right = $this->normalizeNumber($right); + } + + if ($opName == "div" && !$emptyUnit && $left[2] == $right[2]) { + $targetUnit = ""; + } + + if ($opName == "mul") { + $left[2] = "" != $left[2] ? $left[2] : $right[2]; + $right[2] = "" != $right[2] ? $right[2] : $left[2]; + } elseif ($opName == "div" && $left[2] == $right[2]) { + $left[2] = ""; + $right[2] = ""; + } + } + + $shouldEval = $inParens || $inExp; + if (isset($passOp)) { + $out = $this->$fn($op, $left, $right, $shouldEval); + } else { + $out = $this->$fn($left, $right, $shouldEval); + } + + if (isset($out)) { + if ($unitChange && $out[0] == "number") { + $out = $this->coerceUnit($out, $targetUnit); + } + return $out; + } + } + + return $this->expToString($value); + case "unary": + list(, $op, $exp, $inParens) = $value; + $inExp = $inExp || $this->shouldEval($exp); + + $exp = $this->reduce($exp); + if ($exp[0] == "number") { + switch ($op) { + case "+": + return $exp; + case "-": + $exp[1] *= -1; + return $exp; + } + } + + if ($op == "not") { + if ($inExp || $inParens) { + if ($exp == self::$false) { + return self::$true; + } else { + return self::$false; + } + } else { + $op = $op . " "; + } + } + + return array("string", "", array($op, $exp)); + case "var": + list(, $name) = $value; + return $this->reduce($this->get($name)); + case "list": + foreach ($value[2] as &$item) { + $item = $this->reduce($item); + } + return $value; + case "string": + foreach ($value[2] as &$item) { + if (is_array($item)) { + $item = $this->reduce($item); + } + } + return $value; + case "interpolate": + $value[1] = $this->reduce($value[1]); + return $value; + case "fncall": + list(,$name, $argValues) = $value; + + // user defined function? + $func = $this->get(self::$namespaces["function"] . $name, false); + if ($func) { + $this->pushEnv(); + + // set the args + if (isset($func->args)) { + $this->applyArguments($func->args, $argValues); + } + + // throw away lines and children + $tmp = (object)array( + "lines" => array(), + "children" => array() + ); + $ret = $this->compileChildren($func->children, $tmp); + $this->popEnv(); + + return !isset($ret) ? self::$defaultValue : $ret; + } + + // built in function + if ($this->callBuiltin($name, $argValues, $returnValue)) { + return $returnValue; + } + + // need to flatten the arguments into a list + $listArgs = array(); + foreach ((array)$argValues as $arg) { + if (empty($arg[0])) { + $listArgs[] = $this->reduce($arg[1]); + } + } + return array("function", $name, array("list", ",", $listArgs)); + default: + return $value; + } + } + + public function normalizeValue($value) { + $value = $this->coerceForExpression($this->reduce($value)); + list($type) = $value; + + switch ($type) { + case "list": + $value = $this->extractInterpolation($value); + if ($value[0] != "list") { + return array("keyword", $this->compileValue($value)); + } + foreach ($value[2] as $key => $item) { + $value[2][$key] = $this->normalizeValue($item); + } + return $value; + case "number": + return $this->normalizeNumber($value); + default: + return $value; + } + } + + // just does physical lengths for now + protected function normalizeNumber($number) { + list(, $value, $unit) = $number; + if (isset(self::$unitTable["in"][$unit])) { + $conv = self::$unitTable["in"][$unit]; + return array("number", $value / $conv, "in"); + } + return $number; + } + + // $number should be normalized + protected function coerceUnit($number, $unit) { + list(, $value, $baseUnit) = $number; + if (isset(self::$unitTable[$baseUnit][$unit])) { + $value = $value * self::$unitTable[$baseUnit][$unit]; + } + + return array("number", $value, $unit); + } + + protected function op_add_number_number($left, $right) { + return array("number", $left[1] + $right[1], $left[2]); + } + + protected function op_mul_number_number($left, $right) { + return array("number", $left[1] * $right[1], $left[2]); + } + + protected function op_sub_number_number($left, $right) { + return array("number", $left[1] - $right[1], $left[2]); + } + + protected function op_div_number_number($left, $right) { + return array("number", $left[1] / $right[1], $left[2]); + } + + protected function op_mod_number_number($left, $right) { + return array("number", $left[1] % $right[1], $left[2]); + } + + // adding strings + protected function op_add($left, $right) { + if ($strLeft = $this->coerceString($left)) { + if ($right[0] == "string") { + $right[1] = ""; + } + $strLeft[2][] = $right; + return $strLeft; + } + + if ($strRight = $this->coerceString($right)) { + if ($left[0] == "string") { + $left[1] = ""; + } + array_unshift($strRight[2], $left); + return $strRight; + } + } + + protected function op_and($left, $right, $shouldEval) { + if (!$shouldEval) return; + if ($left != self::$false) return $right; + return $left; + } + + protected function op_or($left, $right, $shouldEval) { + if (!$shouldEval) return; + if ($left != self::$false) return $left; + return $right; + } + + protected function op_color_color($op, $left, $right) { + $out = array('color'); + foreach (range(1, 3) as $i) { + $lval = isset($left[$i]) ? $left[$i] : 0; + $rval = isset($right[$i]) ? $right[$i] : 0; + switch ($op) { + case '+': + $out[] = $lval + $rval; + break; + case '-': + $out[] = $lval - $rval; + break; + case '*': + $out[] = $lval * $rval; + break; + case '%': + $out[] = $lval % $rval; + break; + case '/': + if ($rval == 0) { + $this->throwError("color: Can't divide by zero"); + } + $out[] = $lval / $rval; + break; + case "==": + return $this->op_eq($left, $right); + case "!=": + return $this->op_neq($left, $right); + default: + $this->throwError("color: unknown op $op"); + } + } + + if (isset($left[4])) $out[4] = $left[4]; + elseif (isset($right[4])) $out[4] = $right[4]; + + return $this->fixColor($out); + } + + protected function op_color_number($op, $left, $right) { + $value = $right[1]; + return $this->op_color_color($op, $left, + array("color", $value, $value, $value)); + } + + protected function op_number_color($op, $left, $right) { + $value = $left[1]; + return $this->op_color_color($op, + array("color", $value, $value, $value), $right); + } + + protected function op_eq($left, $right) { + if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) { + $lStr[1] = ""; + $rStr[1] = ""; + return $this->toBool($this->compileValue($lStr) == $this->compileValue($rStr)); + } + + return $this->toBool($left == $right); + } + + protected function op_neq($left, $right) { + return $this->toBool($left != $right); + } + + protected function op_gte_number_number($left, $right) { + return $this->toBool($left[1] >= $right[1]); + } + + protected function op_gt_number_number($left, $right) { + return $this->toBool($left[1] > $right[1]); + } + + protected function op_lte_number_number($left, $right) { + return $this->toBool($left[1] <= $right[1]); + } + + protected function op_lt_number_number($left, $right) { + return $this->toBool($left[1] < $right[1]); + } + + public function toBool($thing) { + return $thing ? self::$true : self::$false; + } + + /** + * Compiles a primitive value into a CSS property value. + * + * Values in scssphp are typed by being wrapped in arrays, their format is + * typically: + * + * array(type, contents [, additional_contents]*) + * + * The input is expected to be reduced. This function will not work on + * things like expressions and variables. + * + * @param array $value + */ + protected function compileValue($value) { + $value = $this->reduce($value); + + list($type) = $value; + switch ($type) { + case "keyword": + return $value[1]; + case "color": + // [1] - red component (either number for a %) + // [2] - green component + // [3] - blue component + // [4] - optional alpha component + list(, $r, $g, $b) = $value; + + $r = round($r); + $g = round($g); + $b = round($b); + + if (count($value) == 5 && $value[4] != 1) { // rgba + return 'rgba('.$r.', '.$g.', '.$b.', '.$value[4].')'; + } + + $h = sprintf("#%02x%02x%02x", $r, $g, $b); + + // Converting hex color to short notation (e.g. #003399 to #039) + if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) { + $h = '#' . $h[1] . $h[3] . $h[5]; + } + + return $h; + case "number": + return round($value[1], $this->numberPrecision) . $value[2]; + case "string": + return $value[1] . $this->compileStringContent($value) . $value[1]; + case "function": + $args = !empty($value[2]) ? $this->compileValue($value[2]) : ""; + return "$value[1]($args)"; + case "list": + $value = $this->extractInterpolation($value); + if ($value[0] != "list") return $this->compileValue($value); + + list(, $delim, $items) = $value; + + $filtered = array(); + foreach ($items as $item) { + if ($item[0] == "null") continue; + $filtered[] = $this->compileValue($item); + } + + return implode("$delim ", $filtered); + case "interpolated": # node created by extractInterpolation + list(, $interpolate, $left, $right) = $value; + list(,, $whiteLeft, $whiteRight) = $interpolate; + + $left = count($left[2]) > 0 ? + $this->compileValue($left).$whiteLeft : ""; + + $right = count($right[2]) > 0 ? + $whiteRight.$this->compileValue($right) : ""; + + return $left.$this->compileValue($interpolate).$right; + + case "interpolate": # raw parse node + list(, $exp) = $value; + + // strip quotes if it's a string + $reduced = $this->reduce($exp); + switch ($reduced[0]) { + case "string": + $reduced = array("keyword", + $this->compileStringContent($reduced)); + break; + case "null": + $reduced = array("keyword", ""); + } + + return $this->compileValue($reduced); + case "null": + return "null"; + default: + $this->throwError("unknown value type: $type"); + } + } + + protected function compileStringContent($string) { + $parts = array(); + foreach ($string[2] as $part) { + if (is_array($part)) { + $parts[] = $this->compileValue($part); + } else { + $parts[] = $part; + } + } + + return implode($parts); + } + + // doesn't need to be recursive, compileValue will handle that + protected function extractInterpolation($list) { + $items = $list[2]; + foreach ($items as $i => $item) { + if ($item[0] == "interpolate") { + $before = array("list", $list[1], array_slice($items, 0, $i)); + $after = array("list", $list[1], array_slice($items, $i + 1)); + return array("interpolated", $item, $before, $after); + } + } + return $list; + } + + // find the final set of selectors + protected function multiplySelectors($env) { + $envs = array(); + while (null !== $env) { + if (!empty($env->selectors)) { + $envs[] = $env; + } + $env = $env->parent; + }; + + $selectors = array(); + $parentSelectors = array(array()); + while ($env = array_pop($envs)) { + $selectors = array(); + foreach ($env->selectors as $selector) { + foreach ($parentSelectors as $parent) { + $selectors[] = $this->joinSelectors($parent, $selector); + } + } + $parentSelectors = $selectors; + } + + return $selectors; + } + + // looks for & to replace, or append parent before child + protected function joinSelectors($parent, $child) { + $setSelf = false; + $out = array(); + foreach ($child as $part) { + $newPart = array(); + foreach ($part as $p) { + if ($p == self::$selfSelector) { + $setSelf = true; + foreach ($parent as $i => $parentPart) { + if ($i > 0) { + $out[] = $newPart; + $newPart = array(); + } + + foreach ($parentPart as $pp) { + $newPart[] = $pp; + } + } + } else { + $newPart[] = $p; + } + } + + $out[] = $newPart; + } + + return $setSelf ? $out : array_merge($parent, $child); + } + + protected function multiplyMedia($env, $childQueries = null) { + if (!isset($env) || + !empty($env->block->type) && $env->block->type != "media") + { + return $childQueries; + } + + // plain old block, skip + if (empty($env->block->type)) { + return $this->multiplyMedia($env->parent, $childQueries); + } + + $parentQueries = $env->block->queryList; + if ($childQueries == null) { + $childQueries = $parentQueries; + } else { + $originalQueries = $childQueries; + $childQueries = array(); + + foreach ($parentQueries as $parentQuery){ + foreach ($originalQueries as $childQuery) { + $childQueries []= array_merge($parentQuery, $childQuery); + } + } + } + + return $this->multiplyMedia($env->parent, $childQueries); + } + + // convert something to list + protected function coerceList($item, $delim = ",") { + if (isset($item) && $item[0] == "list") { + return $item; + } + + return array("list", $delim, !isset($item) ? array(): array($item)); + } + + protected function applyArguments($argDef, $argValues) { + $hasVariable = false; + $args = array(); + foreach ($argDef as $i => $arg) { + list($name, $default, $isVariable) = $argDef[$i]; + $args[$name] = array($i, $name, $default, $isVariable); + $hasVariable |= $isVariable; + } + + $keywordArgs = array(); + $deferredKeywordArgs = array(); + $remaining = array(); + // assign the keyword args + foreach ((array) $argValues as $arg) { + if (!empty($arg[0])) { + if (!isset($args[$arg[0][1]])) { + if ($hasVariable) { + $deferredKeywordArgs[$arg[0][1]] = $arg[1]; + } else { + $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]); + } + } elseif ($args[$arg[0][1]][0] < count($remaining)) { + $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]); + } else { + $keywordArgs[$arg[0][1]] = $arg[1]; + } + } elseif (count($keywordArgs)) { + $this->throwError('Positional arguments must come before keyword arguments.'); + } elseif ($arg[2] == true) { + $val = $this->reduce($arg[1], true); + if ($val[0] == "list") { + foreach ($val[2] as $name => $item) { + if (!is_numeric($name)) { + $keywordArgs[$name] = $item; + } else { + $remaining[] = $item; + } + } + } else { + $remaining[] = $val; + } + } else { + $remaining[] = $arg[1]; + } + } + + foreach ($args as $arg) { + list($i, $name, $default, $isVariable) = $arg; + if ($isVariable) { + $val = array("list", ",", array()); + for ($count = count($remaining); $i < $count; $i++) { + $val[2][] = $remaining[$i]; + } + foreach ($deferredKeywordArgs as $itemName => $item) { + $val[2][$itemName] = $item; + } + } elseif (isset($remaining[$i])) { + $val = $remaining[$i]; + } elseif (isset($keywordArgs[$name])) { + $val = $keywordArgs[$name]; + } elseif (!empty($default)) { + $val = $default; + } else { + $this->throwError("Missing argument $name"); + } + + $this->set($name, $this->reduce($val, true), true); + } + } + + protected function pushEnv($block=null) { + $env = new stdClass; + $env->parent = $this->env; + $env->store = array(); + $env->block = $block; + $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0; + + $this->env = $env; + return $env; + } + + protected function normalizeName($name) { + return str_replace("-", "_", $name); + } + + protected function getStoreEnv() { + return isset($this->storeEnv) ? $this->storeEnv : $this->env; + } + + protected function set($name, $value, $shadow=false) { + $name = $this->normalizeName($name); + + if ($shadow) { + $this->setRaw($name, $value); + } else { + $this->setExisting($name, $value); + } + } + + protected function setExisting($name, $value, $env = null) { + if (!isset($env)) $env = $this->getStoreEnv(); + + if (isset($env->store[$name]) || !isset($env->parent)) { + $env->store[$name] = $value; + } else { + $this->setExisting($name, $value, $env->parent); + } + } + + protected function setRaw($name, $value) { + $env = $this->getStoreEnv(); + $env->store[$name] = $value; + } + + public function get($name, $defaultValue = null, $env = null) { + $name = $this->normalizeName($name); + + if (!isset($env)) $env = $this->getStoreEnv(); + if (!isset($defaultValue)) $defaultValue = self::$defaultValue; + + if (isset($env->store[$name])) { + return $env->store[$name]; + } elseif (isset($env->parent)) { + return $this->get($name, $defaultValue, $env->parent); + } + + return $defaultValue; // found nothing + } + + protected function injectVariables(array $args) + { + if (empty($args)) { + return; + } + + $parser = new scss_parser(__METHOD__, false); + + foreach ($args as $name => $strValue) { + if ($name[0] === '$') { + $name = substr($name, 1); + } + + $parser->env = null; + $parser->count = 0; + $parser->buffer = (string) $strValue; + $parser->inParens = false; + $parser->eatWhiteDefault = true; + $parser->insertComments = true; + + if ( ! $parser->valueList($value)) { + throw new Exception("failed to parse passed in variable $name: $strValue"); + } + + $this->set($name, $value); + } + } + + /** + * Set variables + * + * @param array $variables + */ + public function setVariables(array $variables) + { + $this->registeredVars = array_merge($this->registeredVars, $variables); + } + + /** + * Unset variable + * + * @param string $name + */ + public function unsetVariable($name) + { + unset($this->registeredVars[$name]); + } + + protected function popEnv() { + $env = $this->env; + $this->env = $this->env->parent; + return $env; + } + + public function getParsedFiles() { + return $this->parsedFiles; + } + + public function addImportPath($path) { + $this->importPaths[] = $path; + } + + public function setImportPaths($path) { + $this->importPaths = (array)$path; + } + + public function setNumberPrecision($numberPrecision) { + $this->numberPrecision = $numberPrecision; + } + + public function setFormatter($formatterName) { + $this->formatter = $formatterName; + } + + public function registerFunction($name, $func) { + $this->userFunctions[$this->normalizeName($name)] = $func; + } + + public function unregisterFunction($name) { + unset($this->userFunctions[$this->normalizeName($name)]); + } + + protected function importFile($path, $out) { + // see if tree is cached + $realPath = realpath($path); + if (isset($this->importCache[$realPath])) { + $tree = $this->importCache[$realPath]; + } else { + $code = file_get_contents($path); + $parser = new scss_parser($path, false); + $tree = $parser->parse($code); + $this->parsedFiles[] = $path; + + $this->importCache[$realPath] = $tree; + } + + $pi = pathinfo($path); + array_unshift($this->importPaths, $pi['dirname']); + $this->compileChildren($tree->children, $out); + array_shift($this->importPaths); + } + + // results the file path for an import url if it exists + public function findImport($url) { + $urls = array(); + + // for "normal" scss imports (ignore vanilla css and external requests) + if (!preg_match('/\.css|^http:\/\/$/', $url)) { + // try both normal and the _partial filename + $urls = array($url, preg_replace('/[^\/]+$/', '_\0', $url)); + } + + foreach ($this->importPaths as $dir) { + if (is_string($dir)) { + // check urls for normal import paths + foreach ($urls as $full) { + $full = $dir . + (!empty($dir) && substr($dir, -1) != '/' ? '/' : '') . + $full; + + if ($this->fileExists($file = $full.'.scss') || + $this->fileExists($file = $full)) + { + return $file; + } + } + } else { + // check custom callback for import path + $file = call_user_func($dir,$url,$this); + if ($file !== null) { + return $file; + } + } + } + + return null; + } + + protected function fileExists($name) { + return is_file($name); + } + + protected function callBuiltin($name, $args, &$returnValue) { + // try a lib function + $name = $this->normalizeName($name); + $libName = "lib_".$name; + $f = array($this, $libName); + if (is_callable($f)) { + $prototype = isset(self::$$libName) ? self::$$libName : null; + $sorted = $this->sortArgs($prototype, $args); + foreach ($sorted as &$val) { + $val = $this->reduce($val, true); + } + $returnValue = call_user_func($f, $sorted, $this); + } elseif (isset($this->userFunctions[$name])) { + // see if we can find a user function + $fn = $this->userFunctions[$name]; + + foreach ($args as &$val) { + $val = $this->reduce($val[1], true); + } + + $returnValue = call_user_func($fn, $args, $this); + } + + if (isset($returnValue)) { + // coerce a php value into a scss one + if (is_numeric($returnValue)) { + $returnValue = array('number', $returnValue, ""); + } elseif (is_bool($returnValue)) { + $returnValue = $returnValue ? self::$true : self::$false; + } elseif (!is_array($returnValue)) { + $returnValue = array('keyword', $returnValue); + } + + return true; + } + + return false; + } + + // sorts any keyword arguments + // TODO: merge with apply arguments + protected function sortArgs($prototype, $args) { + $keyArgs = array(); + $posArgs = array(); + + foreach ($args as $arg) { + list($key, $value) = $arg; + $key = $key[1]; + if (empty($key)) { + $posArgs[] = $value; + } else { + $keyArgs[$key] = $value; + } + } + + if (!isset($prototype)) return $posArgs; + + $finalArgs = array(); + foreach ($prototype as $i => $names) { + if (isset($posArgs[$i])) { + $finalArgs[] = $posArgs[$i]; + continue; + } + + $set = false; + foreach ((array)$names as $name) { + if (isset($keyArgs[$name])) { + $finalArgs[] = $keyArgs[$name]; + $set = true; + break; + } + } + + if (!$set) { + $finalArgs[] = null; + } + } + + return $finalArgs; + } + + protected function coerceForExpression($value) { + if ($color = $this->coerceColor($value)) { + return $color; + } + + return $value; + } + + protected function coerceColor($value) { + switch ($value[0]) { + case "color": return $value; + case "keyword": + $name = $value[1]; + if (isset(self::$cssColors[$name])) { + $rgba = explode(',', self::$cssColors[$name]); + return isset($rgba[3]) + ? array('color', (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3]) + : array('color', (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]); + } + return null; + } + + return null; + } + + protected function coerceString($value) { + switch ($value[0]) { + case "string": + return $value; + case "keyword": + return array("string", "", array($value[1])); + } + return null; + } + + public function assertList($value) { + if ($value[0] != "list") + $this->throwError("expecting list"); + return $value; + } + + public function assertColor($value) { + if ($color = $this->coerceColor($value)) return $color; + $this->throwError("expecting color"); + } + + public function assertNumber($value) { + if ($value[0] != "number") + $this->throwError("expecting number"); + return $value[1]; + } + + protected function coercePercent($value) { + if ($value[0] == "number") { + if ($value[2] == "%") { + return $value[1] / 100; + } + return $value[1]; + } + return 0; + } + + // make sure a color's components don't go out of bounds + protected function fixColor($c) { + foreach (range(1, 3) as $i) { + if ($c[$i] < 0) $c[$i] = 0; + if ($c[$i] > 255) $c[$i] = 255; + } + + return $c; + } + + public function toHSL($red, $green, $blue) { + $min = min($red, $green, $blue); + $max = max($red, $green, $blue); + + $l = $min + $max; + + if ($min == $max) { + $s = $h = 0; + } else { + $d = $max - $min; + + if ($l < 255) + $s = $d / $l; + else + $s = $d / (510 - $l); + + if ($red == $max) + $h = 60 * ($green - $blue) / $d; + elseif ($green == $max) + $h = 60 * ($blue - $red) / $d + 120; + elseif ($blue == $max) + $h = 60 * ($red - $green) / $d + 240; + } + + return array('hsl', fmod($h, 360), $s * 100, $l / 5.1); + } + + public function hueToRGB($m1, $m2, $h) { + if ($h < 0) + $h += 1; + elseif ($h > 1) + $h -= 1; + + if ($h * 6 < 1) + return $m1 + ($m2 - $m1) * $h * 6; + + if ($h * 2 < 1) + return $m2; + + if ($h * 3 < 2) + return $m1 + ($m2 - $m1) * (2/3 - $h) * 6; + + return $m1; + } + + // H from 0 to 360, S and L from 0 to 100 + public function toRGB($hue, $saturation, $lightness) { + if ($hue < 0) { + $hue += 360; + } + + $h = $hue / 360; + $s = min(100, max(0, $saturation)) / 100; + $l = min(100, max(0, $lightness)) / 100; + + $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s; + $m1 = $l * 2 - $m2; + + $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255; + $g = $this->hueToRGB($m1, $m2, $h) * 255; + $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255; + + $out = array('color', $r, $g, $b); + return $out; + } + + // Built in functions + + protected static $lib_if = array("condition", "if-true", "if-false"); + protected function lib_if($args) { + list($cond,$t, $f) = $args; + if (!$this->isTruthy($cond)) return $f; + return $t; + } + + protected static $lib_index = array("list", "value"); + protected function lib_index($args) { + list($list, $value) = $args; + $list = $this->assertList($list); + + $values = array(); + foreach ($list[2] as $item) { + $values[] = $this->normalizeValue($item); + } + $key = array_search($this->normalizeValue($value), $values); + + return false === $key ? false : $key + 1; + } + + protected static $lib_rgb = array("red", "green", "blue"); + protected function lib_rgb($args) { + list($r,$g,$b) = $args; + return array("color", $r[1], $g[1], $b[1]); + } + + protected static $lib_rgba = array( + array("red", "color"), + "green", "blue", "alpha"); + protected function lib_rgba($args) { + if ($color = $this->coerceColor($args[0])) { + $num = !isset($args[1]) ? $args[3] : $args[1]; + $alpha = $this->assertNumber($num); + $color[4] = $alpha; + return $color; + } + + list($r,$g,$b, $a) = $args; + return array("color", $r[1], $g[1], $b[1], $a[1]); + } + + // helper function for adjust_color, change_color, and scale_color + protected function alter_color($args, $fn) { + $color = $this->assertColor($args[0]); + + foreach (array(1,2,3,7) as $i) { + if (isset($args[$i])) { + $val = $this->assertNumber($args[$i]); + $ii = $i == 7 ? 4 : $i; // alpha + $color[$ii] = + $this->$fn(isset($color[$ii]) ? $color[$ii] : 0, $val, $i); + } + } + + if (isset($args[4]) || isset($args[5]) || isset($args[6])) { + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + foreach (array(4,5,6) as $i) { + if (isset($args[$i])) { + $val = $this->assertNumber($args[$i]); + $hsl[$i - 3] = $this->$fn($hsl[$i - 3], $val, $i); + } + } + + $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]); + if (isset($color[4])) $rgb[4] = $color[4]; + $color = $rgb; + } + + return $color; + } + + protected static $lib_adjust_color = array( + "color", "red", "green", "blue", + "hue", "saturation", "lightness", "alpha" + ); + protected function adjust_color_helper($base, $alter, $i) { + return $base += $alter; + } + protected function lib_adjust_color($args) { + return $this->alter_color($args, "adjust_color_helper"); + } + + protected static $lib_change_color = array( + "color", "red", "green", "blue", + "hue", "saturation", "lightness", "alpha" + ); + protected function change_color_helper($base, $alter, $i) { + return $alter; + } + protected function lib_change_color($args) { + return $this->alter_color($args, "change_color_helper"); + } + + protected static $lib_scale_color = array( + "color", "red", "green", "blue", + "hue", "saturation", "lightness", "alpha" + ); + protected function scale_color_helper($base, $scale, $i) { + // 1,2,3 - rgb + // 4, 5, 6 - hsl + // 7 - a + switch ($i) { + case 1: + case 2: + case 3: + $max = 255; break; + case 4: + $max = 360; break; + case 7: + $max = 1; break; + default: + $max = 100; + } + + $scale = $scale / 100; + if ($scale < 0) { + return $base * $scale + $base; + } else { + return ($max - $base) * $scale + $base; + } + } + protected function lib_scale_color($args) { + return $this->alter_color($args, "scale_color_helper"); + } + + protected static $lib_ie_hex_str = array("color"); + protected function lib_ie_hex_str($args) { + $color = $this->coerceColor($args[0]); + $color[4] = isset($color[4]) ? round(255*$color[4]) : 255; + + return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]); + } + + protected static $lib_red = array("color"); + protected function lib_red($args) { + $color = $this->coerceColor($args[0]); + return $color[1]; + } + + protected static $lib_green = array("color"); + protected function lib_green($args) { + $color = $this->coerceColor($args[0]); + return $color[2]; + } + + protected static $lib_blue = array("color"); + protected function lib_blue($args) { + $color = $this->coerceColor($args[0]); + return $color[3]; + } + + protected static $lib_alpha = array("color"); + protected function lib_alpha($args) { + if ($color = $this->coerceColor($args[0])) { + return isset($color[4]) ? $color[4] : 1; + } + + // this might be the IE function, so return value unchanged + return null; + } + + protected static $lib_opacity = array("color"); + protected function lib_opacity($args) { + $value = $args[0]; + if ($value[0] === 'number') return null; + return $this->lib_alpha($args); + } + + // mix two colors + protected static $lib_mix = array("color-1", "color-2", "weight"); + protected function lib_mix($args) { + list($first, $second, $weight) = $args; + $first = $this->assertColor($first); + $second = $this->assertColor($second); + + if (!isset($weight)) { + $weight = 0.5; + } else { + $weight = $this->coercePercent($weight); + } + + $firstAlpha = isset($first[4]) ? $first[4] : 1; + $secondAlpha = isset($second[4]) ? $second[4] : 1; + + $w = $weight * 2 - 1; + $a = $firstAlpha - $secondAlpha; + + $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0; + $w2 = 1.0 - $w1; + + $new = array('color', + $w1 * $first[1] + $w2 * $second[1], + $w1 * $first[2] + $w2 * $second[2], + $w1 * $first[3] + $w2 * $second[3], + ); + + if ($firstAlpha != 1.0 || $secondAlpha != 1.0) { + $new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1); + } + + return $this->fixColor($new); + } + + protected static $lib_hsl = array("hue", "saturation", "lightness"); + protected function lib_hsl($args) { + list($h, $s, $l) = $args; + return $this->toRGB($h[1], $s[1], $l[1]); + } + + protected static $lib_hsla = array("hue", "saturation", + "lightness", "alpha"); + protected function lib_hsla($args) { + list($h, $s, $l, $a) = $args; + $color = $this->toRGB($h[1], $s[1], $l[1]); + $color[4] = $a[1]; + return $color; + } + + protected static $lib_hue = array("color"); + protected function lib_hue($args) { + $color = $this->assertColor($args[0]); + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + return array("number", $hsl[1], "deg"); + } + + protected static $lib_saturation = array("color"); + protected function lib_saturation($args) { + $color = $this->assertColor($args[0]); + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + return array("number", $hsl[2], "%"); + } + + protected static $lib_lightness = array("color"); + protected function lib_lightness($args) { + $color = $this->assertColor($args[0]); + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + return array("number", $hsl[3], "%"); + } + + protected function adjustHsl($color, $idx, $amount) { + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + $hsl[$idx] += $amount; + $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]); + if (isset($color[4])) $out[4] = $color[4]; + return $out; + } + + protected static $lib_adjust_hue = array("color", "degrees"); + protected function lib_adjust_hue($args) { + $color = $this->assertColor($args[0]); + $degrees = $this->assertNumber($args[1]); + return $this->adjustHsl($color, 1, $degrees); + } + + protected static $lib_lighten = array("color", "amount"); + protected function lib_lighten($args) { + $color = $this->assertColor($args[0]); + $amount = 100*$this->coercePercent($args[1]); + return $this->adjustHsl($color, 3, $amount); + } + + protected static $lib_darken = array("color", "amount"); + protected function lib_darken($args) { + $color = $this->assertColor($args[0]); + $amount = 100*$this->coercePercent($args[1]); + return $this->adjustHsl($color, 3, -$amount); + } + + protected static $lib_saturate = array("color", "amount"); + protected function lib_saturate($args) { + $value = $args[0]; + if ($value[0] === 'number') return null; + $color = $this->assertColor($value); + $amount = 100*$this->coercePercent($args[1]); + return $this->adjustHsl($color, 2, $amount); + } + + protected static $lib_desaturate = array("color", "amount"); + protected function lib_desaturate($args) { + $color = $this->assertColor($args[0]); + $amount = 100*$this->coercePercent($args[1]); + return $this->adjustHsl($color, 2, -$amount); + } + + protected static $lib_grayscale = array("color"); + protected function lib_grayscale($args) { + $value = $args[0]; + if ($value[0] === 'number') return null; + return $this->adjustHsl($this->assertColor($value), 2, -100); + } + + protected static $lib_complement = array("color"); + protected function lib_complement($args) { + return $this->adjustHsl($this->assertColor($args[0]), 1, 180); + } + + protected static $lib_invert = array("color"); + protected function lib_invert($args) { + $value = $args[0]; + if ($value[0] === 'number') return null; + $color = $this->assertColor($value); + $color[1] = 255 - $color[1]; + $color[2] = 255 - $color[2]; + $color[3] = 255 - $color[3]; + return $color; + } + + // increases opacity by amount + protected static $lib_opacify = array("color", "amount"); + protected function lib_opacify($args) { + $color = $this->assertColor($args[0]); + $amount = $this->coercePercent($args[1]); + + $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount; + $color[4] = min(1, max(0, $color[4])); + return $color; + } + + protected static $lib_fade_in = array("color", "amount"); + protected function lib_fade_in($args) { + return $this->lib_opacify($args); + } + + // decreases opacity by amount + protected static $lib_transparentize = array("color", "amount"); + protected function lib_transparentize($args) { + $color = $this->assertColor($args[0]); + $amount = $this->coercePercent($args[1]); + + $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount; + $color[4] = min(1, max(0, $color[4])); + return $color; + } + + protected static $lib_fade_out = array("color", "amount"); + protected function lib_fade_out($args) { + return $this->lib_transparentize($args); + } + + protected static $lib_unquote = array("string"); + protected function lib_unquote($args) { + $str = $args[0]; + if ($str[0] == "string") $str[1] = ""; + return $str; + } + + protected static $lib_quote = array("string"); + protected function lib_quote($args) { + $value = $args[0]; + if ($value[0] == "string" && !empty($value[1])) + return $value; + return array("string", '"', array($value)); + } + + protected static $lib_percentage = array("value"); + protected function lib_percentage($args) { + return array("number", + $this->coercePercent($args[0]) * 100, + "%"); + } + + protected static $lib_round = array("value"); + protected function lib_round($args) { + $num = $args[0]; + $num[1] = round($num[1]); + return $num; + } + + protected static $lib_floor = array("value"); + protected function lib_floor($args) { + $num = $args[0]; + $num[1] = floor($num[1]); + return $num; + } + + protected static $lib_ceil = array("value"); + protected function lib_ceil($args) { + $num = $args[0]; + $num[1] = ceil($num[1]); + return $num; + } + + protected static $lib_abs = array("value"); + protected function lib_abs($args) { + $num = $args[0]; + $num[1] = abs($num[1]); + return $num; + } + + protected function lib_min($args) { + $numbers = $this->getNormalizedNumbers($args); + $min = null; + foreach ($numbers as $key => $number) { + if (null === $min || $number[1] <= $min[1]) { + $min = array($key, $number[1]); + } + } + + return $args[$min[0]]; + } + + protected function lib_max($args) { + $numbers = $this->getNormalizedNumbers($args); + $max = null; + foreach ($numbers as $key => $number) { + if (null === $max || $number[1] >= $max[1]) { + $max = array($key, $number[1]); + } + } + + return $args[$max[0]]; + } + + protected function getNormalizedNumbers($args) { + $unit = null; + $originalUnit = null; + $numbers = array(); + foreach ($args as $key => $item) { + if ('number' != $item[0]) { + $this->throwError("%s is not a number", $item[0]); + } + $number = $this->normalizeNumber($item); + + if (null === $unit) { + $unit = $number[2]; + $originalUnit = $item[2]; + } elseif ($unit !== $number[2]) { + $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item[2]); + } + + $numbers[$key] = $number; + } + + return $numbers; + } + + protected static $lib_length = array("list"); + protected function lib_length($args) { + $list = $this->coerceList($args[0]); + return count($list[2]); + } + + protected static $lib_nth = array("list", "n"); + protected function lib_nth($args) { + $list = $this->coerceList($args[0]); + $n = $this->assertNumber($args[1]) - 1; + return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue; + } + + protected function listSeparatorForJoin($list1, $sep) { + if (!isset($sep)) return $list1[1]; + switch ($this->compileValue($sep)) { + case "comma": + return ","; + case "space": + return ""; + default: + return $list1[1]; + } + } + + protected static $lib_join = array("list1", "list2", "separator"); + protected function lib_join($args) { + list($list1, $list2, $sep) = $args; + $list1 = $this->coerceList($list1, " "); + $list2 = $this->coerceList($list2, " "); + $sep = $this->listSeparatorForJoin($list1, $sep); + return array("list", $sep, array_merge($list1[2], $list2[2])); + } + + protected static $lib_append = array("list", "val", "separator"); + protected function lib_append($args) { + list($list1, $value, $sep) = $args; + $list1 = $this->coerceList($list1, " "); + $sep = $this->listSeparatorForJoin($list1, $sep); + return array("list", $sep, array_merge($list1[2], array($value))); + } + + protected function lib_zip($args) { + foreach ($args as $arg) { + $this->assertList($arg); + } + + $lists = array(); + $firstList = array_shift($args); + foreach ($firstList[2] as $key => $item) { + $list = array("list", "", array($item)); + foreach ($args as $arg) { + if (isset($arg[2][$key])) { + $list[2][] = $arg[2][$key]; + } else { + break 2; + } + } + $lists[] = $list; + } + + return array("list", ",", $lists); + } + + protected static $lib_type_of = array("value"); + protected function lib_type_of($args) { + $value = $args[0]; + switch ($value[0]) { + case "keyword": + if ($value == self::$true || $value == self::$false) { + return "bool"; + } + + if ($this->coerceColor($value)) { + return "color"; + } + + return "string"; + default: + return $value[0]; + } + } + + protected static $lib_unit = array("number"); + protected function lib_unit($args) { + $num = $args[0]; + if ($num[0] == "number") { + return array("string", '"', array($num[2])); + } + return ""; + } + + protected static $lib_unitless = array("number"); + protected function lib_unitless($args) { + $value = $args[0]; + return $value[0] == "number" && empty($value[2]); + } + + protected static $lib_comparable = array("number-1", "number-2"); + protected function lib_comparable($args) { + list($number1, $number2) = $args; + if (!isset($number1[0]) || $number1[0] != "number" || !isset($number2[0]) || $number2[0] != "number") { + $this->throwError('Invalid argument(s) for "comparable"'); + } + + $number1 = $this->normalizeNumber($number1); + $number2 = $this->normalizeNumber($number2); + + return $number1[2] == $number2[2] || $number1[2] == "" || $number2[2] == ""; + } + + /** + * Workaround IE7's content counter bug. + * + * @param array $args + */ + protected function lib_counter($args) { + $list = array_map(array($this, 'compileValue'), $args); + return array('string', '', array('counter(' . implode(',', $list) . ')')); + } + + public function throwError($msg = null) { + if (func_num_args() > 1) { + $msg = call_user_func_array("sprintf", func_get_args()); + } + + if ($this->sourcePos >= 0 && isset($this->sourceParser)) { + $this->sourceParser->throwParseError($msg, $this->sourcePos); + } + + throw new Exception($msg); + } + + /** + * CSS Colors + * + * @see http://www.w3.org/TR/css3-color + */ + static protected $cssColors = array( + 'aliceblue' => '240,248,255', + 'antiquewhite' => '250,235,215', + 'aqua' => '0,255,255', + 'aquamarine' => '127,255,212', + 'azure' => '240,255,255', + 'beige' => '245,245,220', + 'bisque' => '255,228,196', + 'black' => '0,0,0', + 'blanchedalmond' => '255,235,205', + 'blue' => '0,0,255', + 'blueviolet' => '138,43,226', + 'brown' => '165,42,42', + 'burlywood' => '222,184,135', + 'cadetblue' => '95,158,160', + 'chartreuse' => '127,255,0', + 'chocolate' => '210,105,30', + 'coral' => '255,127,80', + 'cornflowerblue' => '100,149,237', + 'cornsilk' => '255,248,220', + 'crimson' => '220,20,60', + 'cyan' => '0,255,255', + 'darkblue' => '0,0,139', + 'darkcyan' => '0,139,139', + 'darkgoldenrod' => '184,134,11', + 'darkgray' => '169,169,169', + 'darkgreen' => '0,100,0', + 'darkgrey' => '169,169,169', + 'darkkhaki' => '189,183,107', + 'darkmagenta' => '139,0,139', + 'darkolivegreen' => '85,107,47', + 'darkorange' => '255,140,0', + 'darkorchid' => '153,50,204', + 'darkred' => '139,0,0', + 'darksalmon' => '233,150,122', + 'darkseagreen' => '143,188,143', + 'darkslateblue' => '72,61,139', + 'darkslategray' => '47,79,79', + 'darkslategrey' => '47,79,79', + 'darkturquoise' => '0,206,209', + 'darkviolet' => '148,0,211', + 'deeppink' => '255,20,147', + 'deepskyblue' => '0,191,255', + 'dimgray' => '105,105,105', + 'dimgrey' => '105,105,105', + 'dodgerblue' => '30,144,255', + 'firebrick' => '178,34,34', + 'floralwhite' => '255,250,240', + 'forestgreen' => '34,139,34', + 'fuchsia' => '255,0,255', + 'gainsboro' => '220,220,220', + 'ghostwhite' => '248,248,255', + 'gold' => '255,215,0', + 'goldenrod' => '218,165,32', + 'gray' => '128,128,128', + 'green' => '0,128,0', + 'greenyellow' => '173,255,47', + 'grey' => '128,128,128', + 'honeydew' => '240,255,240', + 'hotpink' => '255,105,180', + 'indianred' => '205,92,92', + 'indigo' => '75,0,130', + 'ivory' => '255,255,240', + 'khaki' => '240,230,140', + 'lavender' => '230,230,250', + 'lavenderblush' => '255,240,245', + 'lawngreen' => '124,252,0', + 'lemonchiffon' => '255,250,205', + 'lightblue' => '173,216,230', + 'lightcoral' => '240,128,128', + 'lightcyan' => '224,255,255', + 'lightgoldenrodyellow' => '250,250,210', + 'lightgray' => '211,211,211', + 'lightgreen' => '144,238,144', + 'lightgrey' => '211,211,211', + 'lightpink' => '255,182,193', + 'lightsalmon' => '255,160,122', + 'lightseagreen' => '32,178,170', + 'lightskyblue' => '135,206,250', + 'lightslategray' => '119,136,153', + 'lightslategrey' => '119,136,153', + 'lightsteelblue' => '176,196,222', + 'lightyellow' => '255,255,224', + 'lime' => '0,255,0', + 'limegreen' => '50,205,50', + 'linen' => '250,240,230', + 'magenta' => '255,0,255', + 'maroon' => '128,0,0', + 'mediumaquamarine' => '102,205,170', + 'mediumblue' => '0,0,205', + 'mediumorchid' => '186,85,211', + 'mediumpurple' => '147,112,219', + 'mediumseagreen' => '60,179,113', + 'mediumslateblue' => '123,104,238', + 'mediumspringgreen' => '0,250,154', + 'mediumturquoise' => '72,209,204', + 'mediumvioletred' => '199,21,133', + 'midnightblue' => '25,25,112', + 'mintcream' => '245,255,250', + 'mistyrose' => '255,228,225', + 'moccasin' => '255,228,181', + 'navajowhite' => '255,222,173', + 'navy' => '0,0,128', + 'oldlace' => '253,245,230', + 'olive' => '128,128,0', + 'olivedrab' => '107,142,35', + 'orange' => '255,165,0', + 'orangered' => '255,69,0', + 'orchid' => '218,112,214', + 'palegoldenrod' => '238,232,170', + 'palegreen' => '152,251,152', + 'paleturquoise' => '175,238,238', + 'palevioletred' => '219,112,147', + 'papayawhip' => '255,239,213', + 'peachpuff' => '255,218,185', + 'peru' => '205,133,63', + 'pink' => '255,192,203', + 'plum' => '221,160,221', + 'powderblue' => '176,224,230', + 'purple' => '128,0,128', + 'red' => '255,0,0', + 'rosybrown' => '188,143,143', + 'royalblue' => '65,105,225', + 'saddlebrown' => '139,69,19', + 'salmon' => '250,128,114', + 'sandybrown' => '244,164,96', + 'seagreen' => '46,139,87', + 'seashell' => '255,245,238', + 'sienna' => '160,82,45', + 'silver' => '192,192,192', + 'skyblue' => '135,206,235', + 'slateblue' => '106,90,205', + 'slategray' => '112,128,144', + 'slategrey' => '112,128,144', + 'snow' => '255,250,250', + 'springgreen' => '0,255,127', + 'steelblue' => '70,130,180', + 'tan' => '210,180,140', + 'teal' => '0,128,128', + 'thistle' => '216,191,216', + 'tomato' => '255,99,71', + 'transparent' => '0,0,0,0', + 'turquoise' => '64,224,208', + 'violet' => '238,130,238', + 'wheat' => '245,222,179', + 'white' => '255,255,255', + 'whitesmoke' => '245,245,245', + 'yellow' => '255,255,0', + 'yellowgreen' => '154,205,50' + ); +} + +/** + * SCSS parser + * + * @author Leaf Corcoran <leafot@gmail.com> + */ +class scss_parser { + static protected $precedence = array( + "or" => 0, + "and" => 1, + + '==' => 2, + '!=' => 2, + '<=' => 2, + '>=' => 2, + '=' => 2, + '<' => 3, + '>' => 2, + + '+' => 3, + '-' => 3, + '*' => 4, + '/' => 4, + '%' => 4, + ); + + static protected $operators = array("+", "-", "*", "/", "%", + "==", "!=", "<=", ">=", "<", ">", "and", "or"); + + static protected $operatorStr; + static protected $whitePattern; + static protected $commentMulti; + + static protected $commentSingle = "//"; + static protected $commentMultiLeft = "/*"; + static protected $commentMultiRight = "*/"; + + /** + * Constructor + * + * @param string $sourceName + * @param boolean $rootParser + */ + public function __construct($sourceName = null, $rootParser = true) { + $this->sourceName = $sourceName; + $this->rootParser = $rootParser; + + if (empty(self::$operatorStr)) { + self::$operatorStr = $this->makeOperatorStr(self::$operators); + + $commentSingle = $this->preg_quote(self::$commentSingle); + $commentMultiLeft = $this->preg_quote(self::$commentMultiLeft); + $commentMultiRight = $this->preg_quote(self::$commentMultiRight); + self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight; + self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais'; + } + } + + static protected function makeOperatorStr($operators) { + return '('.implode('|', array_map(array('scss_parser','preg_quote'), + $operators)).')'; + } + + /** + * Parser buffer + * + * @param string $buffer; + * + * @return \StdClass + */ + public function parse($buffer) + { + $this->count = 0; + $this->env = null; + $this->inParens = false; + $this->eatWhiteDefault = true; + $this->insertComments = true; + $this->buffer = $buffer; + + $this->pushBlock(null); // root block + $this->whitespace(); + + while (false !== $this->parseChunk()) + ; + + if ($this->count != strlen($this->buffer)) { + $this->throwParseError(); + } + + if (!empty($this->env->parent)) { + $this->throwParseError("unclosed block"); + } + + $this->env->isRoot = true; + + return $this->env; + } + + /** + * Parse a single chunk off the head of the buffer and append it to the + * current parse environment. + * + * Returns false when the buffer is empty, or when there is an error. + * + * This function is called repeatedly until the entire document is + * parsed. + * + * This parser is most similar to a recursive descent parser. Single + * functions represent discrete grammatical rules for the language, and + * they are able to capture the text that represents those rules. + * + * Consider the function scssc::keyword(). (All parse functions are + * structured the same.) + * + * The function takes a single reference argument. When calling the + * function it will attempt to match a keyword on the head of the buffer. + * If it is successful, it will place the keyword in the referenced + * argument, advance the position in the buffer, and return true. If it + * fails then it won't advance the buffer and it will return false. + * + * All of these parse functions are powered by scssc::match(), which behaves + * the same way, but takes a literal regular expression. Sometimes it is + * more convenient to use match instead of creating a new function. + * + * Because of the format of the functions, to parse an entire string of + * grammatical rules, you can chain them together using &&. + * + * But, if some of the rules in the chain succeed before one fails, then + * the buffer position will be left at an invalid state. In order to + * avoid this, scssc::seek() is used to remember and set buffer positions. + * + * Before parsing a chain, use $s = $this->seek() to remember the current + * position into $s. Then if a chain fails, use $this->seek($s) to + * go back where we started. + * + * @return boolean + */ + protected function parseChunk() { + $s = $this->seek(); + + // the directives + if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") { + if ($this->literal("@media") && $this->mediaQueryList($mediaQueryList) && $this->literal("{")) { + $media = $this->pushSpecialBlock("media"); + $media->queryList = $mediaQueryList[2]; + return true; + } else { + $this->seek($s); + } + + if ($this->literal("@mixin") && + $this->keyword($mixinName) && + ($this->argumentDef($args) || true) && + $this->literal("{")) + { + $mixin = $this->pushSpecialBlock("mixin"); + $mixin->name = $mixinName; + $mixin->args = $args; + return true; + } else { + $this->seek($s); + } + + if ($this->literal("@include") && + $this->keyword($mixinName) && + ($this->literal("(") && + ($this->argValues($argValues) || true) && + $this->literal(")") || true) && + ($this->end() || + $this->literal("{") && $hasBlock = true)) + { + $child = array("include", + $mixinName, isset($argValues) ? $argValues : null, null); + + if (!empty($hasBlock)) { + $include = $this->pushSpecialBlock("include"); + $include->child = $child; + } else { + $this->append($child, $s); + } + + return true; + } else { + $this->seek($s); + } + + if ($this->literal("@import") && + $this->valueList($importPath) && + $this->end()) + { + $this->append(array("import", $importPath), $s); + return true; + } else { + $this->seek($s); + } + + if ($this->literal("@extend") && + $this->selectors($selector) && + $this->end()) + { + $this->append(array("extend", $selector), $s); + return true; + } else { + $this->seek($s); + } + + if ($this->literal("@function") && + $this->keyword($fnName) && + $this->argumentDef($args) && + $this->literal("{")) + { + $func = $this->pushSpecialBlock("function"); + $func->name = $fnName; + $func->args = $args; + return true; + } else { + $this->seek($s); + } + + if ($this->literal("@return") && $this->valueList($retVal) && $this->end()) { + $this->append(array("return", $retVal), $s); + return true; + } else { + $this->seek($s); + } + + if ($this->literal("@each") && + $this->variable($varName) && + $this->literal("in") && + $this->valueList($list) && + $this->literal("{")) + { + $each = $this->pushSpecialBlock("each"); + $each->var = $varName[1]; + $each->list = $list; + return true; + } else { + $this->seek($s); + } + + if ($this->literal("@while") && + $this->expression($cond) && + $this->literal("{")) + { + $while = $this->pushSpecialBlock("while"); + $while->cond = $cond; + return true; + } else { + $this->seek($s); + } + + if ($this->literal("@for") && + $this->variable($varName) && + $this->literal("from") && + $this->expression($start) && + ($this->literal("through") || + ($forUntil = true && $this->literal("to"))) && + $this->expression($end) && + $this->literal("{")) + { + $for = $this->pushSpecialBlock("for"); + $for->var = $varName[1]; + $for->start = $start; + $for->end = $end; + $for->until = isset($forUntil); + return true; + } else { + $this->seek($s); + } + + if ($this->literal("@if") && $this->valueList($cond) && $this->literal("{")) { + $if = $this->pushSpecialBlock("if"); + $if->cond = $cond; + $if->cases = array(); + return true; + } else { + $this->seek($s); + } + + if (($this->literal("@debug") || $this->literal("@warn")) && + $this->valueList($value) && + $this->end()) { + $this->append(array("debug", $value, $s), $s); + return true; + } else { + $this->seek($s); + } + + if ($this->literal("@content") && $this->end()) { + $this->append(array("mixin_content"), $s); + return true; + } else { + $this->seek($s); + } + + $last = $this->last(); + if (isset($last) && $last[0] == "if") { + list(, $if) = $last; + if ($this->literal("@else")) { + if ($this->literal("{")) { + $else = $this->pushSpecialBlock("else"); + } elseif ($this->literal("if") && $this->valueList($cond) && $this->literal("{")) { + $else = $this->pushSpecialBlock("elseif"); + $else->cond = $cond; + } + + if (isset($else)) { + $else->dontAppend = true; + $if->cases[] = $else; + return true; + } + } + + $this->seek($s); + } + + if ($this->literal("@charset") && + $this->valueList($charset) && $this->end()) + { + $this->append(array("charset", $charset), $s); + return true; + } else { + $this->seek($s); + } + + // doesn't match built in directive, do generic one + if ($this->literal("@", false) && $this->keyword($dirName) && + ($this->openString("{", $dirValue) || true) && + $this->literal("{")) + { + $directive = $this->pushSpecialBlock("directive"); + $directive->name = $dirName; + if (isset($dirValue)) $directive->value = $dirValue; + return true; + } + + $this->seek($s); + return false; + } + + // property shortcut + // captures most properties before having to parse a selector + if ($this->keyword($name, false) && + $this->literal(": ") && + $this->valueList($value) && + $this->end()) + { + $name = array("string", "", array($name)); + $this->append(array("assign", $name, $value), $s); + return true; + } else { + $this->seek($s); + } + + // variable assigns + if ($this->variable($name) && + $this->literal(":") && + $this->valueList($value) && $this->end()) + { + // check for !default + $defaultVar = $value[0] == "list" && $this->stripDefault($value); + $this->append(array("assign", $name, $value, $defaultVar), $s); + return true; + } else { + $this->seek($s); + } + + // misc + if ($this->literal("-->")) { + return true; + } + + // opening css block + $oldComments = $this->insertComments; + $this->insertComments = false; + if ($this->selectors($selectors) && $this->literal("{")) { + $this->pushBlock($selectors); + $this->insertComments = $oldComments; + return true; + } else { + $this->seek($s); + } + $this->insertComments = $oldComments; + + // property assign, or nested assign + if ($this->propertyName($name) && $this->literal(":")) { + $foundSomething = false; + if ($this->valueList($value)) { + $this->append(array("assign", $name, $value), $s); + $foundSomething = true; + } + + if ($this->literal("{")) { + $propBlock = $this->pushSpecialBlock("nestedprop"); + $propBlock->prefix = $name; + $foundSomething = true; + } elseif ($foundSomething) { + $foundSomething = $this->end(); + } + + if ($foundSomething) { + return true; + } + + $this->seek($s); + } else { + $this->seek($s); + } + + // closing a block + if ($this->literal("}")) { + $block = $this->popBlock(); + if (isset($block->type) && $block->type == "include") { + $include = $block->child; + unset($block->child); + $include[3] = $block; + $this->append($include, $s); + } elseif (empty($block->dontAppend)) { + $type = isset($block->type) ? $block->type : "block"; + $this->append(array($type, $block), $s); + } + return true; + } + + // extra stuff + if ($this->literal(";") || + $this->literal("<!--")) + { + return true; + } + + return false; + } + + protected function stripDefault(&$value) { + $def = end($value[2]); + if ($def[0] == "keyword" && $def[1] == "!default") { + array_pop($value[2]); + $value = $this->flattenList($value); + return true; + } + + if ($def[0] == "list") { + return $this->stripDefault($value[2][count($value[2]) - 1]); + } + + return false; + } + + protected function literal($what, $eatWhitespace = null) { + if (!isset($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault; + + // shortcut on single letter + if (!isset($what[1]) && isset($this->buffer[$this->count])) { + if ($this->buffer[$this->count] == $what) { + if (!$eatWhitespace) { + $this->count++; + return true; + } + // goes below... + } else { + return false; + } + } + + return $this->match($this->preg_quote($what), $m, $eatWhitespace); + } + + // tree builders + + protected function pushBlock($selectors) { + $b = new stdClass; + $b->parent = $this->env; // not sure if we need this yet + + $b->selectors = $selectors; + $b->children = array(); + + $this->env = $b; + return $b; + } + + protected function pushSpecialBlock($type) { + $block = $this->pushBlock(null); + $block->type = $type; + return $block; + } + + protected function popBlock() { + if (empty($this->env->parent)) { + $this->throwParseError("unexpected }"); + } + + $old = $this->env; + $this->env = $this->env->parent; + unset($old->parent); + return $old; + } + + protected function append($statement, $pos=null) { + if ($pos !== null) { + $statement[-1] = $pos; + if (!$this->rootParser) $statement[-2] = $this; + } + $this->env->children[] = $statement; + } + + // last child that was appended + protected function last() { + $i = count($this->env->children) - 1; + if (isset($this->env->children[$i])) + return $this->env->children[$i]; + } + + // high level parsers (they return parts of ast) + + protected function mediaQueryList(&$out) { + return $this->genericList($out, "mediaQuery", ",", false); + } + + protected function mediaQuery(&$out) { + $s = $this->seek(); + + $expressions = null; + $parts = array(); + + if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->mixedKeyword($mediaType)) { + $prop = array("mediaType"); + if (isset($only)) $prop[] = array("keyword", "only"); + if (isset($not)) $prop[] = array("keyword", "not"); + $media = array("list", "", array()); + foreach ((array)$mediaType as $type) { + if (is_array($type)) { + $media[2][] = $type; + } else { + $media[2][] = array("keyword", $type); + } + } + $prop[] = $media; + $parts[] = $prop; + } + + if (empty($parts) || $this->literal("and")) { + $this->genericList($expressions, "mediaExpression", "and", false); + if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]); + } + + $out = $parts; + return true; + } + + protected function mediaExpression(&$out) { + $s = $this->seek(); + $value = null; + if ($this->literal("(") && + $this->expression($feature) && + ($this->literal(":") && $this->expression($value) || true) && + $this->literal(")")) + { + $out = array("mediaExp", $feature); + if ($value) $out[] = $value; + return true; + } + + $this->seek($s); + return false; + } + + protected function argValues(&$out) { + if ($this->genericList($list, "argValue", ",", false)) { + $out = $list[2]; + return true; + } + return false; + } + + protected function argValue(&$out) { + $s = $this->seek(); + + $keyword = null; + if (!$this->variable($keyword) || !$this->literal(":")) { + $this->seek($s); + $keyword = null; + } + + if ($this->genericList($value, "expression")) { + $out = array($keyword, $value, false); + $s = $this->seek(); + if ($this->literal("...")) { + $out[2] = true; + } else { + $this->seek($s); + } + return true; + } + + return false; + } + + /** + * Parse list + * + * @param string $out + * + * @return boolean + */ + public function valueList(&$out) + { + return $this->genericList($out, 'spaceList', ','); + } + + protected function spaceList(&$out) + { + return $this->genericList($out, 'expression'); + } + + protected function genericList(&$out, $parseItem, $delim="", $flatten=true) { + $s = $this->seek(); + $items = array(); + while ($this->$parseItem($value)) { + $items[] = $value; + if ($delim) { + if (!$this->literal($delim)) break; + } + } + + if (count($items) == 0) { + $this->seek($s); + return false; + } + + if ($flatten && count($items) == 1) { + $out = $items[0]; + } else { + $out = array("list", $delim, $items); + } + + return true; + } + + protected function expression(&$out) { + $s = $this->seek(); + + if ($this->literal("(")) { + if ($this->literal(")")) { + $out = array("list", "", array()); + return true; + } + + if ($this->valueList($out) && $this->literal(')') && $out[0] == "list") { + return true; + } + + $this->seek($s); + } + + if ($this->value($lhs)) { + $out = $this->expHelper($lhs, 0); + return true; + } + + return false; + } + + protected function expHelper($lhs, $minP) { + $opstr = self::$operatorStr; + + $ss = $this->seek(); + $whiteBefore = isset($this->buffer[$this->count - 1]) && + ctype_space($this->buffer[$this->count - 1]); + while ($this->match($opstr, $m) && self::$precedence[$m[1]] >= $minP) { + $whiteAfter = isset($this->buffer[$this->count - 1]) && + ctype_space($this->buffer[$this->count - 1]); + + $op = $m[1]; + + // don't turn negative numbers into expressions + if ($op == "-" && $whiteBefore) { + if (!$whiteAfter) break; + } + + if (!$this->value($rhs)) break; + + // peek and see if rhs belongs to next operator + if ($this->peek($opstr, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) { + $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]); + } + + $lhs = array("exp", $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter); + $ss = $this->seek(); + $whiteBefore = isset($this->buffer[$this->count - 1]) && + ctype_space($this->buffer[$this->count - 1]); + } + + $this->seek($ss); + return $lhs; + } + + protected function value(&$out) { + $s = $this->seek(); + + if ($this->literal("not", false) && $this->whitespace() && $this->value($inner)) { + $out = array("unary", "not", $inner, $this->inParens); + return true; + } else { + $this->seek($s); + } + + if ($this->literal("+") && $this->value($inner)) { + $out = array("unary", "+", $inner, $this->inParens); + return true; + } else { + $this->seek($s); + } + + // negation + if ($this->literal("-", false) && + ($this->variable($inner) || + $this->unit($inner) || + $this->parenValue($inner))) + { + $out = array("unary", "-", $inner, $this->inParens); + return true; + } else { + $this->seek($s); + } + + if ($this->parenValue($out)) return true; + if ($this->interpolation($out)) return true; + if ($this->variable($out)) return true; + if ($this->color($out)) return true; + if ($this->unit($out)) return true; + if ($this->string($out)) return true; + if ($this->func($out)) return true; + if ($this->progid($out)) return true; + + if ($this->keyword($keyword)) { + if ($keyword == "null") { + $out = array("null"); + } else { + $out = array("keyword", $keyword); + } + return true; + } + + return false; + } + + // value wrappen in parentheses + protected function parenValue(&$out) { + $s = $this->seek(); + + $inParens = $this->inParens; + if ($this->literal("(") && + ($this->inParens = true) && $this->expression($exp) && + $this->literal(")")) + { + $out = $exp; + $this->inParens = $inParens; + return true; + } else { + $this->inParens = $inParens; + $this->seek($s); + } + + return false; + } + + protected function progid(&$out) { + $s = $this->seek(); + if ($this->literal("progid:", false) && + $this->openString("(", $fn) && + $this->literal("(")) + { + $this->openString(")", $args, "("); + if ($this->literal(")")) { + $out = array("string", "", array( + "progid:", $fn, "(", $args, ")" + )); + return true; + } + } + + $this->seek($s); + return false; + } + + protected function func(&$func) { + $s = $this->seek(); + + if ($this->keyword($name, false) && + $this->literal("(")) + { + if ($name == "alpha" && $this->argumentList($args)) { + $func = array("function", $name, array("string", "", $args)); + return true; + } + + if ($name != "expression" && !preg_match("/^(-[a-z]+-)?calc$/", $name)) { + $ss = $this->seek(); + if ($this->argValues($args) && $this->literal(")")) { + $func = array("fncall", $name, $args); + return true; + } + $this->seek($ss); + } + + if (($this->openString(")", $str, "(") || true ) && + $this->literal(")")) + { + $args = array(); + if (!empty($str)) { + $args[] = array(null, array("string", "", array($str))); + } + + $func = array("fncall", $name, $args); + return true; + } + } + + $this->seek($s); + return false; + } + + protected function argumentList(&$out) { + $s = $this->seek(); + $this->literal("("); + + $args = array(); + while ($this->keyword($var)) { + $ss = $this->seek(); + + if ($this->literal("=") && $this->expression($exp)) { + $args[] = array("string", "", array($var."=")); + $arg = $exp; + } else { + break; + } + + $args[] = $arg; + + if (!$this->literal(",")) break; + + $args[] = array("string", "", array(", ")); + } + + if (!$this->literal(")") || !count($args)) { + $this->seek($s); + return false; + } + + $out = $args; + return true; + } + + protected function argumentDef(&$out) { + $s = $this->seek(); + $this->literal("("); + + $args = array(); + while ($this->variable($var)) { + $arg = array($var[1], null, false); + + $ss = $this->seek(); + if ($this->literal(":") && $this->genericList($defaultVal, "expression")) { + $arg[1] = $defaultVal; + } else { + $this->seek($ss); + } + + $ss = $this->seek(); + if ($this->literal("...")) { + $sss = $this->seek(); + if (!$this->literal(")")) { + $this->throwParseError("... has to be after the final argument"); + } + $arg[2] = true; + $this->seek($sss); + } else { + $this->seek($ss); + } + + $args[] = $arg; + if (!$this->literal(",")) break; + } + + if (!$this->literal(")")) { + $this->seek($s); + return false; + } + + $out = $args; + return true; + } + + protected function color(&$out) { + $color = array('color'); + + if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) { + if (isset($m[3])) { + $num = $m[3]; + $width = 16; + } else { + $num = $m[2]; + $width = 256; + } + + $num = hexdec($num); + foreach (array(3,2,1) as $i) { + $t = $num % $width; + $num /= $width; + + $color[$i] = $t * (256/$width) + $t * floor(16/$width); + } + + $out = $color; + return true; + } + + return false; + } + + protected function unit(&$unit) { + if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) { + $unit = array("number", $m[1], empty($m[3]) ? "" : $m[3]); + return true; + } + return false; + } + + protected function string(&$out) { + $s = $this->seek(); + if ($this->literal('"', false)) { + $delim = '"'; + } elseif ($this->literal("'", false)) { + $delim = "'"; + } else { + return false; + } + + $content = array(); + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + while ($this->matchString($m, $delim)) { + $content[] = $m[1]; + if ($m[2] == "#{") { + $this->count -= strlen($m[2]); + if ($this->interpolation($inter, false)) { + $content[] = $inter; + } else { + $this->count += strlen($m[2]); + $content[] = "#{"; // ignore it + } + } elseif ($m[2] == '\\') { + $content[] = $m[2]; + if ($this->literal($delim, false)) { + $content[] = $delim; + } + } else { + $this->count -= strlen($delim); + break; // delim + } + } + + $this->eatWhiteDefault = $oldWhite; + + if ($this->literal($delim)) { + $out = array("string", $delim, $content); + return true; + } + + $this->seek($s); + return false; + } + + protected function mixedKeyword(&$out) { + $s = $this->seek(); + + $parts = array(); + + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + while (true) { + if ($this->keyword($key)) { + $parts[] = $key; + continue; + } + + if ($this->interpolation($inter)) { + $parts[] = $inter; + continue; + } + + break; + } + + $this->eatWhiteDefault = $oldWhite; + + if (count($parts) == 0) return false; + + if ($this->eatWhiteDefault) { + $this->whitespace(); + } + + $out = $parts; + return true; + } + + // an unbounded string stopped by $end + protected function openString($end, &$out, $nestingOpen=null) { + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + $stop = array("'", '"', "#{", $end); + $stop = array_map(array($this, "preg_quote"), $stop); + $stop[] = self::$commentMulti; + + $patt = '(.*?)('.implode("|", $stop).')'; + + $nestingLevel = 0; + + $content = array(); + while ($this->match($patt, $m, false)) { + if (isset($m[1]) && $m[1] !== '') { + $content[] = $m[1]; + if ($nestingOpen) { + $nestingLevel += substr_count($m[1], $nestingOpen); + } + } + + $tok = $m[2]; + + $this->count-= strlen($tok); + if ($tok == $end) { + if ($nestingLevel == 0) { + break; + } else { + $nestingLevel--; + } + } + + if (($tok == "'" || $tok == '"') && $this->string($str)) { + $content[] = $str; + continue; + } + + if ($tok == "#{" && $this->interpolation($inter)) { + $content[] = $inter; + continue; + } + + $content[] = $tok; + $this->count+= strlen($tok); + } + + $this->eatWhiteDefault = $oldWhite; + + if (count($content) == 0) return false; + + // trim the end + if (is_string(end($content))) { + $content[count($content) - 1] = rtrim(end($content)); + } + + $out = array("string", "", $content); + return true; + } + + // $lookWhite: save information about whitespace before and after + protected function interpolation(&$out, $lookWhite=true) { + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = true; + + $s = $this->seek(); + if ($this->literal("#{") && $this->valueList($value) && $this->literal("}", false)) { + + // TODO: don't error if out of bounds + + if ($lookWhite) { + $left = preg_match('/\s/', $this->buffer[$s - 1]) ? " " : ""; + $right = preg_match('/\s/', $this->buffer[$this->count]) ? " ": ""; + } else { + $left = $right = false; + } + + $out = array("interpolate", $value, $left, $right); + $this->eatWhiteDefault = $oldWhite; + if ($this->eatWhiteDefault) $this->whitespace(); + return true; + } + + $this->seek($s); + $this->eatWhiteDefault = $oldWhite; + return false; + } + + // low level parsers + + // returns an array of parts or a string + protected function propertyName(&$out) { + $s = $this->seek(); + $parts = array(); + + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + while (true) { + if ($this->interpolation($inter)) { + $parts[] = $inter; + } elseif ($this->keyword($text)) { + $parts[] = $text; + } elseif (count($parts) == 0 && $this->match('[:.#]', $m, false)) { + // css hacks + $parts[] = $m[0]; + } else { + break; + } + } + + $this->eatWhiteDefault = $oldWhite; + if (count($parts) == 0) return false; + + // match comment hack + if (preg_match(self::$whitePattern, + $this->buffer, $m, null, $this->count)) + { + if (!empty($m[0])) { + $parts[] = $m[0]; + $this->count += strlen($m[0]); + } + } + + $this->whitespace(); // get any extra whitespace + + $out = array("string", "", $parts); + return true; + } + + // comma separated list of selectors + protected function selectors(&$out) { + $s = $this->seek(); + $selectors = array(); + while ($this->selector($sel)) { + $selectors[] = $sel; + if (!$this->literal(",")) break; + while ($this->literal(",")); // ignore extra + } + + if (count($selectors) == 0) { + $this->seek($s); + return false; + } + + $out = $selectors; + return true; + } + + // whitespace separated list of selectorSingle + protected function selector(&$out) { + $selector = array(); + + while (true) { + if ($this->match('[>+~]+', $m)) { + $selector[] = array($m[0]); + } elseif ($this->selectorSingle($part)) { + $selector[] = $part; + $this->whitespace(); + } elseif ($this->match('\/[^\/]+\/', $m)) { + $selector[] = array($m[0]); + } else { + break; + } + + } + + if (count($selector) == 0) { + return false; + } + + $out = $selector; + return true; + } + + // the parts that make up + // div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder + protected function selectorSingle(&$out) { + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + $parts = array(); + + if ($this->literal("*", false)) { + $parts[] = "*"; + } + + while (true) { + // see if we can stop early + if ($this->match("\s*[{,]", $m)) { + $this->count--; + break; + } + + $s = $this->seek(); + // self + if ($this->literal("&", false)) { + $parts[] = scssc::$selfSelector; + continue; + } + + if ($this->literal(".", false)) { + $parts[] = "."; + continue; + } + + if ($this->literal("|", false)) { + $parts[] = "|"; + continue; + } + + // for keyframes + if ($this->unit($unit)) { + $parts[] = $unit; + continue; + } + + if ($this->keyword($name)) { + $parts[] = $name; + continue; + } + + if ($this->interpolation($inter)) { + $parts[] = $inter; + continue; + } + + if ($this->literal('%', false) && $this->placeholder($placeholder)) { + $parts[] = '%'; + $parts[] = $placeholder; + continue; + } + + if ($this->literal("#", false)) { + $parts[] = "#"; + continue; + } + + // a pseudo selector + if ($this->match("::?", $m) && $this->mixedKeyword($nameParts)) { + $parts[] = $m[0]; + foreach ($nameParts as $sub) { + $parts[] = $sub; + } + + $ss = $this->seek(); + if ($this->literal("(") && + ($this->openString(")", $str, "(") || true ) && + $this->literal(")")) + { + $parts[] = "("; + if (!empty($str)) $parts[] = $str; + $parts[] = ")"; + } else { + $this->seek($ss); + } + + continue; + } else { + $this->seek($s); + } + + // attribute selector + // TODO: replace with open string? + if ($this->literal("[", false)) { + $attrParts = array("["); + // keyword, string, operator + while (true) { + if ($this->literal("]", false)) { + $this->count--; + break; // get out early + } + + if ($this->match('\s+', $m)) { + $attrParts[] = " "; + continue; + } + if ($this->string($str)) { + $attrParts[] = $str; + continue; + } + + if ($this->keyword($word)) { + $attrParts[] = $word; + continue; + } + + if ($this->interpolation($inter, false)) { + $attrParts[] = $inter; + continue; + } + + // operator, handles attr namespace too + if ($this->match('[|-~\$\*\^=]+', $m)) { + $attrParts[] = $m[0]; + continue; + } + + break; + } + + if ($this->literal("]", false)) { + $attrParts[] = "]"; + foreach ($attrParts as $part) { + $parts[] = $part; + } + continue; + } + $this->seek($s); + // should just break here? + } + + break; + } + + $this->eatWhiteDefault = $oldWhite; + + if (count($parts) == 0) return false; + + $out = $parts; + return true; + } + + protected function variable(&$out) { + $s = $this->seek(); + if ($this->literal("$", false) && $this->keyword($name)) { + $out = array("var", $name); + return true; + } + $this->seek($s); + return false; + } + + protected function keyword(&$word, $eatWhitespace = null) { + if ($this->match('([\w_\-\*!"\'\\\\][\w\-_"\'\\\\]*)', + $m, $eatWhitespace)) + { + $word = $m[1]; + return true; + } + return false; + } + + protected function placeholder(&$placeholder) { + if ($this->match('([\w\-_]+)', $m)) { + $placeholder = $m[1]; + return true; + } + return false; + } + + // consume an end of statement delimiter + protected function end() { + if ($this->literal(';')) { + return true; + } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') { + // if there is end of file or a closing block next then we don't need a ; + return true; + } + return false; + } + + // advance counter to next occurrence of $what + // $until - don't include $what in advance + // $allowNewline, if string, will be used as valid char set + protected function to($what, &$out, $until = false, $allowNewline = false) { + if (is_string($allowNewline)) { + $validChars = $allowNewline; + } else { + $validChars = $allowNewline ? "." : "[^\n]"; + } + if (!$this->match('('.$validChars.'*?)'.$this->preg_quote($what), $m, !$until)) return false; + if ($until) $this->count -= strlen($what); // give back $what + $out = $m[1]; + return true; + } + + public function throwParseError($msg = "parse error", $count = null) { + $count = !isset($count) ? $this->count : $count; + + $line = $this->getLineNo($count); + + if (!empty($this->sourceName)) { + $loc = "$this->sourceName on line $line"; + } else { + $loc = "line: $line"; + } + + if ($this->peek("(.*?)(\n|$)", $m, $count)) { + throw new Exception("$msg: failed at `$m[1]` $loc"); + } else { + throw new Exception("$msg: $loc"); + } + } + + public function getLineNo($pos) { + return 1 + substr_count(substr($this->buffer, 0, $pos), "\n"); + } + + /** + * Match string looking for either ending delim, escape, or string interpolation + * + * {@internal This is a workaround for preg_match's 250K string match limit. }} + * + * @param array $m Matches (passed by reference) + * @param string $delim Delimeter + * + * @return boolean True if match; false otherwise + */ + protected function matchString(&$m, $delim) { + $token = null; + + $end = strpos($this->buffer, "\n", $this->count); + if ($end === false || $this->buffer[$end - 1] == '\\' || $this->buffer[$end - 2] == '\\' && $this->buffer[$end - 1] == "\r") { + $end = strlen($this->buffer); + } + + // look for either ending delim, escape, or string interpolation + foreach (array('#{', '\\', $delim) as $lookahead) { + $pos = strpos($this->buffer, $lookahead, $this->count); + if ($pos !== false && $pos < $end) { + $end = $pos; + $token = $lookahead; + } + } + + if (!isset($token)) { + return false; + } + + $match = substr($this->buffer, $this->count, $end - $this->count); + $m = array( + $match . $token, + $match, + $token + ); + $this->count = $end + strlen($token); + + return true; + } + + // try to match something on head of buffer + protected function match($regex, &$out, $eatWhitespace = null) { + if (!isset($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault; + + $r = '/'.$regex.'/Ais'; + if (preg_match($r, $this->buffer, $out, null, $this->count)) { + $this->count += strlen($out[0]); + if ($eatWhitespace) $this->whitespace(); + return true; + } + return false; + } + + // match some whitespace + protected function whitespace() { + $gotWhite = false; + while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) { + if ($this->insertComments) { + if (isset($m[1]) && empty($this->commentsSeen[$this->count])) { + $this->append(array("comment", $m[1])); + $this->commentsSeen[$this->count] = true; + } + } + $this->count += strlen($m[0]); + $gotWhite = true; + } + return $gotWhite; + } + + protected function peek($regex, &$out, $from=null) { + if (!isset($from)) $from = $this->count; + + $r = '/'.$regex.'/Ais'; + $result = preg_match($r, $this->buffer, $out, null, $from); + + return $result; + } + + protected function seek($where = null) { + if ($where === null) return $this->count; + else $this->count = $where; + return true; + } + + static function preg_quote($what) { + return preg_quote($what, '/'); + } + + protected function show() { + if ($this->peek("(.*?)(\n|$)", $m, $this->count)) { + return $m[1]; + } + return ""; + } + + // turn list of length 1 into value type + protected function flattenList($value) { + if ($value[0] == "list" && count($value[2]) == 1) { + return $this->flattenList($value[2][0]); + } + return $value; + } +} + +/** + * SCSS base formatter + * + * @author Leaf Corcoran <leafot@gmail.com> + */ +class scss_formatter { + public $indentChar = " "; + + public $break = "\n"; + public $open = " {"; + public $close = "}"; + public $tagSeparator = ", "; + public $assignSeparator = ": "; + + public function __construct() { + $this->indentLevel = 0; + } + + public function indentStr($n = 0) { + return str_repeat($this->indentChar, max($this->indentLevel + $n, 0)); + } + + public function property($name, $value) { + return $name . $this->assignSeparator . $value . ";"; + } + + protected function block($block) { + if (empty($block->lines) && empty($block->children)) return; + + $inner = $pre = $this->indentStr(); + + if (!empty($block->selectors)) { + echo $pre . + implode($this->tagSeparator, $block->selectors) . + $this->open . $this->break; + $this->indentLevel++; + $inner = $this->indentStr(); + } + + if (!empty($block->lines)) { + $glue = $this->break.$inner; + echo $inner . implode($glue, $block->lines); + if (!empty($block->children)) { + echo $this->break; + } + } + + foreach ($block->children as $child) { + $this->block($child); + } + + if (!empty($block->selectors)) { + $this->indentLevel--; + if (empty($block->children)) echo $this->break; + echo $pre . $this->close . $this->break; + } + } + + public function format($block) { + ob_start(); + $this->block($block); + $out = ob_get_clean(); + + return $out; + } +} + +/** + * SCSS nested formatter + * + * @author Leaf Corcoran <leafot@gmail.com> + */ +class scss_formatter_nested extends scss_formatter { + public $close = " }"; + + // adjust the depths of all children, depth first + public function adjustAllChildren($block) { + // flatten empty nested blocks + $children = array(); + foreach ($block->children as $i => $child) { + if (empty($child->lines) && empty($child->children)) { + if (isset($block->children[$i + 1])) { + $block->children[$i + 1]->depth = $child->depth; + } + continue; + } + $children[] = $child; + } + + $count = count($children); + for ($i = 0; $i < $count; $i++) { + $depth = $children[$i]->depth; + $j = $i + 1; + if (isset($children[$j]) && $depth < $children[$j]->depth) { + $childDepth = $children[$j]->depth; + for (; $j < $count; $j++) { + if ($depth < $children[$j]->depth && $childDepth >= $children[$j]->depth) { + $children[$j]->depth = $depth + 1; + } + } + } + } + + $block->children = $children; + + // make relative to parent + foreach ($block->children as $child) { + $this->adjustAllChildren($child); + $child->depth = $child->depth - $block->depth; + } + } + + protected function block($block) { + if ($block->type == "root") { + $this->adjustAllChildren($block); + } + + $inner = $pre = $this->indentStr($block->depth - 1); + if (!empty($block->selectors)) { + echo $pre . + implode($this->tagSeparator, $block->selectors) . + $this->open . $this->break; + $this->indentLevel++; + $inner = $this->indentStr($block->depth - 1); + } + + if (!empty($block->lines)) { + $glue = $this->break.$inner; + echo $inner . implode($glue, $block->lines); + if (!empty($block->children)) echo $this->break; + } + + foreach ($block->children as $i => $child) { + // echo "*** block: ".$block->depth." child: ".$child->depth."\n"; + $this->block($child); + if ($i < count($block->children) - 1) { + echo $this->break; + + if (isset($block->children[$i + 1])) { + $next = $block->children[$i + 1]; + if ($next->depth == max($block->depth, 1) && $child->depth >= $next->depth) { + echo $this->break; + } + } + } + } + + if (!empty($block->selectors)) { + $this->indentLevel--; + echo $this->close; + } + + if ($block->type == "root") { + echo $this->break; + } + } +} + +/** + * SCSS compressed formatter + * + * @author Leaf Corcoran <leafot@gmail.com> + */ +class scss_formatter_compressed extends scss_formatter { + public $open = "{"; + public $tagSeparator = ","; + public $assignSeparator = ":"; + public $break = ""; + + public function indentStr($n = 0) { + return ""; + } +} + +/** + * SCSS server + * + * @author Leaf Corcoran <leafot@gmail.com> + */ +class scss_server { + /** + * Join path components + * + * @param string $left Path component, left of the directory separator + * @param string $right Path component, right of the directory separator + * + * @return string + */ + protected function join($left, $right) { + return rtrim($left, '/\\') . DIRECTORY_SEPARATOR . ltrim($right, '/\\'); + } + + /** + * Get name of requested .scss file + * + * @return string|null + */ + protected function inputName() { + switch (true) { + case isset($_GET['p']): + return $_GET['p']; + case isset($_SERVER['PATH_INFO']): + return $_SERVER['PATH_INFO']; + case isset($_SERVER['DOCUMENT_URI']): + return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME'])); + } + } + + /** + * Get path to requested .scss file + * + * @return string + */ + protected function findInput() { + if (($input = $this->inputName()) + && strpos($input, '..') === false + && substr($input, -5) === '.scss' + ) { + $name = $this->join($this->dir, $input); + + if (is_file($name) && is_readable($name)) { + return $name; + } + } + + return false; + } + + /** + * Get path to cached .css file + * + * @return string + */ + protected function cacheName($fname) { + return $this->join($this->cacheDir, md5($fname) . '.css'); + } + + /** + * Get path to cached imports + * + * @return string + */ + protected function importsCacheName($out) { + return $out . '.imports'; + } + + /** + * Determine whether .scss file needs to be re-compiled. + * + * @param string $in Input path + * @param string $out Output path + * + * @return boolean True if compile required. + */ + protected function needsCompile($in, $out) { + if (!is_file($out)) return true; + + $mtime = filemtime($out); + if (filemtime($in) > $mtime) return true; + + // look for modified imports + $icache = $this->importsCacheName($out); + if (is_readable($icache)) { + $imports = unserialize(file_get_contents($icache)); + foreach ($imports as $import) { + if (filemtime($import) > $mtime) return true; + } + } + return false; + } + + /** + * Get If-Modified-Since header from client request + * + * @return string + */ + protected function getModifiedSinceHeader() + { + $modifiedSince = ''; + + if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { + $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE']; + + if (false !== ($semicolonPos = strpos($modifiedSince, ';'))) { + $modifiedSince = substr($modifiedSince, 0, $semicolonPos); + } + } + + return $modifiedSince; + } + + /** + * Compile .scss file + * + * @param string $in Input path (.scss) + * @param string $out Output path (.css) + * + * @return string + */ + protected function compile($in, $out) { + $start = microtime(true); + $css = $this->scss->compile(file_get_contents($in), $in); + $elapsed = round((microtime(true) - $start), 4); + + $v = scssc::$VERSION; + $t = @date('r'); + $css = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css; + + file_put_contents($out, $css); + file_put_contents($this->importsCacheName($out), + serialize($this->scss->getParsedFiles())); + return $css; + } + + /** + * Compile requested scss and serve css. Outputs HTTP response. + * + * @param string $salt Prefix a string to the filename for creating the cache name hash + */ + public function serve($salt = '') { + $protocol = isset($_SERVER['SERVER_PROTOCOL']) + ? $_SERVER['SERVER_PROTOCOL'] + : 'HTTP/1.0'; + + if ($input = $this->findInput()) { + $output = $this->cacheName($salt . $input); + + if ($this->needsCompile($input, $output)) { + try { + $css = $this->compile($input, $output); + + $lastModified = gmdate('D, d M Y H:i:s', filemtime($output)) . ' GMT'; + + header('Last-Modified: ' . $lastModified); + header('Content-type: text/css'); + + echo $css; + + return; + } catch (Exception $e) { + header($protocol . ' 500 Internal Server Error'); + header('Content-type: text/plain'); + + echo 'Parse error: ' . $e->getMessage() . "\n"; + } + } + + header('X-SCSS-Cache: true'); + header('Content-type: text/css'); + + $modifiedSince = $this->getModifiedSinceHeader(); + $mtime = filemtime($output); + + if (@strtotime($modifiedSince) === $mtime) { + header($protocol . ' 304 Not Modified'); + + return; + } + + $lastModified = gmdate('D, d M Y H:i:s', $mtime) . ' GMT'; + header('Last-Modified: ' . $lastModified); + + echo file_get_contents($output); + + return; + } + + header($protocol . ' 404 Not Found'); + header('Content-type: text/plain'); + + $v = scssc::$VERSION; + echo "/* INPUT NOT FOUND scss $v */\n"; + } + + /** + * Constructor + * + * @param string $dir Root directory to .scss files + * @param string $cacheDir Cache directory + * @param \scssc|null $scss SCSS compiler instance + */ + public function __construct($dir, $cacheDir=null, $scss=null) { + $this->dir = $dir; + + if (!isset($cacheDir)) { + $cacheDir = $this->join($dir, 'scss_cache'); + } + + $this->cacheDir = $cacheDir; + if (!is_dir($this->cacheDir)) mkdir($this->cacheDir, 0755, true); + + if (!isset($scss)) { + $scss = new scssc(); + $scss->setImportPaths($this->dir); + } + $this->scss = $scss; + } + + /** + * Helper method to serve compiled scss + * + * @param string $path Root path + */ + static public function serveFrom($path) { + $server = new self($path); + $server->serve(); + } +} |