public function compile($code, $path = null)
{
if ($this->cache) {
- $cacheKey = ($path ? $path : "(stdin)") . ":" . md5($code);
+ $cacheKey = ($path ? $path : "(stdin)") . ":" . md5($code);
$compileOptions = $this->getCompileOptions();
- $cache = $this->cache->getCache("compile", $cacheKey, $compileOptions);
+ $cache = $this->cache->getCache("compile", $cacheKey, $compileOptions);
- if (is_array($cache)
- && isset($cache['dependencies'])
- && isset($cache['out'])
- ) {
+ if (is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) {
// check if any dependency file changed before accepting the cache
foreach ($cache['dependencies'] as $file => $mtime) {
- if (! file_exists($file)
- || filemtime($file) !== $mtime
- ) {
+ if (! file_exists($file) || filemtime($file) !== $mtime) {
unset($cache);
break;
}
$this->stderr = fopen('php://stderr', 'w');
$this->parser = $this->parserFactory($path);
- $tree = $this->parser->parse($code);
+ $tree = $this->parser->parse($code);
$this->parser = null;
$this->formatter = new $this->formatter();
} else {
// a selector part finishing with a ) is the last part of a :not( or :nth-child(
// and need to be joined to this
- if (count($new) && is_string($new[count($new) - 1])
- && strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
+ if (count($new) && is_string($new[count($new) - 1]) &&
+ strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
) {
$new[count($new) - 1] .= $part;
} else {
$selector = $this->glueFunctionSelectors($selector);
+ if (count($selector) == 1 && in_array(reset($selector), $partsPile)) {
+ return;
+ }
+
foreach ($selector as $i => $part) {
if ($i < $from) {
continue;
{
$env = $this->pushEnv($block);
$envs = $this->compactEnv($env);
- $without = isset($block->with) ? $this->compileWith($block->with) : static::WITH_RULE;
+ list($with, $without) = $this->compileWith(isset($block->with) ? $block->with : null);
// wrap inline selector
if ($block->selector) {
$selfParent = $block->parent;
}
- $this->env = $this->filterWithout($envs, $without);
+ $this->env = $this->filterWithWithout($envs, $with, $without);
$saveScope = $this->scope;
- $this->scope = $this->filterScopeWithout($saveScope, $without);
+ $this->scope = $this->filterScopeWithWithout($saveScope, $with, $without);
// propagate selfParent to the children where they still can be useful
$this->compileChildrenNoReturn($block->children, $this->scope, $selfParent);
* Filter at-root scope depending of with/without option
*
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
- * @param mixed $without
+ * @param array $with
+ * @param array $without
*
* @return mixed
*/
- protected function filterScopeWithout($scope, $without)
+ protected function filterScopeWithWithout($scope, $with, $without)
{
$filteredScopes = [];
break;
}
- if (! $this->isWithout($without, $scope)) {
+ if ($this->isWith($scope, $with, $without)) {
$s = clone $scope;
$s->children = [];
$s->lines = [];
}
/**
- * Compile @at-root's with: inclusion / without: exclusion into filter flags
+ * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later
*
- * @param array $with
+ * @param array $withCondition
*
- * @return integer
+ * @return array
*/
- protected function compileWith($with)
+ protected function compileWith($withCondition)
{
- static $mapping = [
- 'rule' => self::WITH_RULE,
- 'media' => self::WITH_MEDIA,
- 'supports' => self::WITH_SUPPORTS,
- 'all' => self::WITH_ALL,
- ];
-
- // exclude selectors by default
- $without = static::WITH_RULE;
+ // just compile what we have in 2 lists
+ $with = [];
+ $without = ['rule' => true];
- if ($this->libMapHasKey([$with, static::$with])) {
- $without = static::WITH_ALL;
+ if ($withCondition) {
+ if ($this->libMapHasKey([$withCondition, static::$with])) {
+ $without = []; // cancel the default
+ $list = $this->coerceList($this->libMapGet([$withCondition, static::$with]));
- $list = $this->coerceList($this->libMapGet([$with, static::$with]));
-
- foreach ($list[2] as $item) {
- $keyword = $this->compileStringContent($this->coerceString($item));
+ foreach ($list[2] as $item) {
+ $keyword = $this->compileStringContent($this->coerceString($item));
- if (array_key_exists($keyword, $mapping)) {
- $without &= ~($mapping[$keyword]);
+ $with[$keyword] = true;
}
}
- }
-
- if ($this->libMapHasKey([$with, static::$without])) {
- $without = 0;
- $list = $this->coerceList($this->libMapGet([$with, static::$without]));
+ if ($this->libMapHasKey([$withCondition, static::$without])) {
+ $without = []; // cancel the default
+ $list = $this->coerceList($this->libMapGet([$withCondition, static::$without]));
- foreach ($list[2] as $item) {
- $keyword = $this->compileStringContent($this->coerceString($item));
+ foreach ($list[2] as $item) {
+ $keyword = $this->compileStringContent($this->coerceString($item));
- if (array_key_exists($keyword, $mapping)) {
- $without |= $mapping[$keyword];
+ $without[$keyword] = true;
}
}
}
- return $without;
+ return [$with, $without];
}
/**
* Filter env stack
*
* @param array $envs
- * @param integer $without
+ * @param array $with
+ * @param array $without
*
* @return \ScssPhp\ScssPhp\Compiler\Environment
*/
- protected function filterWithout($envs, $without)
+ protected function filterWithWithout($envs, $with, $without)
{
$filtered = [];
foreach ($envs as $e) {
- if ($e->block && $this->isWithout($without, $e->block)) {
+ if ($e->block && ! $this->isWith($e->block, $with, $without)) {
$ec = clone $e;
$ec->block = null;
$ec->selectors = [];
/**
* Filter WITH rules
*
- * @param integer $without
* @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block
+ * @param array $with
+ * @param array $without
*
* @return boolean
*/
- protected function isWithout($without, $block)
+ protected function isWith($block, $with, $without)
{
if (isset($block->type)) {
if ($block->type === Type::T_MEDIA) {
- return ($without & static::WITH_MEDIA) ? true : false;
+ return $this->testWithWithout('media', $with, $without);
}
if ($block->type === Type::T_DIRECTIVE) {
- if (isset($block->name) && $block->name === 'supports') {
- return ($without & static::WITH_SUPPORTS) ? true : false;
+ if (isset($block->name)) {
+ return $this->testWithWithout($block->name, $with, $without);
}
-
- if (isset($block->selectors) && strpos(serialize($block->selectors), '@supports') !== false) {
- return ($without & static::WITH_SUPPORTS) ? true : false;
+ elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
+ return $this->testWithWithout($m[1], $with, $without);
+ }
+ else {
+ return $this->testWithWithout('???', $with, $without);
}
}
}
+ elseif (isset($block->selectors)) {
+ return $this->testWithWithout('rule', $with, $without);
+ }
- if ((($without & static::WITH_RULE) && isset($block->selectors))) {
- return true;
+ return true;
+ }
+
+ /**
+ * Test a single type of block against with/without lists
+ *
+ * @param string $what
+ * @param array $with
+ * @param array $without
+ * @return bool
+ * true if the block should be kept, false to reject
+ */
+ protected function testWithWithout($what, $with, $without) {
+
+ // if without, reject only if in the list (or 'all' is in the list)
+ if (count($without)) {
+ return (isset($without[$what]) || isset($without['all'])) ? false : true;
}
- return false;
+ // otherwise reject all what is not in the with list
+ return (isset($with[$what]) || isset($with['all'])) ? true : false;
}
+
/**
* Compile keyframe block
*
$this->popEnv();
}
+ /**
+ * Compile nested properties lines
+ *
+ * @param \ScssPhp\ScssPhp\Block $block
+ * @param OutputBlock $out
+ */
+ protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out)
+ {
+ $prefix = $this->compileValue($block->prefix) . '-';
+
+ $nested = $this->makeOutputBlock($block->type);
+ $nested->parent = $out;
+
+ if ($block->hasValue) {
+ $nested->depth = $out->depth + 1;
+ }
+
+ $out->children[] = $nested;
+
+ foreach ($block->children as $child) {
+ switch ($child[0]) {
+ case Type::T_ASSIGN:
+ array_unshift($child[1][2], $prefix);
+ break;
+
+ case Type::T_NESTED_PROPERTY:
+ array_unshift($child[1]->prefix[2], $prefix);
+ break;
+ }
+ $this->compileChild($child, $nested);
+ }
+ }
+
/**
* Compile nested block
*
*/
protected function evaluateMediaQuery($queryList)
{
+ static $parser = null;
+ $outQueryList = [];
foreach ($queryList as $kql => $query) {
+ $shouldReparse = false;
foreach ($query as $kq => $q) {
for ($i = 1; $i < count($q); $i++) {
$value = $this->compileValue($q[$i]);
// the parser had no mean to know if media type or expression if it was an interpolation
+ // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type
if ($q[0] == Type::T_MEDIA_TYPE &&
(strpos($value, '(') !== false ||
strpos($value, ')') !== false ||
- strpos($value, ':') !== false)
+ strpos($value, ':') !== false ||
+ strpos($value, ',') !== false)
) {
- $queryList[$kql][$kq][0] = Type::T_MEDIA_EXPRESSION;
-
- if (strpos($value, 'and') !== false) {
- $values = explode('and', $value);
- $value = trim(array_pop($values));
-
- while ($v = trim(array_pop($values))) {
- $type = Type::T_MEDIA_EXPRESSION;
-
- if (strpos($v, '(') === false &&
- strpos($v, ')') === false &&
- strpos($v, ':') === false
- ) {
- $type = Type::T_MEDIA_TYPE;
- }
-
- if (substr($v, 0, 1) === '(' && substr($v, -1) === ')') {
- $v = substr($v, 1, -1);
- }
-
- $queryList[$kql][] = [$type,[Type::T_KEYWORD, $v]];
- }
- }
-
- if (substr($value, 0, 1) === '(' && substr($value, -1) === ')') {
- $value = substr($value, 1, -1);
- }
+ $shouldReparse = true;
}
$queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value];
}
}
+ if ($shouldReparse) {
+ if (is_null($parser)) {
+ $parser = $this->parserFactory(__METHOD__);
+ }
+ $queryString = $this->compileMediaQuery([$queryList[$kql]]);
+ $queryString = reset($queryString);
+ if (strpos($queryString, '@media ') === 0) {
+ $queryString = substr($queryString, 7);
+ $queries = [];
+ if ($parser->parseMediaQueryList($queryString, $queries)) {
+ $queries = $this->evaluateMediaQuery($queries[2]);
+ while (count($queries)) {
+ $outQueryList[] = array_shift($queries);
+ }
+ continue;
+ }
+ }
+ }
+ $outQueryList[] = $queryList[$kql];
}
- return $queryList;
+ return $outQueryList;
}
/**
return false;
}
+
+ /**
+ * Append a root directive like @import or @charset as near as the possible from the source code
+ * (keeping before comments, @import and @charset coming before in the source code)
+ *
+ * @param string $line
+ * @param @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
+ * @param array $allowed
+ */
+ protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT])
+ {
+ $root = $out;
+
+ while ($root->parent) {
+ $root = $root->parent;
+ }
+
+ $i = 0;
+
+ while ($i < count($root->children)) {
+ if (! isset($root->children[$i]->type) || ! in_array($root->children[$i]->type, $allowed)) {
+ break;
+ }
+
+ $i++;
+ }
+
+ // remove incompatible children from the bottom of the list
+ $saveChildren = [];
+
+ while ($i < count($root->children)) {
+ $saveChildren[] = array_pop($root->children);
+ }
+
+ // insert the directive as a comment
+ $child = $this->makeOutputBlock(Type::T_COMMENT);
+ $child->lines[] = $line;
+ $child->sourceName = $this->sourceNames[$this->sourceIndex];
+ $child->sourceLine = $this->sourceLine;
+ $child->sourceColumn = $this->sourceColumn;
+
+ $root->children[] = $child;
+
+ // repush children
+ while (count($saveChildren)) {
+ $root->children[] = array_pop($saveChildren);
+ }
+ }
+
+ /**
+ * Append lines to the courrent output block:
+ * directly to the block or through a child if necessary
+ *
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
+ * @param string $type
+ * @param string $line
+ */
+ protected function appendOutputLine(OutputBlock $out, $type, $line)
+ {
+ $outWrite = &$out;
+
+ if ($type === Type::T_COMMENT) {
+ $parent = $out->parent;
+
+ if (end($parent->children) !== $out) {
+ $outWrite = &$parent->children[count($parent->children)-1];
+ }
+ }
+
+ // check if it's a flat output or not
+ if (count($out->children)) {
+ $lastChild = &$out->children[count($out->children) -1];
+
+ if ($lastChild->depth === $out->depth && is_null($lastChild->selectors) && ! count($lastChild->children)) {
+ $outWrite = $lastChild;
+ } else {
+ $nextLines = $this->makeOutputBlock($type);
+ $nextLines->parent = $out;
+ $nextLines->depth = $out->depth;
+
+ $out->children[] = $nextLines;
+ $outWrite = &$nextLines;
+ }
+ }
+
+ $outWrite->lines[] = $line;
+ }
+
/**
* Compile child; returns a value to halt execution
*
$rawPath = $this->reduce($child[1]);
if (! $this->compileImport($rawPath, $out, true)) {
- $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
+ $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
}
break;
$rawPath = $this->reduce($child[1]);
if (! $this->compileImport($rawPath, $out)) {
- $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
+ $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
}
break;
case Type::T_CHARSET:
if (! $this->charsetSeen) {
$this->charsetSeen = true;
-
- $out->lines[] = '@charset ' . $this->compileValue($child[1]) . ';';
+ $this->appendRootDirective('@charset ' . $this->compileValue($child[1]) . ';', $out);
}
break;
}
$shouldSet = $isDefault &&
- (($result = $this->get($name[1], false)) === null
- || $result === static::$null);
+ (($result = $this->get($name[1], false)) === null ||
+ $result === static::$null);
if (! $isDefault || $shouldSet) {
$this->set($name[1], $this->reduce($value), true, null, $value);
$compiledValue = $this->compileValue($value);
- $out->lines[] = $this->formatter->property(
+ $line = $this->formatter->property(
$compiledName,
$compiledValue
);
+ $this->appendOutputLine($out, Type::T_ASSIGN, $line);
break;
case Type::T_COMMENT:
break;
}
- $out->lines[] = $child[1];
+ $this->appendOutputLine($out, Type::T_COMMENT, $child[1]);
break;
case Type::T_MIXIN:
case Type::T_FUNCTION:
list(, $block) = $child;
- $this->set(static::$namespaces[$block->type] . $block->name, $block);
+ $this->set(static::$namespaces[$block->type] . $block->name, $block, true);
break;
case Type::T_EXTEND:
return $this->reduce($child[1], true);
case Type::T_NESTED_PROPERTY:
- list(, $prop) = $child;
-
- $prefixed = [];
- $prefix = $this->compileValue($prop->prefix) . '-';
-
- foreach ($prop->children as $child) {
- switch ($child[0]) {
- case Type::T_ASSIGN:
- array_unshift($child[1][2], $prefix);
- break;
-
- case Type::T_NESTED_PROPERTY:
- array_unshift($child[1]->prefix[2], $prefix);
- break;
- }
-
- $prefixed[] = $child;
- }
-
- $this->compileChildrenNoReturn($prefixed, $out);
+ $this->compileNestedPropertiesBlock($child[1], $out);
break;
case Type::T_INCLUDE:
$copyContent->scope = $callingScope;
$this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env);
+ } else {
+ $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env);
}
if (isset($mixin->args)) {
if (! $content) {
$content = new \stdClass();
$content->scope = new \stdClass();
- $content->children = $this->storeEnv->parent->block->children;
+ $content->children = $env->parent->block->children;
break;
}
}
// special case: looks like css shorthand
- if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2])
- && (($right[0] !== Type::T_NUMBER && $right[2] != '')
- || ($right[0] === Type::T_NUMBER && ! $right->unitless()))
+ if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2]) &&
+ (($right[0] !== Type::T_NUMBER && $right[2] != '') ||
+ ($right[0] === Type::T_NUMBER && ! $right->unitless()))
) {
return $this->expToString($value);
}
return 'null';
default:
- $this->throwError("unknown value type: $value[0]");
+ $this->throwError("unknown value type: ".json_encode($value));
}
}
*
* @return array
*/
- protected function sortArgs($prototype, $args)
+ protected function sortArgs($prototypes, $args)
{
- $keyArgs = [];
- $posArgs = [];
+ static $parser = null;
- // separate positional and keyword arguments
- foreach ($args as $arg) {
- list($key, $value) = $arg;
+ if (! isset($prototypes)) {
+ $keyArgs = [];
+ $posArgs = [];
- $key = $key[1];
+ // separate positional and keyword arguments
+ foreach ($args as $arg) {
+ list($key, $value) = $arg;
- if (empty($key)) {
- $posArgs[] = empty($arg[2]) ? $value : $arg;
- } else {
- $keyArgs[$key] = $value;
+ $key = $key[1];
+
+ if (empty($key)) {
+ $posArgs[] = empty($arg[2]) ? $value : $arg;
+ } else {
+ $keyArgs[$key] = $value;
+ }
}
- }
- if (! isset($prototype)) {
return [$posArgs, $keyArgs];
}
- // copy positional args
- $finalArgs = array_pad($posArgs, count($prototype), null);
+ $finalArgs = [];
+
+ if (! is_array(reset($prototypes))) {
+ $prototypes = [$prototypes];
+ }
+
+ $keyArgs = [];
+
+ // trying each prototypes
+ $prototypeHasMatch = false;
+ $exceptionMessage = '';
+
+ foreach ($prototypes as $prototype) {
+ $argDef = [];
+
+ foreach ($prototype as $i => $p) {
+ $default = null;
+ $p = explode(':', $p, 2);
+ $name = array_shift($p);
+
+ if (count($p)) {
+ $p = trim(reset($p));
- // overwrite positional args with keyword args
- foreach ($prototype as $i => $names) {
- foreach ((array) $names as $name) {
- if (isset($keyArgs[$name])) {
- $finalArgs[$i] = $keyArgs[$name];
+ if ($p === 'null') {
+ // differentiate this null from the static::$null
+ $default = [Type::T_KEYWORD, 'null'];
+ } else {
+ if (is_null($parser)) {
+ $parser = $this->parserFactory(__METHOD__);
+ }
+
+ $parser->parseValue($p, $default);
+ }
+ }
+
+ $isVariable = false;
+
+ if (substr($name, -3) === '...') {
+ $isVariable = true;
+ $name = substr($name, 0, -3);
+ }
+
+ $argDef[] = [$name, $default, $isVariable];
+ }
+
+ try {
+ $vars = $this->applyArguments($argDef, $args, false);
+
+ // ensure all args are populated
+ foreach ($prototype as $i => $p) {
+ $name = explode(':', $p)[0];
+
+ if (! isset($finalArgs[$i])) {
+ $finalArgs[$i] = null;
+ }
+ }
+
+ // apply positional args
+ foreach (array_values($vars) as $i => $val) {
+ $finalArgs[$i] = $val;
}
+
+ $keyArgs = array_merge($keyArgs, $vars);
+ $prototypeHasMatch = true;
+
+ // overwrite positional args with keyword args
+ foreach ($prototype as $i => $p) {
+ $name = explode(':', $p)[0];
+
+ if (isset($keyArgs[$name])) {
+ $finalArgs[$i] = $keyArgs[$name];
+ }
+
+ // special null value as default: translate to real null here
+ if ($finalArgs[$i] === [Type::T_KEYWORD, 'null']) {
+ $finalArgs[$i] = null;
+ }
+ }
+ // should we break if this prototype seems fulfilled?
+ } catch (CompilerException $e) {
+ $exceptionMessage = $e->getMessage();
}
}
+ if ($exceptionMessage && ! $prototypeHasMatch) {
+ $this->throwError($exceptionMessage);
+ }
+
return [$finalArgs, $keyArgs];
}
*
* @throws \Exception
*/
- protected function applyArguments($argDef, $argValues)
+ protected function applyArguments($argDef, $argValues, $storeInEnv = true)
{
- $storeEnv = $this->getStoreEnv();
+ $output = [];
- $env = new Environment;
- $env->store = $storeEnv->store;
+ if ($storeInEnv) {
+ $storeEnv = $this->getStoreEnv();
+
+ $env = new Environment;
+ $env->store = $storeEnv->store;
+ }
$hasVariable = false;
$args = [];
$hasVariable |= $isVariable;
}
- $keywordArgs = [];
+ $splatSeparator = null;
+ $keywordArgs = [];
$deferredKeywordArgs = [];
- $remaining = [];
+ $remaining = [];
+ $hasKeywordArgument = false;
// assign the keyword args
foreach ((array) $argValues as $arg) {
if (! empty($arg[0])) {
- if (! isset($args[$arg[0][1]])) {
+ $hasKeywordArgument = true;
+
+ if (! isset($args[$arg[0][1]]) || $args[$arg[0][1]][3]) {
if ($hasVariable) {
$deferredKeywordArgs[$arg[0][1]] = $arg[1];
} else {
} else {
$keywordArgs[$arg[0][1]] = $arg[1];
}
- } elseif (count($keywordArgs)) {
- $this->throwError('Positional arguments must come before keyword arguments.');
- break;
} elseif ($arg[2] === true) {
$val = $this->reduce($arg[1], true);
if ($val[0] === Type::T_LIST) {
foreach ($val[2] as $name => $item) {
if (! is_numeric($name)) {
- $keywordArgs[$name] = $item;
+ if (!isset($args[$name])) {
+ foreach (array_keys($args) as $an) {
+ if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
+ $name = $an;
+ break;
+ }
+ }
+ }
+
+ if ($hasVariable) {
+ $deferredKeywordArgs[$name] = $item;
+ } else {
+ $keywordArgs[$name] = $item;
+ }
} else {
+ if (is_null($splatSeparator)) {
+ $splatSeparator = $val[1];
+ }
$remaining[] = $item;
}
}
$item = $val[2][$i];
if (! is_numeric($name)) {
- $keywordArgs[$name] = $item;
+ if (!isset($args[$name])) {
+ foreach (array_keys($args) as $an) {
+ if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
+ $name = $an;
+ break;
+ }
+ }
+ }
+ if ($hasVariable) {
+ $deferredKeywordArgs[$name] = $item;
+ } else {
+ $keywordArgs[$name] = $item;
+ }
} else {
+ if (is_null($splatSeparator)) {
+ $splatSeparator = $val[1];
+ }
$remaining[] = $item;
}
}
} else {
$remaining[] = $val;
}
+ } elseif ($hasKeywordArgument) {
+ $this->throwError('Positional arguments must come before keyword arguments.');
+ break;
} else {
$remaining[] = $arg[1];
}
list($i, $name, $default, $isVariable) = $arg;
if ($isVariable) {
- $val = [Type::T_LIST, ',', [], $isVariable];
+ $val = [Type::T_LIST, is_null($splatSeparator) ? ',' : $splatSeparator , [], $isVariable];
for ($count = count($remaining); $i < $count; $i++) {
$val[2][] = $remaining[$i];
break;
}
- $this->set($name, $this->reduce($val, true), true, $env);
+ if ($storeInEnv) {
+ $this->set($name, $this->reduce($val, true), true, $env);
+ } else {
+ $output[$name] = $val;
+ }
}
- $storeEnv->store = $env->store;
+ if ($storeInEnv) {
+ $storeEnv->store = $env->store;
+ }
foreach ($args as $arg) {
list($i, $name, $default, $isVariable) = $arg;
continue;
}
- $this->set($name, $this->reduce($default, true), true);
+ if ($storeInEnv) {
+ $this->set($name, $this->reduce($default, true), true);
+ } else {
+ $output[$name] = $default;
+ }
}
+
+ return $output;
}
/**
$key = $keys[$i];
$value = $values[$i];
+ switch ($key[0]) {
+ case Type::T_LIST:
+ case Type::T_MAP:
+ break;
+
+ default:
+ $key = [Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))];
+ break;
+ }
+
$list[] = [
Type::T_LIST,
'',
- [[Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))], $value]
+ [$key, $value]
];
}
// Built in functions
- //protected static $libCall = ['name', 'args...'];
+ protected static $libCall = ['name', 'args...'];
protected function libCall($args, $kwargs)
{
$name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
+ $callArgs = [];
- $posArgs = [];
-
- foreach ($args as $arg) {
- if (empty($arg[0])) {
- if ($arg[2] === true) {
- $tmp = $this->reduce($arg[1]);
-
- if ($tmp[0] === Type::T_LIST) {
- foreach ($tmp[2] as $item) {
- $posArgs[] = [null, $item, false];
- }
- } else {
- $posArgs[] = [null, $tmp, true];
- }
-
- continue;
- }
-
- $posArgs[] = [null, $this->reduce($arg), false];
- continue;
+ // $kwargs['args'] is [Type::T_LIST, ',', [..]]
+ foreach ($kwargs['args'][2] as $varname => $arg) {
+ if (is_numeric($varname)) {
+ $varname = null;
+ } else {
+ $varname = [ 'var', $varname];
}
- $posArgs[] = [null, $arg, false];
+ $callArgs[] = [$varname, $arg, false];
}
- if (count($kwargs)) {
- foreach ($kwargs as $key => $value) {
- $posArgs[] = [[Type::T_VARIABLE, $key], $value, false];
- }
- }
-
- return $this->reduce([Type::T_FUNCTION_CALL, $name, $posArgs]);
+ return $this->reduce([Type::T_FUNCTION_CALL, $name, $callArgs]);
}
- protected static $libIf = ['condition', 'if-true', 'if-false'];
+ protected static $libIf = ['condition', 'if-true', 'if-false:'];
protected function libIf($args)
{
list($cond, $t, $f) = $args;
}
protected static $libRgba = [
- ['red', 'color'],
- 'green', 'blue', 'alpha'];
+ ['color', 'alpha:1'],
+ ['red', 'green', 'blue', 'alpha:1'] ];
protected function libRgba($args)
{
if ($color = $this->coerceColor($args[0])) {
}
}
- if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
+ if (! empty($args[4]) || ! empty($args[5]) || ! empty($args[6])) {
$hsl = $this->toHSL($color[1], $color[2], $color[3]);
foreach ([4, 5, 6] as $i) {
- if (isset($args[$i])) {
+ if (! empty($args[$i])) {
$val = $this->assertNumber($args[$i]);
$hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i);
}
}
protected static $libAdjustColor = [
- 'color', 'red', 'green', 'blue',
- 'hue', 'saturation', 'lightness', 'alpha'
+ 'color', 'red:null', 'green:null', 'blue:null',
+ 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
];
protected function libAdjustColor($args)
{
}
protected static $libChangeColor = [
- 'color', 'red', 'green', 'blue',
- 'hue', 'saturation', 'lightness', 'alpha'
+ 'color', 'red:null', 'green:null', 'blue:null',
+ 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
];
protected function libChangeColor($args)
{
}
protected static $libScaleColor = [
- 'color', 'red', 'green', 'blue',
- 'hue', 'saturation', 'lightness', 'alpha'
+ 'color', 'red:null', 'green:null', 'blue:null',
+ 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
];
protected function libScaleColor($args)
{
}
// mix two colors
- protected static $libMix = ['color-1', 'color-2', 'weight'];
+ protected static $libMix = ['color-1', 'color-2', 'weight:0.5'];
protected function libMix($args)
{
list($first, $second, $weight) = $args;
return $this->adjustHsl($color, 3, -$amount);
}
- protected static $libSaturate = ['color', 'amount'];
+ protected static $libSaturate = [['color', 'amount'], ['number']];
protected function libSaturate($args)
{
$value = $args[0];
protected function libMapGet($args)
{
$map = $this->assertMap($args[0]);
- $key = $this->compileStringContent($this->coerceString($args[1]));
+ $key = $args[1];
- for ($i = count($map[1]) - 1; $i >= 0; $i--) {
- if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
- return $map[2][$i];
+ if (! is_null($key)) {
+ $key = $this->compileStringContent($this->coerceString($key));
+ for ($i = count($map[1]) - 1; $i >= 0; $i--) {
+ if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
+ return $map[2][$i];
+ }
}
}
}
}
- protected static $libJoin = ['list1', 'list2', 'separator'];
+ protected static $libJoin = ['list1', 'list2', 'separator:null'];
protected function libJoin($args)
{
list($list1, $list2, $sep) = $args;
return [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
}
- protected static $libAppend = ['list', 'val', 'separator'];
+ protected static $libAppend = ['list', 'val', 'separator:null'];
protected function libAppend($args)
{
list($list1, $value, $sep) = $args;
return new Node\Number(strlen($stringContent), '');
}
- protected static $libStrSlice = ['string', 'start-at', 'end-at'];
+ protected static $libStrSlice = ['string', 'start-at', 'end-at:null'];
protected function libStrSlice($args)
{
if (isset($args[2]) && $args[2][1] == 0) {
return true;
}
- //protected static $libSelectorAppend = ['selector...'];
+ protected static $libSelectorAppend = ['selector...'];
protected function libSelectorAppend($args)
{
+ // get the selector... list
+ $args = reset($args);
+ $args = $args[2];
if (count($args) < 1) {
$this->throwError("selector-append() needs at least 1 argument");
}
return $extended;
}
- //protected static $libSelectorNest = ['selector...'];
+ protected static $libSelectorNest = ['selector...'];
protected function libSelectorNest($args)
{
+ // get the selector... list
+ $args = reset($args);
+ $args = $args[2];
if (count($args) < 1) {
$this->throwError("selector-nest() needs at least 1 argument");
}
private $env;
private $inParens;
private $eatWhiteDefault;
+ private $discardComments;
private $buffer;
private $utf8;
private $encoding;
$this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8';
$this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais';
$this->commentsSeen = [];
+ $this->discardComments = false;
if (empty(static::$operatorPattern)) {
static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
return $selector;
}
+ /**
+ * Parse a media Query
+ *
+ * @api
+ *
+ * @param string $buffer
+ * @param string $out
+ *
+ * @return array
+ */
+ public function parseMediaQueryList($buffer, &$out)
+ {
+ $this->count = 0;
+ $this->env = null;
+ $this->inParens = false;
+ $this->eatWhiteDefault = true;
+ $this->buffer = (string) $buffer;
+
+ $this->saveEncoding();
+
+
+ $isMediaQuery = $this->mediaQueryList($out);
+
+ $this->restoreEncoding();
+
+ return $isMediaQuery;
+ }
+
/**
* Parse a single chunk off the head of the buffer and append it to the
* current parse environment.
if ($this->literal('@at-root', 8) &&
($this->selectors($selector) || true) &&
($this->map($with) || true) &&
- $this->matchChar('{')
+ $this->matchChar('{', false)
) {
$atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
$atRoot->selector = $selector;
$this->seek($s);
- if ($this->literal('@media', 6) && $this->mediaQueryList($mediaQueryList) && $this->matchChar('{')) {
+ if ($this->literal('@media', 6) && $this->mediaQueryList($mediaQueryList) && $this->matchChar('{', false)) {
$media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
$media->queryList = $mediaQueryList[2];
if ($this->literal('@mixin', 6) &&
$this->keyword($mixinName) &&
($this->argumentDef($args) || true) &&
- $this->matchChar('{')
+ $this->matchChar('{', false)
) {
$mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
$mixin->name = $mixinName;
if ($this->literal('@function', 9) &&
$this->keyword($fnName) &&
$this->argumentDef($args) &&
- $this->matchChar('{')
+ $this->matchChar('{', false)
) {
$func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
$func->name = $fnName;
$this->genericList($varNames, 'variable', ',', false) &&
$this->literal('in', 2) &&
$this->valueList($list) &&
- $this->matchChar('{')
+ $this->matchChar('{', false)
) {
$each = $this->pushSpecialBlock(Type::T_EACH, $s);
if ($this->literal('@while', 6) &&
$this->expression($cond) &&
- $this->matchChar('{')
+ $this->matchChar('{', false)
) {
$while = $this->pushSpecialBlock(Type::T_WHILE, $s);
$while->cond = $cond;
($this->literal('through', 7) ||
($forUntil = true && $this->literal('to', 2))) &&
$this->expression($end) &&
- $this->matchChar('{')
+ $this->matchChar('{', false)
) {
$for = $this->pushSpecialBlock(Type::T_FOR, $s);
$for->var = $varName[1];
$this->seek($s);
- if ($this->literal('@if', 3) && $this->valueList($cond) && $this->matchChar('{')) {
+ if ($this->literal('@if', 3) && $this->valueList($cond) && $this->matchChar('{', false)) {
$if = $this->pushSpecialBlock(Type::T_IF, $s);
$if->cond = $cond;
$if->cases = [];
list(, $if) = $last;
if ($this->literal('@else', 5)) {
- if ($this->matchChar('{')) {
+ if ($this->matchChar('{', false)) {
$else = $this->pushSpecialBlock(Type::T_ELSE, $s);
- } elseif ($this->literal('if', 2) && $this->valueList($cond) && $this->matchChar('{')) {
+ } elseif ($this->literal('if', 2) && $this->valueList($cond) && $this->matchChar('{', false)) {
$else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
$else->cond = $cond;
}
$this->seek($s);
+ if ($this->literal('@supports', 9) &&
+ ($t1=$this->supportsQuery($supportQuery)) &&
+ ($t2=$this->matchChar('{', false)) ) {
+ $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
+ $directive->name = 'supports';
+ $directive->value = $supportQuery;
+
+ return true;
+ }
+
+ $this->seek($s);
+
// doesn't match built in directive, do generic one
if ($this->matchChar('@', false) &&
$this->keyword($dirName) &&
($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
- $this->matchChar('{')
+ $this->matchChar('{', false)
) {
if ($dirName === 'media') {
$directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
$foundSomething = true;
}
- if ($this->matchChar('{')) {
+ if ($this->matchChar('{', false)) {
$propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
$propBlock->prefix = $name;
+ $propBlock->hasValue = $foundSomething;
+
$foundSomething = true;
} elseif ($foundSomething) {
$foundSomething = $this->end();
$this->seek($s);
// closing a block
- if ($this->matchChar('}')) {
+ if ($this->matchChar('}', false)) {
$block = $this->popBlock();
+ if (!isset($block->type) || $block->type !== Type::T_IF) {
+ if ($this->env->parent) {
+ $this->append(null); // collect comments before next statement if needed
+ }
+ }
+
if (isset($block->type) && $block->type === Type::T_INCLUDE) {
$include = $block->child;
unset($block->child);
$this->append([$type, $block], $s);
}
+ // collect comments just after the block closing if needed
+ if ($this->eatWhiteDefault) {
+ $this->whitespace();
+
+ if ($this->env->comments) {
+ $this->append(null);
+ }
+ }
+
return true;
}
$this->env = $b;
+ // collect comments at the begining of a block if needed
+ if ($this->eatWhiteDefault) {
+ $this->whitespace();
+
+ if ($this->env->comments) {
+ $this->append(null);
+ }
+ }
+
return $b;
}
*/
protected function popBlock()
{
+
+ // collect comments ending just before of a block closing
+ if ($this->env->comments) {
+ $this->append(null);
+ }
+
+ // pop the block
$block = $this->env;
if (empty($block->parent)) {
unset($block->parent);
- $comments = $block->comments;
-
- if ($comments) {
- $this->env->comments = $comments;
- unset($block->comments);
- }
-
return $block;
}
*/
protected function appendComment($comment)
{
- if ($comment[0] === Type::T_COMMENT && is_string($comment[1])) {
- $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
- }
+ if (! $this->discardComments) {
+ if ($comment[0] === Type::T_COMMENT && is_string($comment[1])) {
+ $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
+ }
- $this->env->comments[] = $comment;
+ $this->env->comments[] = $comment;
+ }
}
/**
return true;
}
+ /**
+ * Parse supports query
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function supportsQuery(&$out)
+ {
+ $expressions = null;
+ $parts = [];
+
+ $s = $this->count;
+
+ $not = false;
+ if (($this->literal('not', 3) && ($not = true) || true) &&
+ $this->matchChar('(') &&
+ ($this->expression($property)) &&
+ $this->literal(': ', 2) &&
+ $this->valueList($value) &&
+ $this->matchChar(')')) {
+ $support = [Type::T_STRING, '', [[Type::T_KEYWORD, ($not ? 'not ' : '') . '(']]];
+ $support[2][] = $property;
+ $support[2][] = [Type::T_KEYWORD, ': '];
+ $support[2][] = $value;
+ $support[2][] = [Type::T_KEYWORD, ')'];
+
+ $parts[] = $support;
+ $s = $this->count;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->matchChar('(') &&
+ $this->supportsQuery($subQuery) &&
+ $this->matchChar(')')) {
+ $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, '('], $subQuery, [Type::T_KEYWORD, ')']]];
+ $s = $this->count;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->literal('not', 3) &&
+ $this->supportsQuery($subQuery)) {
+ $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, 'not '], $subQuery]];
+ $s = $this->count;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->literal('selector(', 9) &&
+ $this->selector($selector) &&
+ $this->matchChar(')')) {
+ $support = [Type::T_STRING, '', [[Type::T_KEYWORD, 'selector(']]];
+
+ $selectorList = [Type::T_LIST, '', []];
+ foreach ($selector as $sc) {
+ $compound = [Type::T_STRING, '', []];
+ foreach ($sc as $scp) {
+ if (is_array($scp)) {
+ $compound[2][] = $scp;
+ } else {
+ $compound[2][] = [Type::T_KEYWORD, $scp];
+ }
+ }
+ $selectorList[2][] = $compound;
+ }
+ $support[2][] = $selectorList;
+ $support[2][] = [Type::T_KEYWORD, ')'];
+ $parts[] = $support;
+ $s = $this->count;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->variable($var) or $this->interpolation($var)) {
+ $parts[] = $var;
+ $s = $this->count;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->literal('and', 3) &&
+ $this->genericList($expressions, 'supportsQuery', ' and', false)) {
+ array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
+ $parts = [$expressions];
+ $s = $this->count;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->literal('or', 2) &&
+ $this->genericList($expressions, 'supportsQuery', ' or', false)) {
+ array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
+ $parts = [$expressions];
+ $s = $this->count;
+ } else {
+ $this->seek($s);
+ }
+
+ if (count($parts)) {
+ if ($this->eatWhiteDefault) {
+ $this->whitespace();
+ }
+ $out = [Type::T_STRING, '', $parts];
+ return true;
+ }
+
+ return false;
+ }
+
+
/**
* Parse media expression
*
protected function expression(&$out)
{
$s = $this->count;
+ $discard = $this->discardComments;
+ $this->discardComments = true;
if ($this->matchChar('(')) {
if ($this->parenExpression($out, $s, ")")) {
+ $this->discardComments = $discard;
return true;
}
$out = [Type::T_STRING, '', [ '[', $out, ']' ]];
}
+ $this->discardComments = $discard;
return true;
}
if ($this->value($lhs)) {
$out = $this->expHelper($lhs, 0);
+ $this->discardComments = $discard;
return true;
}
+ $this->discardComments = $discard;
return false;
}
$content[] = $m[2] . "'";
} elseif ($this->literal("\\", 1, false)) {
$content[] = $m[2] . "\\";
- } elseif ($this->literal("\r\n", 2, false)
- || $this->matchChar("\r", false)
- || $this->matchChar("\n", false)
- || $this->matchChar("\f", false)) {
+ } elseif ($this->literal("\r\n", 2, false) ||
+ $this->matchChar("\r", false) ||
+ $this->matchChar("\n", false) ||
+ $this->matchChar("\f", false)
+ ) {
// this is a continuation escaping, to be ignored
} else {
$content[] = $m[2];
/**
* Parse keyword or interpolation
*
- * @param array $out
+ * @param array $out
+ * @param boolean $restricted
*
* @return boolean
*/
- protected function mixedKeyword(&$out)
+ protected function mixedKeyword(&$out, $restricted = false)
{
$parts = [];
$this->eatWhiteDefault = false;
for (;;) {
- if ($this->keyword($key)) {
+ if ($restricted ? $this->restrictedKeyword($key) : $this->keyword($key)) {
$parts[] = $key;
continue;
}
$out = $value;
} else {
if ($lookWhite) {
- $left = preg_match('/\s/', $this->buffer[$s - 1]) ? ' ' : '';
+ $left = ($s > 0 && preg_match('/\s/', $this->buffer[$s - 1])) ? ' ' : '';
$right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
} else {
$left = $right = false;
$part = ':';
}
- if ($this->mixedKeyword($nameParts)) {
+ if ($this->mixedKeyword($nameParts, true)) {
$parts[] = $part;
foreach ($nameParts as $sub) {
continue;
}
- if ($this->keyword($name)) {
+ if ($this->restrictedKeyword($name)) {
$parts[] = $name;
continue;
}
return false;
}
+ /**
+ * Parse a keyword that should not start with a number
+ *
+ * @param string $word
+ * @param boolean $eatWhitespace
+ *
+ * @return boolean
+ */
+ protected function restrictedKeyword(&$word, $eatWhitespace = null)
+ {
+ $s = $this->count;
+
+ if ($this->keyword($word, $eatWhitespace) && (ord($word[0]) > 57 || ord($word[0]) < 48)) {
+ return true;
+ }
+
+ $this->seek($s);
+
+ return false;
+ }
+
/**
* Parse a placeholder
*