<?php
+
/**
* SCSSPHP
*
use ScssPhp\ScssPhp\Colors;
use ScssPhp\ScssPhp\Compiler\Environment;
use ScssPhp\ScssPhp\Exception\CompilerException;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
use ScssPhp\ScssPhp\Formatter\OutputBlock;
use ScssPhp\ScssPhp\Node;
+use ScssPhp\ScssPhp\Node\Number;
use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
use ScssPhp\ScssPhp\Type;
use ScssPhp\ScssPhp\Parser;
*/
class Compiler
{
+ /**
+ * @deprecated
+ */
const LINE_COMMENTS = 1;
+ /**
+ * @deprecated
+ */
const DEBUG_INFO = 2;
+ /**
+ * @deprecated
+ */
const WITH_RULE = 1;
+ /**
+ * @deprecated
+ */
const WITH_MEDIA = 2;
+ /**
+ * @deprecated
+ */
const WITH_SUPPORTS = 4;
+ /**
+ * @deprecated
+ */
const WITH_ALL = 7;
const SOURCE_MAP_NONE = 0;
'<=' => 'lte',
'>=' => 'gte',
- '<=>' => 'cmp',
];
/**
public static $true = [Type::T_KEYWORD, 'true'];
public static $false = [Type::T_KEYWORD, 'false'];
+ /** @deprecated */
public static $NaN = [Type::T_KEYWORD, 'NaN'];
+ /** @deprecated */
public static $Infinity = [Type::T_KEYWORD, 'Infinity'];
public static $null = [Type::T_NULL];
public static $nullString = [Type::T_STRING, '', []];
];
protected $encoding = null;
+ /**
+ * @deprecated
+ */
protected $lineNumberStyle = null;
protected $sourceMap = self::SOURCE_MAP_NONE;
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'])) {
// check if any dependency file changed before accepting the cache
$this->shouldEvaluate = null;
$this->ignoreCallStackMessage = false;
- $this->parser = $this->parserFactory($path);
- $tree = $this->parser->parse($code);
- $this->parser = null;
+ try {
+ $this->parser = $this->parserFactory($path);
+ $tree = $this->parser->parse($code);
+ $this->parser = null;
- $this->formatter = new $this->formatter();
- $this->rootBlock = null;
- $this->rootEnv = $this->pushEnv($tree);
+ $this->formatter = new $this->formatter();
+ $this->rootBlock = null;
+ $this->rootEnv = $this->pushEnv($tree);
- $this->injectVariables($this->registeredVars);
- $this->compileRoot($tree);
- $this->popEnv();
+ $this->injectVariables($this->registeredVars);
+ $this->compileRoot($tree);
+ $this->popEnv();
- $sourceMapGenerator = null;
+ $sourceMapGenerator = null;
- if ($this->sourceMap) {
- if (\is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) {
- $sourceMapGenerator = $this->sourceMap;
- $this->sourceMap = self::SOURCE_MAP_FILE;
- } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) {
- $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
+ if ($this->sourceMap) {
+ if (\is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) {
+ $sourceMapGenerator = $this->sourceMap;
+ $this->sourceMap = self::SOURCE_MAP_FILE;
+ } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) {
+ $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
+ }
}
- }
- $out = $this->formatter->format($this->scope, $sourceMapGenerator);
+ $out = $this->formatter->format($this->scope, $sourceMapGenerator);
- if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) {
- $sourceMap = $sourceMapGenerator->generateJson();
- $sourceMapUrl = null;
+ if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) {
+ $sourceMap = $sourceMapGenerator->generateJson();
+ $sourceMapUrl = null;
- switch ($this->sourceMap) {
- case self::SOURCE_MAP_INLINE:
- $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap));
- break;
+ switch ($this->sourceMap) {
+ case self::SOURCE_MAP_INLINE:
+ $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap));
+ break;
- case self::SOURCE_MAP_FILE:
- $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap);
- break;
- }
+ case self::SOURCE_MAP_FILE:
+ $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap);
+ break;
+ }
- $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
+ $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
+ }
+ } catch (SassScriptException $e) {
+ throw $this->error($e->getMessage());
}
if ($this->cache && isset($cacheKey) && isset($compileOptions)) {
'out' => &$out,
];
- $this->cache->setCache("compile", $cacheKey, $v, $compileOptions);
+ $this->cache->setCache('compile', $cacheKey, $v, $compileOptions);
+ }
+
+ if (!$this->charsetSeen && function_exists('mb_strlen')) {
+ if (strlen($out) !== mb_strlen($out)) {
+ $out = '@charset "UTF-8";' . "\n" . $out;
+ }
}
return $out;
*/
protected function makeOutputBlock($type, $selectors = null)
{
- $out = new OutputBlock;
+ $out = new OutputBlock();
$out->type = $type;
$out->lines = [];
$out->children = [];
$origin = $this->collapseSelectors($origin);
$this->sourceLine = $block[Parser::SOURCE_LINE];
- $this->throwError("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
+ throw $this->error("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
}
}
} 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]) &&
+ if (
+ \count($new) && \is_string($new[\count($new) - 1]) &&
\strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
) {
- while (\count($new)>1 && substr($new[\count($new) - 1], -1) !== '(') {
+ while (\count($new) > 1 && substr($new[\count($new) - 1], -1) !== '(') {
$part = array_pop($new) . $part;
}
$new[\count($new) - 1] .= $part;
}
}
- if (\count($nonBreakableBefore) and $k == \count($new)) {
+ if (\count($nonBreakableBefore) && $k === \count($new)) {
$k--;
}
*/
protected function isPseudoSelector($part, &$matches)
{
- if (strpos($part, ":") === 0
- && preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)
+ if (
+ strpos($part, ':') === 0 &&
+ preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)
) {
return true;
}
$single = reset($extended);
$part = reset($single);
- if ($this->isPseudoSelector($part, $matchesExtended) &&
+ if (
+ $this->isPseudoSelector($part, $matchesExtended) &&
\in_array($matchesExtended[1], [ 'slotted' ])
) {
$prev = end($out);
$single = reset($prev);
$part = reset($single);
- if ($this->isPseudoSelector($part, $matchesPrev) &&
+ if (
+ $this->isPseudoSelector($part, $matchesPrev) &&
$matchesPrev[1] === $matchesExtended[1]
) {
$extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2);
- $extended[1] = $matchesPrev[2] . ", " . $extended[1];
+ $extended[1] = $matchesPrev[2] . ', ' . $extended[1];
$extended = implode($matchesExtended[1] . '(', $extended);
$extended = [ [ $extended ]];
array_pop($out);
}
}
- if ($initial &&
+ if (
+ $initial &&
$this->isPseudoSelector($part, $matches) &&
! \in_array($matches[1], [ 'not' ])
) {
$buffer = $matches[2];
$parser = $this->parserFactory(__METHOD__);
- if ($parser->parseSelector($buffer, $subSelectors)) {
+ if ($parser->parseSelector($buffer, $subSelectors, false)) {
foreach ($subSelectors as $ksub => $subSelector) {
$subExtended = [];
$this->matchExtends($subSelector, $subExtended, 0, false);
$subSelectorsExtended = implode(', ', $subSelectorsExtended);
$singleExtended = $single;
- $singleExtended[$k] = str_replace("(".$buffer.")", "($subSelectorsExtended)", $part);
+ $singleExtended[$k] = str_replace('(' . $buffer . ')', "($subSelectorsExtended)", $part);
$outOrigin[] = [ $singleExtended ];
$found = true;
}
foreach ($origin as $j => $new) {
// prevent infinite loop when target extends itself
- if ($this->isSelfExtend($single, $origin) and !$initial) {
+ if ($this->isSelfExtend($single, $origin) && ! $initial) {
return false;
}
$replacement = end($new);
// Extending a decorated tag with another tag is not possible.
- if ($extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
+ if (
+ $extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
preg_match('/^[a-z0-9]+$/i', $replacement[0])
) {
unset($origin[$j]);
$wasTag = false;
$pseudo = [];
- while (\count($other) && strpos(end($other), ':')===0) {
+ while (\count($other) && strpos(end($other), ':') === 0) {
array_unshift($pseudo, array_pop($other));
}
foreach ([array_reverse($base), array_reverse($other)] as $single) {
$rang = count($single);
+
foreach ($single as $part) {
if (preg_match('/^[\[:]/', $part)) {
$out[] = $part;
} elseif (preg_match('/^[\.#]/', $part)) {
array_unshift($out, $part);
$wasTag = false;
- } elseif (preg_match('/^[^_-]/', $part) and $rang==1) {
+ } elseif (preg_match('/^[^_-]/', $part) && $rang === 1) {
$tag[] = $part;
$wasTag = true;
} elseif ($wasTag) {
foreach ($media->children as $child) {
$type = $child[0];
- if ($type !== Type::T_BLOCK &&
+ if (
+ $type !== Type::T_BLOCK &&
$type !== Type::T_MEDIA &&
$type !== Type::T_DIRECTIVE &&
$type !== Type::T_IMPORT
}
if ($needsWrap) {
- $wrapped = new Block;
+ $wrapped = new Block();
$wrapped->sourceName = $media->sourceName;
$wrapped->sourceIndex = $media->sourceIndex;
$wrapped->sourceLine = $media->sourceLine;
$wrapped->children = $media->children;
$media->children = [[Type::T_BLOCK, $wrapped]];
-
- if (isset($this->lineNumberStyle)) {
- $annotation = $this->makeOutputBlock(Type::T_COMMENT);
- $annotation->depth = 0;
-
- $file = $this->sourceNames[$media->sourceIndex];
- $line = $media->sourceLine;
-
- switch ($this->lineNumberStyle) {
- case static::LINE_COMMENTS:
- $annotation->lines[] = '/* line ' . $line
- . ($file ? ', ' . $file : '')
- . ' */';
- break;
-
- case static::DEBUG_INFO:
- $annotation->lines[] = '@media -sass-debug-info{'
- . ($file ? 'filename{font-family:"' . $file . '"}' : '')
- . 'line{font-family:' . $line . '}}';
- break;
- }
-
- $this->scope->children[] = $annotation;
- }
}
$this->compileChildrenNoReturn($media->children, $this->scope);
protected function compileDirective($directive, OutputBlock $out)
{
if (\is_array($directive)) {
- $s = '@' . $directive[0];
+ $directiveName = $this->compileDirectiveName($directive[0]);
+ $s = '@' . $directiveName;
if (! empty($directive[1])) {
$s .= ' ' . $this->compileValue($directive[1]);
}
+ // sass-spec compliance on newline after directives, a bit tricky :/
+ $appendNewLine = (! empty($directive[2]) || strpos($s, "\n")) ? "\n" : "";
+ if (\is_array($directive[0]) && empty($directive[1])) {
+ $appendNewLine = "\n";
+ }
- $this->appendRootDirective($s . ';', $out);
+ if (empty($directive[3])) {
+ $this->appendRootDirective($s . ';' . $appendNewLine, $out, [Type::T_COMMENT, Type::T_DIRECTIVE]);
+ } else {
+ $this->appendOutputLine($out, Type::T_DIRECTIVE, $s . ';');
+ }
} else {
+ $directive->name = $this->compileDirectiveName($directive->name);
$s = '@' . $directive->name;
if (! empty($directive->value)) {
}
}
+ /**
+ * directive names can include some interpolation
+ *
+ * @param string|array $directiveName
+ * @return array|string
+ * @throws CompilerException
+ */
+ protected function compileDirectiveName($directiveName)
+ {
+ if (is_string($directiveName)) {
+ return $directiveName;
+ }
+
+ return $this->compileValue($directiveName);
+ }
+
/**
* Compile at-root
*
// wrap inline selector
if ($block->selector) {
- $wrapped = new Block;
+ $wrapped = new Block();
$wrapped->sourceName = $block->sourceName;
$wrapped->sourceIndex = $block->sourceIndex;
$wrapped->sourceLine = $block->sourceLine;
$selfParent = $block->selfParent;
- if (! $block->selfParent->selectors && isset($block->parent) && $block->parent &&
+ if (
+ ! $block->selfParent->selectors &&
+ isset($block->parent) && $block->parent &&
isset($block->parent->selectors) && $block->parent->selectors
) {
$selfParent = $block->parent;
$without = ['rule' => true];
if ($withCondition) {
+ if ($withCondition[0] === Type::T_INTERPOLATE) {
+ $w = $this->compileValue($withCondition);
+
+ $buffer = "($w)";
+ $parser = $this->parserFactory(__METHOD__);
+
+ if ($parser->parseValue($buffer, $reParsedWith)) {
+ $withCondition = $reParsedWith;
+ }
+ }
+
if ($this->libMapHasKey([$withCondition, static::$with])) {
$without = []; // cancel the default
$list = $this->coerceList($this->libMapGet([$withCondition, static::$with]));
if ($block->type === Type::T_DIRECTIVE) {
if (isset($block->name)) {
- return $this->testWithWithout($block->name, $with, $without);
+ return $this->testWithWithout($this->compileDirectiveName($block->name), $with, $without);
} elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
return $this->testWithWithout($m[1], $with, $without);
} else {
$s = reset($s);
}
- if (\is_object($s) && $s instanceof Node\Number) {
+ if (\is_object($s) && $s instanceof Number) {
return $this->testWithWithout('keyframes', $with, $without);
}
}
*/
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;
// wrap assign children in a block
// except for @font-face
- if ($block->type !== Type::T_DIRECTIVE || $block->name !== "font-face") {
+ if ($block->type !== Type::T_DIRECTIVE || $this->compileDirectiveName($block->name) !== 'font-face') {
// need wrapping?
$needWrapping = false;
}
if ($needWrapping) {
- $wrapped = new Block;
+ $wrapped = new Block();
$wrapped->sourceName = $block->sourceName;
$wrapped->sourceIndex = $block->sourceIndex;
$wrapped->sourceLine = $block->sourceLine;
$out = $this->makeOutputBlock(null);
- if (isset($this->lineNumberStyle) && \count($env->selectors) && \count($block->children)) {
- $annotation = $this->makeOutputBlock(Type::T_COMMENT);
- $annotation->depth = 0;
-
- $file = $this->sourceNames[$block->sourceIndex];
- $line = $block->sourceLine;
-
- switch ($this->lineNumberStyle) {
- case static::LINE_COMMENTS:
- $annotation->lines[] = '/* line ' . $line
- . ($file ? ', ' . $file : '')
- . ' */';
- break;
-
- case static::DEBUG_INFO:
- $annotation->lines[] = '@media -sass-debug-info{'
- . ($file ? 'filename{font-family:"' . $file . '"}' : '')
- . 'line{font-family:' . $line . '}}';
- break;
- }
-
- $this->scope->children[] = $annotation;
- }
-
$this->scope->children[] = $out;
if (\count($block->children)) {
// after evaluating interpolates, we might need a second pass
if ($this->shouldEvaluate) {
- $selectors = $this->revertSelfSelector($selectors);
+ $selectors = $this->replaceSelfSelector($selectors, '&');
$buffer = $this->collapseSelectors($selectors);
$parser = $this->parserFactory(__METHOD__);
- if ($parser->parseSelector($buffer, $newSelectors)) {
+ if ($parser->parseSelector($buffer, $newSelectors, true)) {
$selectors = array_map([$this, 'evalSelector'], $newSelectors);
}
}
if (\is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
$p = $this->compileValue($p);
- // force re-evaluation
- if (strpos($p, '&') !== false || strpos($p, ',') !== false) {
+ // force re-evaluation if self char or non standard char
+ if (preg_match(',[^\w-],', $p)) {
$this->shouldEvaluate = true;
}
- } elseif (\is_string($p) && \strlen($p) >= 2 &&
+ } elseif (
+ \is_string($p) && \strlen($p) >= 2 &&
($first = $p[0]) && ($first === '"' || $first === "'") &&
substr($p, -1) === $first
) {
*
* @return array
*/
- protected function revertSelfSelector($selectors)
+ protected function replaceSelfSelector($selectors, $replace = null)
{
foreach ($selectors as &$part) {
if (\is_array($part)) {
if ($part === [Type::T_SELF]) {
- $part = '&';
+ if (\is_null($replace)) {
+ $replace = $this->reduce([Type::T_SELF]);
+ $replace = $this->compileValue($replace);
+ }
+ $part = $replace;
} else {
- $part = $this->revertSelfSelector($part);
+ $part = $this->replaceSelfSelector($part, $replace);
}
}
}
$joined = [];
foreach ($single as $part) {
- if (empty($joined) ||
+ if (
+ empty($joined) ||
! \is_string($part) ||
preg_match('/[\[.:#%]/', $part)
) {
if (\count($this->callStack) > 25000) {
// not displayed but you can var_dump it to deep debug
$msg = $this->callStackMessage(true, 100);
- $msg = "Infinite calling loop";
+ $msg = 'Infinite calling loop';
- $this->throwError($msg);
+ throw $this->error($msg);
}
}
}
if (isset($ret)) {
- $this->throwError('@return may only be used within a function');
- $this->popCallStack();
-
- return;
+ throw $this->error('@return may only be used within a function');
}
}
// 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 &&
+ if (
+ $q[0] == Type::T_MEDIA_TYPE &&
(strpos($value, '(') !== false ||
strpos($value, ')') !== false ||
strpos($value, ':') !== false ||
$start = '@media ';
$default = trim($start);
$out = [];
- $current = "";
+ $current = '';
foreach ($queryList as $query) {
$type = null;
$out[] = $start . $current;
}
- $current = "";
+ $current = '';
$type = null;
$parts = [];
}
}
// t1 == t2, neither m1 nor m2 are "not"
- return [empty($m1)? $m2 : $m1, $t1];
+ return [empty($m1) ? $m2 : $m1, $t1];
}
/**
if ($rawPath[0] === Type::T_STRING) {
$path = $this->compileStringContent($rawPath);
- if ($path = $this->findImport($path)) {
+ if (strpos($path, 'url(') !== 0 && $path = $this->findImport($path)) {
if (! $once || ! \in_array($path, $this->importedFiles)) {
$this->importFile($path, $out);
$this->importedFiles[] = $path;
return true;
}
- $this->appendRootDirective('@import ' . $this->compileValue($rawPath). ';', $out);
+ $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
return false;
}
foreach ($rawPath[2] as $path) {
if ($path[0] !== Type::T_STRING) {
- $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
+ $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
return false;
}
return true;
}
- $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
+ $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
return false;
}
+ /**
+ * @param $rawPath
+ * @return string
+ * @throws CompilerException
+ */
+ protected function compileImportPath($rawPath)
+ {
+ $path = $this->compileValue($rawPath);
+
+ // case url() without quotes : supress \r \n remaining in the path
+ // if this is a real string there can not be CR or LF char
+ if (strpos($path, 'url(') === 0) {
+ $path = str_replace(array("\r", "\n"), array('', ' '), $path);
+ } else {
+ // if this is a file name in a string, spaces shoudl be escaped
+ $path = $this->reduce($rawPath);
+ $path = $this->escapeImportPathString($path);
+ $path = $this->compileValue($path);
+ }
+
+ return $path;
+ }
+
+ /**
+ * @param array $path
+ * @return array
+ * @throws CompilerException
+ */
+ protected function escapeImportPathString($path)
+ {
+ switch ($path[0]) {
+ case Type::T_LIST:
+ foreach ($path[2] as $k => $v) {
+ $path[2][$k] = $this->escapeImportPathString($v);
+ }
+ break;
+ case Type::T_STRING:
+ if ($path[1]) {
+ $path = $this->compileValue($path);
+ $path = str_replace(' ', '\\ ', $path);
+ $path = [Type::T_KEYWORD, $path];
+ }
+ break;
+ }
+
+ return $path;
+ }
/**
* Append a root directive like @import or @charset as near as the possible from the source code
{
$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 &&
+ if (
+ $lastChild->depth === $out->depth &&
\is_null($lastChild->selectors) &&
! \count($lastChild->children)
) {
break;
}
- if ($compiledName === 'font' and $value[0] === Type::T_LIST && $value[1]==',') {
+ if ($compiledName === 'font' && $value[0] === Type::T_LIST && $value[1] === ',') {
// this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica"
// we need to handle the first list element
$shorthandValue=&$value[2][0];
$divider = $this->reduce($divider, true);
}
- if (\intval($divider->dimension) and ! \count($divider->units)) {
+ if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) {
$revert = false;
}
}
if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
if ($maxShorthandDividers > 0) {
$revert = true;
+
// if the list of values is too long, this has to be a shorthand,
// otherwise it could be a real division
- if (\is_null($maxListElements) or \count($shorthandValue[2]) <= $maxListElements) {
+ if (\is_null($maxListElements) || \count($shorthandValue[2]) <= $maxListElements) {
if ($shorthandDividerNeedsUnit) {
$divider = $item[3];
$divider = $this->reduce($divider, true);
}
- if (\intval($divider->dimension) and ! \count($divider->units)) {
+ if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) {
$revert = false;
}
}
case Type::T_EXTEND:
foreach ($child[1] as $sel) {
+ $sel = $this->replaceSelfSelector($sel);
$results = $this->evalSelectors([$sel]);
foreach ($results as $result) {
}
foreach ($if->cases as $case) {
- if ($case->type === Type::T_ELSE ||
+ if (
+ $case->type === Type::T_ELSE ||
$case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond))
) {
return $this->compileChildren($case->children, $out);
$ret = $this->compileChildren($each->children, $out);
if ($ret) {
- if ($ret[0] !== Type::T_CONTROL) {
- $store = $this->env->store;
- $this->popEnv();
- $this->backPropagateEnv($store, $each->vars);
+ $store = $this->env->store;
+ $this->popEnv();
+ $this->backPropagateEnv($store, $each->vars);
- return $ret;
- }
-
- if ($ret[1]) {
- break;
- }
+ return $ret;
}
}
$store = $this->env->store;
$ret = $this->compileChildren($while->children, $out);
if ($ret) {
- if ($ret[0] !== Type::T_CONTROL) {
- return $ret;
- }
-
- if ($ret[1]) {
- break;
- }
+ return $ret;
}
}
break;
$start = $this->reduce($for->start, true);
$end = $this->reduce($for->end, true);
- if (! ($start[2] == $end[2] || $end->unitless())) {
- $this->throwError('Incompatible units: "%s" and "%s".', $start->unitStr(), $end->unitStr());
+ if (! $start instanceof Number) {
+ throw $this->error('%s is not a number', $start[0]);
+ }
- break;
+ if (! $end instanceof Number) {
+ throw $this->error('%s is not a number', $end[0]);
}
- $unit = $start[2];
- $start = $start[1];
- $end = $end[1];
+ $start->assertSameUnitOrUnitless($end);
+
+ $numeratorUnits = $start->getNumeratorUnits();
+ $denominatorUnits = $start->getDenominatorUnits();
+
+ $start = $start->getDimension();
+ $end = $end->getDimension();
$d = $start < $end ? 1 : -1;
$this->pushEnv();
for (;;) {
- if ((! $for->until && $start - $d == $end) ||
+ if (
+ (! $for->until && $start - $d == $end) ||
($for->until && $start == $end)
) {
break;
}
- $this->set($for->var, new Node\Number($start, $unit));
+ $this->set($for->var, new Number($start, $numeratorUnits, $denominatorUnits));
$start += $d;
$ret = $this->compileChildren($for->children, $out);
if ($ret) {
- if ($ret[0] !== Type::T_CONTROL) {
- $store = $this->env->store;
- $this->popEnv();
- $this->backPropagateEnv($store, [$for->var]);
- return $ret;
- }
+ $store = $this->env->store;
+ $this->popEnv();
+ $this->backPropagateEnv($store, [$for->var]);
- if ($ret[1]) {
- break;
- }
+ return $ret;
}
}
break;
- case Type::T_BREAK:
- return [Type::T_CONTROL, true];
-
- case Type::T_CONTINUE:
- return [Type::T_CONTROL, false];
-
case Type::T_RETURN:
return $this->reduce($child[1], true);
$mixin = $this->get(static::$namespaces['mixin'] . $name, false);
if (! $mixin) {
- $this->throwError("Undefined mixin $name");
- break;
+ throw $this->error("Undefined mixin $name");
}
$callingScope = $this->getStoreEnv();
if (! empty($mixin->parentEnv)) {
$this->env->declarationScopeParent = $mixin->parentEnv;
} else {
- $this->throwError("@mixin $name() without parentEnv");
+ throw $this->error("@mixin $name() without parentEnv");
}
- $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . " " . $name);
+ $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . ' ' . $name);
$this->popEnv();
break;
$fname = $this->sourceNames[$this->sourceIndex];
$line = $this->sourceLine;
- $value = $this->compileValue($this->reduce($value, true));
+ $value = $this->compileDebugValue($value);
- fwrite($this->stderr, "File $fname on line $line DEBUG: $value\n");
+ fwrite($this->stderr, "$fname:$line DEBUG: $value\n");
break;
case Type::T_WARN:
$fname = $this->sourceNames[$this->sourceIndex];
$line = $this->sourceLine;
- $value = $this->compileValue($this->reduce($value, true));
+ $value = $this->compileDebugValue($value);
- fwrite($this->stderr, "File $fname on line $line WARN: $value\n");
+ fwrite($this->stderr, "WARNING: $value\n on line $line of $fname\n\n");
break;
case Type::T_ERROR:
$line = $this->sourceLine;
$value = $this->compileValue($this->reduce($value, true));
- $this->throwError("File $fname on line $line ERROR: $value\n");
- break;
-
- case Type::T_CONTROL:
- $this->throwError('@break/@continue not permitted in this scope');
- break;
+ throw $this->error("File $fname on line $line ERROR: $value\n");
default:
- $this->throwError("unknown child type: $child[0]");
+ throw $this->error("unknown child type: $child[0]");
}
}
* Reduce expression to string
*
* @param array $exp
+ * @param true $keepParens
*
* @return array
*/
- protected function expToString($exp)
+ protected function expToString($exp, $keepParens = false)
{
- list(, $op, $left, $right, /* $inParens */, $whiteLeft, $whiteRight) = $exp;
+ list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
+
+ $content = [];
- $content = [$this->reduce($left)];
+ if ($keepParens && $inParens) {
+ $content[] = '(';
+ }
+
+ $content[] = $this->reduce($left);
if ($whiteLeft) {
$content[] = ' ';
$content[] = $this->reduce($right);
+ if ($keepParens && $inParens) {
+ $content[] = ')';
+ }
+
return [Type::T_STRING, '', $content];
}
* @param array $value
* @param boolean $inExp
*
- * @return null|string|array|\ScssPhp\ScssPhp\Node\Number
+ * @return null|string|array|Number
*/
protected function reduce($value, $inExp = false)
{
}
// special case: looks like css shorthand
- if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2]) &&
- (($right[0] !== Type::T_NUMBER && $right[2] != '') ||
+ if (
+ $opName == 'div' && ! $inParens && ! $inExp &&
+ (($right[0] !== Type::T_NUMBER && isset($right[2]) && $right[2] != '') ||
($right[0] === Type::T_NUMBER && ! $right->unitless()))
) {
return $this->expToString($value);
// 3. op[op name]
$fn = "op${ucOpName}${ucLType}${ucRType}";
- if (\is_callable([$this, $fn]) ||
+ if (
+ \is_callable([$this, $fn]) ||
(($fn = "op${ucLType}${ucRType}") &&
\is_callable([$this, $fn]) &&
$passOp = true) ||
\is_callable([$this, $fn]) &&
$genOp = true)
) {
- $coerceUnit = false;
-
- if (! isset($genOp) &&
- $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER
- ) {
- $coerceUnit = true;
-
- switch ($opName) {
- case 'mul':
- $targetUnit = $left[2];
-
- foreach ($right[2] as $unit => $exp) {
- $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) + $exp;
- }
- break;
-
- case 'div':
- $targetUnit = $left[2];
-
- foreach ($right[2] as $unit => $exp) {
- $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) - $exp;
- }
- break;
-
- case 'mod':
- $targetUnit = $left[2];
- break;
-
- default:
- $targetUnit = $left->unitless() ? $right[2] : $left[2];
- }
-
- $baseUnitLeft = $left->isNormalizable();
- $baseUnitRight = $right->isNormalizable();
-
- if ($baseUnitLeft && $baseUnitRight && $baseUnitLeft === $baseUnitRight) {
- $left = $left->normalize();
- $right = $right->normalize();
- }
- else {
- if ($coerceUnit) {
- $left = new Node\Number($left[1], []);
- }
- }
- }
-
$shouldEval = $inParens || $inExp;
if (isset($passOp)) {
}
if (isset($out)) {
- if ($coerceUnit && $out[0] === Type::T_NUMBER) {
- $out = $out->coerce($targetUnit);
- }
-
return $out;
}
}
$inExp = $inExp || $this->shouldEval($exp);
$exp = $this->reduce($exp);
- if ($exp[0] === Type::T_NUMBER) {
+ if ($exp instanceof Number) {
switch ($op) {
case '+':
- return new Node\Number($exp[1], $exp[2]);
+ return $exp;
case '-':
- return new Node\Number(-$exp[1], $exp[2]);
+ return $exp->unaryMinus();
}
}
return $this->fncall($value[1], $value[2]);
case Type::T_SELF:
- $selfSelector = $this->multiplySelectors($this->env,!empty($this->env->block->selfParent) ? $this->env->block->selfParent : null);
+ $selfParent = ! empty($this->env->block->selfParent) ? $this->env->block->selfParent : null;
+ $selfSelector = $this->multiplySelectors($this->env, $selfParent);
$selfSelector = $this->collapseSelectors($selfSelector, true);
return $selfSelector;
*
* @return array|null
*/
- protected function fncall($name, $argValues)
+ protected function fncall($functionReference, $argValues)
{
- // SCSS @function
- if ($this->callScssFunction($name, $argValues, $returnValue)) {
- return $returnValue;
- }
+ // a string means this is a static hard reference coming from the parsing
+ if (is_string($functionReference)) {
+ $name = $functionReference;
- // native PHP functions
- if ($this->callNativeFunction($name, $argValues, $returnValue)) {
- return $returnValue;
+ $functionReference = $this->getFunctionReference($name);
+ if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) {
+ $functionReference = [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]];
+ }
}
- // for CSS functions, simply flatten the arguments into a list
- $listArgs = [];
+ // a function type means we just want a plain css function call
+ if ($functionReference[0] === Type::T_FUNCTION) {
+ // for CSS functions, simply flatten the arguments into a list
+ $listArgs = [];
- foreach ((array) $argValues as $arg) {
- if (empty($arg[0])) {
- $listArgs[] = $this->reduce($arg[1]);
+ foreach ((array) $argValues as $arg) {
+ if (empty($arg[0]) || count($argValues) === 1) {
+ $listArgs[] = $this->reduce($this->stringifyFncallArgs($arg[1]));
+ }
}
+
+ return [Type::T_FUNCTION, $functionReference[1], [Type::T_LIST, ',', $listArgs]];
}
- return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', $listArgs]];
- }
+ if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) {
+ return static::$defaultValue;
+ }
- /**
- * Normalize name
- *
- * @param string $name
- *
- * @return string
- */
- protected function normalizeName($name)
- {
- return str_replace('-', '_', $name);
- }
- /**
- * Normalize value
- *
- * @param array $value
- *
- * @return array
- */
- public function normalizeValue($value)
- {
- $value = $this->coerceForExpression($this->reduce($value));
+ switch ($functionReference[1]) {
+ // SCSS @function
+ case 'scss':
+ return $this->callScssFunction($functionReference[3], $argValues);
- switch ($value[0]) {
- case Type::T_LIST:
- $value = $this->extractInterpolation($value);
+ // native PHP functions
+ case 'user':
+ case 'native':
+ list(,,$name, $fn, $prototype) = $functionReference;
- if ($value[0] !== Type::T_LIST) {
- return [Type::T_KEYWORD, $this->compileValue($value)];
+ // special cases of css valid functions min/max
+ $name = strtolower($name);
+ if (\in_array($name, ['min', 'max'])) {
+ $cssFunction = $this->cssValidArg(
+ [Type::T_FUNCTION_CALL, $name, $argValues],
+ ['min', 'max', 'calc', 'env', 'var']
+ );
+ if ($cssFunction !== false) {
+ return $cssFunction;
+ }
}
+ $returnValue = $this->callNativeFunction($name, $fn, $prototype, $argValues);
- foreach ($value[2] as $key => $item) {
- $value[2][$key] = $this->normalizeValue($item);
+ if (! isset($returnValue)) {
+ return $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $argValues);
}
- if (! empty($value['enclosing'])) {
- unset($value['enclosing']);
+ return $returnValue;
+
+ default:
+ return static::$defaultValue;
+ }
+ }
+
+ protected function cssValidArg($arg, $allowed_function = [], $inFunction = false)
+ {
+ switch ($arg[0]) {
+ case Type::T_INTERPOLATE:
+ return [Type::T_KEYWORD, $this->CompileValue($arg)];
+
+ case Type::T_FUNCTION:
+ if (! \in_array($arg[1], $allowed_function)) {
+ return false;
+ }
+ if ($arg[2][0] === Type::T_LIST) {
+ foreach ($arg[2][2] as $k => $subarg) {
+ $arg[2][2][$k] = $this->cssValidArg($subarg, $allowed_function, $arg[1]);
+ if ($arg[2][2][$k] === false) {
+ return false;
+ }
+ }
}
+ return $arg;
- return $value;
+ case Type::T_FUNCTION_CALL:
+ if (! \in_array($arg[1], $allowed_function)) {
+ return false;
+ }
+ $cssArgs = [];
+ foreach ($arg[2] as $argValue) {
+ if ($argValue === static::$null) {
+ return false;
+ }
+ $cssArg = $this->cssValidArg($argValue[1], $allowed_function, $arg[1]);
+ if (empty($argValue[0]) && $cssArg !== false) {
+ $cssArgs[] = [$argValue[0], $cssArg];
+ } else {
+ return false;
+ }
+ }
+
+ return $this->fncall([Type::T_FUNCTION, $arg[1], [Type::T_LIST, ',', []]], $cssArgs);
case Type::T_STRING:
- return [$value[0], '"', [$this->compileStringContent($value)]];
+ case Type::T_KEYWORD:
+ if (!$inFunction or !\in_array($inFunction, ['calc', 'env', 'var'])) {
+ return false;
+ }
+ return $this->stringifyFncallArgs($arg);
case Type::T_NUMBER:
- return $value->normalize();
+ return $this->stringifyFncallArgs($arg);
- case Type::T_INTERPOLATE:
- return [Type::T_KEYWORD, $this->compileValue($value)];
+ case Type::T_LIST:
+ if (!$inFunction) {
+ return false;
+ }
+ if (empty($arg['enclosing']) and $arg[1] === '') {
+ foreach ($arg[2] as $k => $subarg) {
+ $arg[2][$k] = $this->cssValidArg($subarg, $allowed_function, $inFunction);
+ if ($arg[2][$k] === false) {
+ return false;
+ }
+ }
+ $arg[0] = Type::T_STRING;
+ return $arg;
+ }
+ return false;
+
+ case Type::T_EXPRESSION:
+ if (! \in_array($arg[1], ['+', '-', '/', '*'])) {
+ return false;
+ }
+ $arg[2] = $this->cssValidArg($arg[2], $allowed_function, $inFunction);
+ $arg[3] = $this->cssValidArg($arg[3], $allowed_function, $inFunction);
+ if ($arg[2] === false || $arg[3] === false) {
+ return false;
+ }
+ return $this->expToString($arg, true);
+ case Type::T_VARIABLE:
+ case Type::T_SELF:
default:
- return $value;
+ return false;
}
}
+
/**
- * Add numbers
+ * Reformat fncall arguments to proper css function output
*
- * @param array $left
- * @param array $right
+ * @param $arg
*
- * @return \ScssPhp\ScssPhp\Node\Number
+ * @return array|\ArrayAccess|Number|string|null
*/
- protected function opAddNumberNumber($left, $right)
+ protected function stringifyFncallArgs($arg)
{
- return new Node\Number($left[1] + $right[1], $left[2]);
+
+ switch ($arg[0]) {
+ case Type::T_LIST:
+ foreach ($arg[2] as $k => $v) {
+ $arg[2][$k] = $this->stringifyFncallArgs($v);
+ }
+ break;
+
+ case Type::T_EXPRESSION:
+ if ($arg[1] === '/') {
+ $arg[2] = $this->stringifyFncallArgs($arg[2]);
+ $arg[3] = $this->stringifyFncallArgs($arg[3]);
+ $arg[5] = $arg[6] = false; // no space around /
+ $arg = $this->expToString($arg);
+ }
+ break;
+
+ case Type::T_FUNCTION_CALL:
+ $name = strtolower($arg[1]);
+
+ if (in_array($name, ['max', 'min', 'calc'])) {
+ $args = $arg[2];
+ $arg = $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $args);
+ }
+ break;
+ }
+
+ return $arg;
}
/**
- * Multiply numbers
- *
- * @param array $left
- * @param array $right
+ * Find a function reference
+ * @param string $name
+ * @param bool $safeCopy
+ * @return array
+ */
+ protected function getFunctionReference($name, $safeCopy = false)
+ {
+ // SCSS @function
+ if ($func = $this->get(static::$namespaces['function'] . $name, false)) {
+ if ($safeCopy) {
+ $func = clone $func;
+ }
+
+ return [Type::T_FUNCTION_REFERENCE, 'scss', $name, $func];
+ }
+
+ // native PHP functions
+
+ // try to find a native lib function
+ $normalizedName = $this->normalizeName($name);
+ $libName = null;
+
+ if (isset($this->userFunctions[$normalizedName])) {
+ // see if we can find a user function
+ list($f, $prototype) = $this->userFunctions[$normalizedName];
+
+ return [Type::T_FUNCTION_REFERENCE, 'user', $name, $f, $prototype];
+ }
+
+ if (($f = $this->getBuiltinFunction($normalizedName)) && \is_callable($f)) {
+ $libName = $f[1];
+ $prototype = isset(static::$$libName) ? static::$$libName : null;
+
+ return [Type::T_FUNCTION_REFERENCE, 'native', $name, $f, $prototype];
+ }
+
+ return static::$null;
+ }
+
+
+ /**
+ * Normalize name
*
- * @return \ScssPhp\ScssPhp\Node\Number
+ * @param string $name
+ *
+ * @return string
*/
- protected function opMulNumberNumber($left, $right)
+ protected function normalizeName($name)
{
- return new Node\Number($left[1] * $right[1], $left[2]);
+ return str_replace('-', '_', $name);
+ }
+
+ /**
+ * Normalize value
+ *
+ * @param array $value
+ *
+ * @return array
+ */
+ public function normalizeValue($value)
+ {
+ $value = $this->coerceForExpression($this->reduce($value));
+
+ switch ($value[0]) {
+ case Type::T_LIST:
+ $value = $this->extractInterpolation($value);
+
+ if ($value[0] !== Type::T_LIST) {
+ return [Type::T_KEYWORD, $this->compileValue($value)];
+ }
+
+ foreach ($value[2] as $key => $item) {
+ $value[2][$key] = $this->normalizeValue($item);
+ }
+
+ if (! empty($value['enclosing'])) {
+ unset($value['enclosing']);
+ }
+
+ return $value;
+
+ case Type::T_STRING:
+ return [$value[0], '"', [$this->compileStringContent($value)]];
+
+ case Type::T_INTERPOLATE:
+ return [Type::T_KEYWORD, $this->compileValue($value)];
+
+ default:
+ return $value;
+ }
+ }
+
+ /**
+ * Add numbers
+ *
+ * @param Number $left
+ * @param Number $right
+ *
+ * @return Number
+ */
+ protected function opAddNumberNumber(Number $left, Number $right)
+ {
+ return $left->plus($right);
+ }
+
+ /**
+ * Multiply numbers
+ *
+ * @param Number $left
+ * @param Number $right
+ *
+ * @return Number
+ */
+ protected function opMulNumberNumber(Number $left, Number $right)
+ {
+ return $left->times($right);
}
/**
* Subtract numbers
*
- * @param array $left
- * @param array $right
+ * @param Number $left
+ * @param Number $right
*
- * @return \ScssPhp\ScssPhp\Node\Number
+ * @return Number
*/
- protected function opSubNumberNumber($left, $right)
+ protected function opSubNumberNumber(Number $left, Number $right)
{
- return new Node\Number($left[1] - $right[1], $left[2]);
+ return $left->minus($right);
}
/**
* Divide numbers
*
- * @param array $left
- * @param array $right
+ * @param Number $left
+ * @param Number $right
*
- * @return array|\ScssPhp\ScssPhp\Node\Number
+ * @return Number
*/
- protected function opDivNumberNumber($left, $right)
+ protected function opDivNumberNumber(Number $left, Number $right)
{
- if ($right[1] == 0) {
- return ($left[1] == 0) ? static::$NaN : static::$Infinity;
- }
-
- return new Node\Number($left[1] / $right[1], $left[2]);
+ return $left->dividedBy($right);
}
/**
* Mod numbers
*
- * @param array $left
- * @param array $right
+ * @param Number $left
+ * @param Number $right
*
- * @return \ScssPhp\ScssPhp\Node\Number
+ * @return Number
*/
- protected function opModNumberNumber($left, $right)
+ protected function opModNumberNumber(Number $left, Number $right)
{
- if ($right[1] == 0) {
- return static::$NaN;
- }
-
- return new Node\Number($left[1] % $right[1], $left[2]);
+ return $left->modulo($right);
}
/**
break;
case '%':
+ if ($rval == 0) {
+ throw $this->error("color: Can't take modulo by zero");
+ }
+
$out[] = $lval % $rval;
break;
case '/':
if ($rval == 0) {
- $this->throwError("color: Can't divide by zero");
- break 2;
+ throw $this->error("color: Can't divide by zero");
}
$out[] = (int) ($lval / $rval);
return $this->opNeq($left, $right);
default:
- $this->throwError("color: unknown op $op");
- break 2;
+ throw $this->error("color: unknown op $op");
}
}
*
* @param string $op
* @param array $left
- * @param array $right
+ * @param Number $right
*
* @return array
*/
- protected function opColorNumber($op, $left, $right)
+ protected function opColorNumber($op, $left, Number $right)
{
- $value = $right[1];
+ $value = $right->getDimension();
return $this->opColorColor(
$op,
* Compare number and color
*
* @param string $op
- * @param array $left
+ * @param Number $left
* @param array $right
*
* @return array
*/
- protected function opNumberColor($op, $left, $right)
+ protected function opNumberColor($op, Number $left, $right)
{
- $value = $left[1];
+ $value = $left->getDimension();
return $this->opColorColor(
$op,
}
/**
- * Compare number1 >= number2
+ * Compare number1 == number2
*
- * @param array $left
- * @param array $right
+ * @param Number $left
+ * @param Number $right
*
* @return array
*/
- protected function opGteNumberNumber($left, $right)
+ protected function opEqNumberNumber(Number $left, Number $right)
{
- return $this->toBool($left[1] >= $right[1]);
+ return $this->toBool($left->equals($right));
}
/**
- * Compare number1 > number2
+ * Compare number1 != number2
*
- * @param array $left
- * @param array $right
+ * @param Number $left
+ * @param Number $right
*
* @return array
*/
- protected function opGtNumberNumber($left, $right)
+ protected function opNeqNumberNumber(Number $left, Number $right)
{
- return $this->toBool($left[1] > $right[1]);
+ return $this->toBool(!$left->equals($right));
}
/**
- * Compare number1 <= number2
+ * Compare number1 >= number2
*
- * @param array $left
- * @param array $right
+ * @param Number $left
+ * @param Number $right
*
* @return array
*/
- protected function opLteNumberNumber($left, $right)
+ protected function opGteNumberNumber(Number $left, Number $right)
{
- return $this->toBool($left[1] <= $right[1]);
+ return $this->toBool($left->greaterThanOrEqual($right));
}
/**
- * Compare number1 < number2
+ * Compare number1 > number2
*
- * @param array $left
- * @param array $right
+ * @param Number $left
+ * @param Number $right
*
* @return array
*/
- protected function opLtNumberNumber($left, $right)
+ protected function opGtNumberNumber(Number $left, Number $right)
{
- return $this->toBool($left[1] < $right[1]);
+ return $this->toBool($left->greaterThan($right));
}
/**
- * Three-way comparison, aka spaceship operator
+ * Compare number1 <= number2
*
- * @param array $left
- * @param array $right
+ * @param Number $left
+ * @param Number $right
*
- * @return \ScssPhp\ScssPhp\Node\Number
+ * @return array
*/
- protected function opCmpNumberNumber($left, $right)
+ protected function opLteNumberNumber(Number $left, Number $right)
{
- $n = $left[1] - $right[1];
+ return $this->toBool($left->lessThanOrEqual($right));
+ }
- return new Node\Number($n ? $n / abs($n) : 0, '');
+ /**
+ * Compare number1 < number2
+ *
+ * @param Number $left
+ * @param Number $right
+ *
+ * @return array
+ */
+ protected function opLtNumberNumber(Number $left, Number $right)
+ {
+ return $this->toBool($left->lessThan($right));
}
/**
return $thing ? static::$true : static::$false;
}
+ /**
+ * Escape non printable chars in strings output as in dart-sass
+ * @param $string
+ * @return string|string[]
+ */
+ public function escapeNonPrintableChars($string, $inKeyword = false)
+ {
+ static $replacement = [];
+ if (empty($replacement[$inKeyword])) {
+ for ($i = 0; $i < 32; $i++) {
+ if ($i !== 9 || $inKeyword) {
+ $replacement[$inKeyword][chr($i)] = '\\' . dechex($i) . ($inKeyword ? ' ' : chr(0));
+ }
+ }
+ }
+ $string = str_replace(array_keys($replacement[$inKeyword]), array_values($replacement[$inKeyword]), $string);
+ // chr(0) is not a possible char from the input, so any chr(0) comes from our escaping replacement
+ if (strpos($string, chr(0)) !== false) {
+ if (substr($string, -1) === chr(0)) {
+ $string = substr($string, 0, -1);
+ }
+ $string = str_replace(
+ [chr(0) . '\\',chr(0) . ' '],
+ [ '\\', ' '],
+ $string
+ );
+ if (strpos($string, chr(0)) !== false) {
+ $parts = explode(chr(0), $string);
+ $string = array_shift($parts);
+ while (count($parts)) {
+ $next = array_shift($parts);
+ if (strpos("0123456789abcdefABCDEF" . chr(9), $next[0]) !== false) {
+ $string .= " ";
+ }
+ $string .= $next;
+ }
+ }
+ }
+
+ return $string;
+ }
+
/**
* Compiles a primitive value into a CSS property value.
*
switch ($value[0]) {
case Type::T_KEYWORD:
+ if (is_string($value[1])) {
+ $value[1] = $this->escapeNonPrintableChars($value[1], true);
+ }
return $value[1];
case Type::T_COLOR:
}
if (is_numeric($alpha)) {
- $a = new Node\Number($alpha, '');
+ $a = new Number($alpha, '');
} else {
$a = $alpha;
}
return $value->output($this);
case Type::T_STRING:
- return $value[1] . $this->compileStringContent($value) . $value[1];
+ $content = $this->compileStringContent($value);
+
+ if ($value[1]) {
+ $content = str_replace('\\', '\\\\', $content);
+
+ $content = $this->escapeNonPrintableChars($content);
+
+ // force double quote as string quote for the output in certain cases
+ if (
+ $value[1] === "'" &&
+ (strpos($content, '"') === false or strpos($content, "'") !== false) &&
+ strpbrk($content, '{}\\\'') !== false
+ ) {
+ $value[1] = '"';
+ } elseif (
+ $value[1] === '"' &&
+ (strpos($content, '"') !== false and strpos($content, "'") === false)
+ ) {
+ $value[1] = "'";
+ }
+
+ $content = str_replace($value[1], '\\' . $value[1], $content);
+ }
+
+ return $value[1] . $content . $value[1];
case Type::T_FUNCTION:
$args = ! empty($value[2]) ? $this->compileValue($value[2]) : '';
return "$value[1]($args)";
+ case Type::T_FUNCTION_REFERENCE:
+ $name = ! empty($value[2]) ? $value[2] : '';
+
+ return "get-function(\"$name\")";
+
case Type::T_LIST:
$value = $this->extractInterpolation($value);
}
list(, $delim, $items) = $value;
- $pre = $post = "";
+ $pre = $post = '';
if (! empty($value['enclosing'])) {
switch ($value['enclosing']) {
case 'parent':
- //$pre = "(";
- //$post = ")";
+ //$pre = '(';
+ //$post = ')';
break;
case 'forced_parent':
- $pre = "(";
- $post = ")";
+ $pre = '(';
+ $post = ')';
break;
case 'bracket':
case 'forced_bracket':
- $pre = "[";
- $post = "]";
+ $pre = '[';
+ $post = ']';
break;
}
}
$prefix_value = '';
+
if ($delim !== ' ') {
$prefix_value = ' ';
}
$filtered = [];
+ $same_string_quote = null;
foreach ($items as $item) {
+ if (\is_null($same_string_quote)) {
+ $same_string_quote = false;
+ if ($item[0] === Type::T_STRING) {
+ $same_string_quote = $item[1];
+ foreach ($items as $ii) {
+ if ($ii[0] !== Type::T_STRING) {
+ $same_string_quote = false;
+ break;
+ }
+ }
+ }
+ }
if ($item[0] === Type::T_NULL) {
continue;
}
+ if ($same_string_quote === '"' && $item[0] === Type::T_STRING && $item[1]) {
+ $item[1] = $same_string_quote;
+ }
$compiled = $this->compileValue($item);
+
if ($prefix_value && \strlen($compiled)) {
$compiled = $prefix_value . $compiled;
}
+
$filtered[] = $compiled;
}
$delim .= ' ';
}
- $left = \count($left[2]) > 0 ?
- $this->compileValue($left) . $delim . $whiteLeft: '';
+ $left = \count($left[2]) > 0
+ ? $this->compileValue($left) . $delim . $whiteLeft
+ : '';
$delim = $right[1];
break;
case Type::T_STRING:
- $reduced = [Type::T_KEYWORD, $this->compileStringContent($reduced)];
+ $reduced = [Type::T_STRING, '', [$this->compileStringContent($reduced)]];
break;
case Type::T_NULL:
return $this->compileCommentValue($value);
default:
- $this->throwError("unknown value type: ".json_encode($value));
+ throw $this->error('unknown value type: ' . json_encode($value));
+ }
+ }
+
+ /**
+ * @param array $value
+ *
+ * @return array|string
+ */
+ protected function compileDebugValue($value)
+ {
+ $value = $this->reduce($value, true);
+
+ switch ($value[0]) {
+ case Type::T_STRING:
+ return $this->compileStringContent($value);
+
+ default:
+ return $this->compileValue($value);
}
}
$prevSelectors = $selectors;
$selectors = [];
- foreach ($prevSelectors as $selector) {
- foreach ($parentSelectors as $parent) {
+ foreach ($parentSelectors as $parent) {
+ foreach ($prevSelectors as $selector) {
if ($selfParentSelectors) {
foreach ($selfParentSelectors as $selfParent) {
// if no '&' in the selector, each call will give same result, only add once
$selectors = array_values($selectors);
// case we are just starting a at-root : nothing to multiply but parentSelectors
- if (!$selectors and $selfParentSelectors) {
+ if (! $selectors && $selfParentSelectors) {
$selectors = $selfParentSelectors;
}
*/
protected function multiplyMedia(Environment $env = null, $childQueries = null)
{
- if (! isset($env) ||
+ if (
+ ! isset($env) ||
! empty($env->block->type) && $env->block->type !== Type::T_MEDIA
) {
return $childQueries;
*/
protected function pushEnv(Block $block = null)
{
- $env = new Environment;
+ $env = new Environment();
$env->parent = $this->env;
$env->parentStore = $this->storeEnv;
$env->store = [];
}
if ($shouldThrow) {
- $this->throwError("Undefined variable \$$name" . ($maxDepth <= 0 ? " (infinite recursion)" : ""));
+ throw $this->error("Undefined variable \$$name" . ($maxDepth <= 0 ? ' (infinite recursion)' : ''));
}
// found nothing
* @api
*
* @param integer $numberPrecision
+ *
+ * @deprecated The number precision is not configurable anymore. The default is enough for all browsers.
*/
public function setNumberPrecision($numberPrecision)
{
- Node\Number::$precision = $numberPrecision;
+ @trigger_error('The number precision is not configurable anymore. '
+ . 'The default is enough for all browsers.', E_USER_DEPRECATED);
}
/**
* @api
*
* @param string $lineNumberStyle
+ *
+ * @deprecated The line number output is not supported anymore. Use source maps instead.
*/
public function setLineNumberStyle($lineNumberStyle)
{
- $this->lineNumberStyle = $lineNumberStyle;
+ @trigger_error('The line number output is not supported anymore. '
+ . 'Use source maps instead.', E_USER_DEPRECATED);
}
/**
* @api
*
* @param string $name
+ *
+ * @deprecated Registering additional features is deprecated.
*/
public function addFeature($name)
{
+ @trigger_error('Registering additional features is deprecated.', E_USER_DEPRECATED);
+
$this->registeredFeatures[$name] = true;
}
*/
protected function importFile($path, OutputBlock $out)
{
- $this->pushCallStack('import '.$path);
+ $this->pushCallStack('import ' . $path);
// see if tree is cached
$realPath = realpath($path);
if (! $hasExtension) {
$urls[] = "$url/index.scss";
- $urls[] = "$url/_index.scss";
// allow to find a plain css file, *if* no scss or partial scss is found
- $urls[] .= $url . ".css";
+ $urls[] .= $url . '.css';
}
}
if (\is_string($dir)) {
// check urls for normal import paths
foreach ($urls as $full) {
+ $found = [];
$separator = (
! empty($dir) &&
substr($dir, -1) !== '/' &&
$full = $dir . $separator . $full;
if (is_file($file = $full)) {
- return $file;
+ $found[] = $file;
+ }
+ if (! $isPartial) {
+ $full = dirname($full) . '/_' . basename($full);
+ if (is_file($file = $full)) {
+ $found[] = $file;
+ }
+ }
+ if ($found) {
+ if (\count($found) === 1) {
+ return reset($found);
+ }
+ if (\count($found) > 1) {
+ throw $this->error(
+ "Error: It's not clear which file to import. Found: " . implode(', ', $found)
+ );
+ }
}
}
} elseif (\is_callable($dir)) {
}
if ($urls) {
- if (! $hasExtension or preg_match('/[.]scss$/', $url)) {
- $this->throwError("`$url` file not found for @import");
+ if (! $hasExtension || preg_match('/[.]scss$/', $url)) {
+ throw $this->error("`$url` file not found for @import");
}
}
* @param boolean $ignoreErrors
*
* @return \ScssPhp\ScssPhp\Compiler
+ *
+ * @deprecated Ignoring Sass errors is not longer supported.
*/
public function setIgnoreErrors($ignoreErrors)
{
- $this->ignoreErrors = $ignoreErrors;
+ @trigger_error('Ignoring Sass errors is not longer supported.', E_USER_DEPRECATED);
return $this;
}
+ /**
+ * Get source position
+ *
+ * @api
+ *
+ * @return array
+ */
+ public function getSourcePosition()
+ {
+ $sourceFile = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : '';
+
+ return [$sourceFile, $this->sourceLine, $this->sourceColumn];
+ }
+
/**
* Throw error (exception)
*
* @param string $msg Message with optional sprintf()-style vararg parameters
*
* @throws \ScssPhp\ScssPhp\Exception\CompilerException
+ *
+ * @deprecated use "error" and throw the exception in the caller instead.
*/
public function throwError($msg)
{
- if ($this->ignoreErrors) {
- return;
- }
+ @trigger_error(
+ 'The method "throwError" is deprecated. Use "error" and throw the exception in the caller instead',
+ E_USER_DEPRECATED
+ );
+
+ throw $this->error(...func_get_args());
+ }
- if (\func_num_args() > 1) {
- $msg = \call_user_func_array('sprintf', \func_get_args());
+ /**
+ * Build an error (exception)
+ *
+ * @api
+ *
+ * @param string $msg Message with optional sprintf()-style vararg parameters
+ *
+ * @return CompilerException
+ */
+ public function error($msg, ...$args)
+ {
+ if ($args) {
+ $msg = sprintf($msg, ...$args);
}
if (! $this->ignoreCallStackMessage) {
}
}
- throw new CompilerException($msg);
+ return new CompilerException($msg);
+ }
+
+ /**
+ * @param string $functionName
+ * @param array $ExpectedArgs
+ * @param int $nbActual
+ * @return CompilerException
+ */
+ public function errorArgsNumber($functionName, $ExpectedArgs, $nbActual)
+ {
+ $nbExpected = \count($ExpectedArgs);
+
+ if ($nbActual > $nbExpected) {
+ return $this->error(
+ 'Error: Only %d arguments allowed in %s(), but %d were passed.',
+ $nbExpected,
+ $functionName,
+ $nbActual
+ );
+ } else {
+ $missing = [];
+
+ while (count($ExpectedArgs) && count($ExpectedArgs) > $nbActual) {
+ array_unshift($missing, array_pop($ExpectedArgs));
+ }
+
+ return $this->error(
+ 'Error: %s() argument%s %s missing.',
+ $functionName,
+ count($missing) > 1 ? 's' : '',
+ implode(', ', $missing)
+ );
+ }
}
/**
if ($this->callStack) {
foreach (array_reverse($this->callStack) as $call) {
if ($all || (isset($call['n']) && $call['n'])) {
- $msg = "#" . $ncall++ . " " . $call['n'] . " ";
+ $msg = '#' . $ncall++ . ' ' . $call['n'] . ' ';
$msg .= (isset($this->sourceNames[$call[Parser::SOURCE_INDEX]])
? $this->sourceNames[$call[Parser::SOURCE_INDEX]]
: '(unknown file)');
- $msg .= " on line " . $call[Parser::SOURCE_LINE];
+ $msg .= ' on line ' . $call[Parser::SOURCE_LINE];
$callStackMsg[] = $msg;
$file = $this->sourceNames[$env->block->sourceIndex];
if (realpath($file) === $name) {
- $this->throwError('An @import loop has been found: %s imports %s', $file, basename($file));
- break;
+ throw $this->error('An @import loop has been found: %s imports %s', $file, basename($file));
}
}
}
/**
* Call SCSS @function
*
- * @param string $name
+ * @param Object $func
* @param array $argValues
- * @param array $returnValue
*
- * @return boolean Returns true if returnValue is set; otherwise, false
+ * @return array $returnValue
*/
- protected function callScssFunction($name, $argValues, &$returnValue)
+ protected function callScssFunction($func, $argValues)
{
- $func = $this->get(static::$namespaces['function'] . $name, false);
-
if (! $func) {
- return false;
+ return static::$defaultValue;
}
+ $name = $func->name;
$this->pushEnv();
}
// throw away lines and children
- $tmp = new OutputBlock;
+ $tmp = new OutputBlock();
$tmp->lines = [];
$tmp->children = [];
if (! empty($func->parentEnv)) {
$this->env->declarationScopeParent = $func->parentEnv;
} else {
- $this->throwError("@function $name() without parentEnv");
+ throw $this->error("@function $name() without parentEnv");
}
- $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . " " . $name);
+ $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . ' ' . $name);
$this->popEnv();
- $returnValue = ! isset($ret) ? static::$defaultValue : $ret;
-
- return true;
+ return ! isset($ret) ? static::$defaultValue : $ret;
}
/**
* Call built-in and registered (PHP) functions
*
* @param string $name
+ * @param string|array $function
+ * @param array $prototype
* @param array $args
- * @param array $returnValue
*
- * @return boolean Returns true if returnValue is set; otherwise, false
+ * @return array
*/
- protected function callNativeFunction($name, $args, &$returnValue)
+ protected function callNativeFunction($name, $function, $prototype, $args)
{
- // try a lib function
- $name = $this->normalizeName($name);
- $libName = null;
+ $libName = (is_array($function) ? end($function) : null);
+ $sorted_kwargs = $this->sortNativeFunctionArgs($libName, $prototype, $args);
- if (isset($this->userFunctions[$name])) {
- // see if we can find a user function
- list($f, $prototype) = $this->userFunctions[$name];
- } elseif (($f = $this->getBuiltinFunction($name)) && \is_callable($f)) {
- $libName = $f[1];
- $prototype = isset(static::$$libName) ? static::$$libName : null;
- } else {
- return false;
+ if (\is_null($sorted_kwargs)) {
+ return null;
}
-
- @list($sorted, $kwargs) = $this->sortNativeFunctionArgs($libName, $prototype, $args);
+ @list($sorted, $kwargs) = $sorted_kwargs;
if ($name !== 'if' && $name !== 'call') {
$inExp = true;
}
}
- $returnValue = \call_user_func($f, $sorted, $kwargs);
+ $returnValue = \call_user_func($function, $sorted, $kwargs);
if (! isset($returnValue)) {
- return false;
+ return null;
}
- $returnValue = $this->coerceValue($returnValue);
-
- return true;
+ return $this->coerceValue($returnValue);
}
/**
*/
protected function getBuiltinFunction($name)
{
+ $libName = self::normalizeNativeFunctionName($name);
+ return [$this, $libName];
+ }
+
+ /**
+ * Normalize native function name
+ * @param $name
+ * @return string
+ */
+ public static function normalizeNativeFunctionName($name)
+ {
+ $name = str_replace("-", "_", $name);
$libName = 'lib' . preg_replace_callback(
'/_(.)/',
function ($m) {
},
ucfirst($name)
);
+ return $libName;
+ }
- return [$this, $libName];
+ /**
+ * Check if a function is a native built-in scss function, for css parsing
+ * @param $name
+ * @return bool
+ */
+ public static function isNativeFunction($name)
+ {
+ return method_exists(Compiler::class, self::normalizeNativeFunctionName($name));
}
/**
* @param array $prototypes
* @param array $args
*
- * @return array
+ * @return array|null
*/
protected function sortNativeFunctionArgs($functionName, $prototypes, $args)
{
$keyArgs = [];
$posArgs = [];
+ if (\is_array($args) && \count($args) && \end($args) === static::$null) {
+ array_pop($args);
+ }
+
// separate positional and keyword arguments
foreach ($args as $arg) {
list($key, $value) = $arg;
- $key = $key[1];
-
- if (empty($key)) {
+ if (empty($key) or empty($key[1])) {
$posArgs[] = empty($arg[2]) ? $value : $arg;
} else {
- $keyArgs[$key] = $value;
+ $keyArgs[$key[1]] = $value;
}
}
$this->ignoreCallStackMessage = true;
try {
+ if (\count($args) > \count($argDef)) {
+ $lastDef = end($argDef);
+
+ // check that last arg is not a ...
+ if (empty($lastDef[2])) {
+ throw $this->errorArgsNumber($functionName, $argDef, \count($args));
+ }
+ }
$vars = $this->applyArguments($argDef, $args, false, false);
// ensure all args are populated
}
if ($exceptionMessage && ! $prototypeHasMatch) {
- $this->throwError($exceptionMessage);
+ if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) {
+ // if var() or calc() is used as an argument, return as a css function
+ foreach ($args as $arg) {
+ if ($arg[1][0] === Type::T_FUNCTION_CALL && in_array($arg[1][1], ['var'])) {
+ return null;
+ }
+ }
+ }
+
+ throw $this->error($exceptionMessage);
}
return [$finalArgs, $keyArgs];
if ($storeInEnv) {
$storeEnv = $this->getStoreEnv();
- $env = new Environment;
+ $env = new Environment();
$env->store = $storeEnv->store;
}
$splatSeparator = null;
$keywordArgs = [];
$deferredKeywordArgs = [];
+ $deferredNamedKeywordArgs = [];
$remaining = [];
$hasKeywordArgument = false;
$hasKeywordArgument = true;
$name = $arg[0][1];
+
if (! isset($args[$name])) {
foreach (array_keys($args) as $an) {
- if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
+ if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) {
$name = $an;
break;
}
if (! isset($args[$name]) || $args[$name][3]) {
if ($hasVariable) {
- $deferredKeywordArgs[$name] = $arg[1];
+ $deferredNamedKeywordArgs[$name] = $arg[1];
} else {
- $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
- break;
+ throw $this->error("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
}
} elseif ($args[$name][0] < \count($remaining)) {
- $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
- break;
+ throw $this->error("The argument $%s was passed both by position and by name.", $arg[0][1]);
} else {
$keywordArgs[$name] = $arg[1];
}
- } elseif ($arg[2] === true) {
+ } elseif (! empty($arg[2])) {
+ // $arg[2] means a var followed by ... in the arg ($list... )
$val = $this->reduce($arg[1], true);
if ($val[0] === Type::T_LIST) {
if (! is_numeric($name)) {
if (! isset($args[$name])) {
foreach (array_keys($args) as $an) {
- if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
+ if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) {
$name = $an;
break;
}
if (! is_numeric($name)) {
if (! isset($args[$name])) {
foreach (array_keys($args) as $an) {
- if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
+ if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) {
$name = $an;
break;
}
$remaining[] = $val;
}
} elseif ($hasKeywordArgument) {
- $this->throwError('Positional arguments must come before keyword arguments.');
- break;
+ throw $this->error('Positional arguments must come before keyword arguments.');
} else {
$remaining[] = $arg[1];
}
list($i, $name, $default, $isVariable) = $arg;
if ($isVariable) {
+ // only if more than one arg : can not be passed as position and value
+ // see https://github.com/sass/libsass/issues/2927
+ if (count($args) > 1) {
+ if (isset($remaining[$i]) && isset($deferredNamedKeywordArgs[$name])) {
+ throw $this->error("The argument $%s was passed both by position and by name.", $name);
+ }
+ }
+
$val = [Type::T_LIST, \is_null($splatSeparator) ? ',' : $splatSeparator , [], $isVariable];
for ($count = \count($remaining); $i < $count; $i++) {
foreach ($deferredKeywordArgs as $itemName => $item) {
$val[2][$itemName] = $item;
}
+
+ foreach ($deferredNamedKeywordArgs as $itemName => $item) {
+ $val[2][$itemName] = $item;
+ }
} elseif (isset($remaining[$i])) {
$val = $remaining[$i];
} elseif (isset($keywordArgs[$name])) {
} elseif (! empty($default)) {
continue;
} else {
- $this->throwError("Missing argument $name");
- break;
+ throw $this->error("Missing argument $name");
}
if ($storeInEnv) {
*
* @param mixed $value
*
- * @return array|\ScssPhp\ScssPhp\Node\Number
+ * @return array|Number
*/
protected function coerceValue($value)
{
}
if (is_numeric($value)) {
- return new Node\Number($value, '');
+ return new Number($value, '');
}
if ($value === '') {
return $item;
}
- if ($item[0] === static::$emptyList[0] &&
+ if (
+ $item[0] === static::$emptyList[0] &&
$item[1] === static::$emptyList[1] &&
$item[2] === static::$emptyList[2]
) {
return static::$emptyMap;
}
- return [Type::T_MAP, [$item], [static::$null]];
+ return $item;
}
/**
return [Type::T_LIST, ',', $list];
}
- return [Type::T_LIST, $delim, ! isset($item) ? []: [$item]];
+ return [Type::T_LIST, $delim, ! isset($item) ? [] : [$item]];
}
/**
if ($color[3] === 255) {
$color[3] = 1; // fully opaque
} else {
- $color[3] = round($color[3] / 255, 3);
+ $color[3] = round($color[3] / 255, Number::PRECISION);
}
}
}
/**
- * @param integer|\ScssPhp\ScssPhp\Node\Number $value
- * @param boolean $isAlpha
+ * @param integer|Number $value
+ * @param boolean $isAlpha
*
* @return integer|mixed
*/
* @param integer|float $min
* @param integer|float $max
* @param boolean $isInt
- * @param boolean $clamp
- * @param boolean $modulo
*
* @return integer|mixed
*/
- protected function compileColorPartValue($value, $min, $max, $isInt = true, $clamp = true, $modulo = false)
+ protected function compileColorPartValue($value, $min, $max, $isInt = true)
{
if (! is_numeric($value)) {
if (\is_array($value)) {
$reduced = $this->reduce($value);
- if (\is_object($reduced) && $value->type === Type::T_NUMBER) {
+ if ($reduced instanceof Number) {
$value = $reduced;
}
}
- if (\is_object($value) && $value->type === Type::T_NUMBER) {
- $num = $value->dimension;
-
- if (\count($value->units)) {
- $unit = array_keys($value->units);
- $unit = reset($unit);
-
- switch ($unit) {
- case '%':
- $num *= $max / 100;
- break;
- default:
- break;
- }
+ if ($value instanceof Number) {
+ if ($value->unitless()) {
+ $num = $value->getDimension();
+ } elseif ($value->hasUnit('%')) {
+ $num = $max * $value->getDimension() / 100;
+ } else {
+ throw $this->error('Expected %s to have no units or "%%".', $value);
}
$value = $num;
$value = round($value);
}
- if ($clamp) {
- $value = min($max, max($min, $value));
- }
-
- if ($modulo) {
- $value = $value % $max;
-
- // still negative?
- while ($value < $min) {
- $value += $max;
- }
- }
+ $value = min($max, max($min, $value));
return $value;
}
return [Type::T_STRING, '', [$this->compileValue($value)]];
}
+ /**
+ * Assert value is a string (or keyword)
+ *
+ * @api
+ *
+ * @param array $value
+ * @param string $varName
+ *
+ * @return array
+ *
+ * @throws \Exception
+ */
+ public function assertString($value, $varName = null)
+ {
+ // case of url(...) parsed a a function
+ if ($value[0] === Type::T_FUNCTION) {
+ $value = $this->coerceString($value);
+ }
+
+ if (! \in_array($value[0], [Type::T_STRING, Type::T_KEYWORD])) {
+ $value = $this->compileValue($value);
+ $var_display = ($varName ? " \${$varName}:" : '');
+ throw $this->error("Error:{$var_display} $value is not a string.");
+ }
+
+ $value = $this->coerceString($value);
+
+ return $value;
+ }
+
/**
* Coerce value to a percentage
*
*/
protected function coercePercent($value)
{
- if ($value[0] === Type::T_NUMBER) {
- if (! empty($value[2]['%'])) {
- return $value[1] / 100;
+ if ($value instanceof Number) {
+ if ($value->hasUnit('%')) {
+ return $value->getDimension() / 100;
}
- return $value[1];
+ return $value->getDimension();
}
return 0;
$value = $this->coerceMap($value);
if ($value[0] !== Type::T_MAP) {
- $this->throwError('expecting map, %s received', $value[0]);
+ throw $this->error('expecting map, %s received', $value[0]);
}
return $value;
public function assertList($value)
{
if ($value[0] !== Type::T_LIST) {
- $this->throwError('expecting list, %s received', $value[0]);
+ throw $this->error('expecting list, %s received', $value[0]);
}
return $value;
return $color;
}
- $this->throwError('expecting color, %s received', $value[0]);
+ throw $this->error('expecting color, %s received', $value[0]);
}
/**
*
* @api
*
+ * @param mixed $value
+ * @param string $varName
+ *
+ * @return Number
+ *
+ * @throws \Exception
+ */
+ public function assertNumber($value, $varName = null)
+ {
+ if (!$value instanceof Number) {
+ $value = $this->compileValue($value);
+ $var_display = ($varName ? " \${$varName}:" : '');
+ throw $this->error("Error:{$var_display} $value is not a number.");
+ }
+
+ return $value;
+ }
+
+ /**
+ * Assert value is a integer
+ *
+ * @api
+ *
* @param array $value
+ * @param string $varName
*
* @return integer|float
*
* @throws \Exception
*/
- public function assertNumber($value)
+ public function assertInteger($value, $varName = null)
{
- if ($value[0] !== Type::T_NUMBER) {
- $this->throwError('expecting number, %s received', $value[0]);
+
+ $value = $this->assertNumber($value, $varName)->getDimension();
+ if (round($value - \intval($value), Number::PRECISION) > 0) {
+ $var_display = ($varName ? " \${$varName}:" : '');
+ throw $this->error("Error:{$var_display} $value is not an integer.");
}
- return $value[1];
+ return intval($value);
}
+
/**
* Make sure a color's components don't go out of bounds
*
}
if ($h * 3 < 2) {
- return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
+ return $m1 + ($m2 - $m1) * (2 / 3 - $h) * 6;
}
return $m1;
$m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
$m1 = $l * 2 - $m2;
- $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
+ $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;
+ $b = $this->hueToRGB($m1, $m2, $h - 1 / 3) * 255;
$out = [Type::T_COLOR, $r, $g, $b];
// Built in functions
- protected static $libCall = ['name', 'args...'];
+ protected static $libCall = ['function', 'args...'];
protected function libCall($args, $kwargs)
{
- $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
+ $functionReference = $this->reduce(array_shift($args), true);
+
+ if (in_array($functionReference[0], [Type::T_STRING, Type::T_KEYWORD])) {
+ $name = $this->compileStringContent($this->coerceString($this->reduce($functionReference, true)));
+ $warning = "DEPRECATION WARNING: Passing a string to call() is deprecated and will be illegal\n"
+ . "in Sass 4.0. Use call(function-reference($name)) instead.";
+ fwrite($this->stderr, "$warning\n\n");
+ $functionReference = $this->libGetFunction([$functionReference]);
+ }
+
+ if ($functionReference === static::$null) {
+ return static::$null;
+ }
+
+ if (! in_array($functionReference[0], [Type::T_FUNCTION_REFERENCE, Type::T_FUNCTION])) {
+ throw $this->error('Function reference expected, got ' . $functionReference[0]);
+ }
+
$callArgs = [];
// $kwargs['args'] is [Type::T_LIST, ',', [..]]
$callArgs[] = [$varname, $arg, false];
}
- return $this->reduce([Type::T_FUNCTION_CALL, $name, $callArgs]);
+ return $this->reduce([Type::T_FUNCTION_CALL, $functionReference, $callArgs]);
+ }
+
+
+ protected static $libGetFunction = [
+ ['name'],
+ ['name', 'css']
+ ];
+ protected function libGetFunction($args)
+ {
+ $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
+ $isCss = false;
+
+ if (count($args)) {
+ $isCss = $this->reduce(array_shift($args), true);
+ $isCss = (($isCss === static::$true) ? true : false);
+ }
+
+ if ($isCss) {
+ return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]];
+ }
+
+ return $this->getFunctionReference($name, true);
}
protected static $libIf = ['condition', 'if-true', 'if-false:'];
{
list($list, $value) = $args;
- if ($list[0] === Type::T_MAP ||
+ if (
+ $list[0] === Type::T_MAP ||
$list[0] === Type::T_STRING ||
$list[0] === Type::T_KEYWORD ||
$list[0] === Type::T_INTERPOLATE
return static::$null;
}
+ // Numbers are represented with value objects, for which the PHP equality operator does not
+ // match the Sass rules (and we cannot overload it). As they are the only type of values
+ // represented with a value object for now, they require a special case.
+ if ($value instanceof Number) {
+ $key = 0;
+ foreach ($list[2] as $item) {
+ $key++;
+ $itemValue = $this->normalizeValue($item);
+
+ if ($itemValue instanceof Number && $value->equals($itemValue)) {
+ return new Number($key, '');
+ }
+ }
+ return static::$null;
+ }
+
$values = [];
+
foreach ($list[2] as $item) {
$values[] = $this->normalizeValue($item);
}
$color = [Type::T_COLOR, $args[0], $args[1], $args[2]];
if (! $color = $this->coerceColor($color)) {
- $color = [Type::T_STRING, '', [$funcName .'(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
+ $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
}
return $color;
foreach ([1 => 1, 2 => 2, 3 => 3, 7 => 4] as $iarg => $irgba) {
if (isset($args[$iarg])) {
- $val = $this->assertNumber($args[$iarg]);
+ $val = $this->assertNumber($args[$iarg])->getDimension();
if (! isset($color[$irgba])) {
$color[$irgba] = (($irgba < 4) ? 0 : 1);
foreach ([4 => 1, 5 => 2, 6 => 3] as $iarg => $ihsl) {
if (! empty($args[$iarg])) {
- $val = $this->assertNumber($args[$iarg]);
+ $val = $this->assertNumber($args[$iarg])->getDimension();
$hsl[$ihsl] = \call_user_func($fn, $hsl[$ihsl], $val, $iarg);
}
}
protected function libIeHexStr($args)
{
$color = $this->coerceColor($args[0]);
+
+ if (\is_null($color)) {
+ $this->throwError('Error: argument `$color` of `ie-hex-str($color)` must be a color');
+ }
+
$color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
return [Type::T_STRING, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]];
{
$color = $this->coerceColor($args[0]);
+ if (\is_null($color)) {
+ $this->throwError('Error: argument `$color` of `red($color)` must be a color');
+ }
+
return $color[1];
}
{
$color = $this->coerceColor($args[0]);
+ if (\is_null($color)) {
+ $this->throwError('Error: argument `$color` of `green($color)` must be a color');
+ }
+
return $color[2];
}
{
$color = $this->coerceColor($args[0]);
+ if (\is_null($color)) {
+ $this->throwError('Error: argument `$color` of `blue($color)` must be a color');
+ }
+
return $color[3];
}
{
$value = $args[0];
- if ($value[0] === Type::T_NUMBER) {
+ if ($value instanceof Number) {
return null;
}
}
// mix two colors
- protected static $libMix = ['color-1', 'color-2', 'weight:0.5'];
+ protected static $libMix = [
+ ['color1', 'color2', 'weight:0.5'],
+ ['color-1', 'color-2', 'weight:0.5']
+ ];
protected function libMix($args)
{
list($first, $second, $weight) = $args;
return $this->fixColor($new);
}
- protected static $libHsl =[
+ protected static $libHsl = [
['channels'],
['hue', 'saturation', 'lightness'],
['hue', 'saturation', 'lightness', 'alpha'] ];
protected function libHsl($args, $kwargs, $funcName = 'hsl')
{
+ $args_to_check = $args;
+
if (\count($args) == 1) {
if ($args[0][0] !== Type::T_LIST || \count($args[0][2]) < 3 || \count($args[0][2]) > 4) {
return [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
}
$args = $args[0][2];
+ $args_to_check = $kwargs['channels'][2];
}
- $hue = $this->compileColorPartValue($args[0], 0, 360, false, false, true);
- $saturation = $this->compileColorPartValue($args[1], 0, 100, false);
- $lightness = $this->compileColorPartValue($args[2], 0, 100, false);
+ foreach ($kwargs as $k => $arg) {
+ if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
+ return null;
+ }
+ }
+
+ foreach ($args_to_check as $k => $arg) {
+ if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
+ if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) {
+ return null;
+ }
+
+ $args[$k] = $this->stringifyFncallArgs($arg);
+ }
+ if (
+ $k >= 2 && count($args) === 4 &&
+ in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) &&
+ in_array($arg[1], ['calc','env'])
+ ) {
+ return null;
+ }
+ }
+
+ $hue = $this->reduce($args[0]);
+ $saturation = $this->reduce($args[1]);
+ $lightness = $this->reduce($args[2]);
$alpha = null;
if (\count($args) === 4) {
$alpha = $this->compileColorPartValue($args[3], 0, 100, false);
- if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness) || ! is_numeric($alpha)) {
+ if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number || ! is_numeric($alpha)) {
return [Type::T_STRING, '',
[$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
}
} else {
- if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness)) {
+ if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number) {
return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
}
}
- $color = $this->toRGB($hue, $saturation, $lightness);
+ $hueValue = $hue->getDimension() % 360;
+
+ while ($hueValue < 0) {
+ $hueValue += 360;
+ }
+
+ $color = $this->toRGB($hueValue, max(0, min($saturation->getDimension(), 100)), max(0, min($lightness->getDimension(), 100)));
if (! \is_null($alpha)) {
$color[4] = $alpha;
protected static $libHsla = [
['channels'],
- ['hue', 'saturation', 'lightness', 'alpha:1'] ];
+ ['hue', 'saturation', 'lightness'],
+ ['hue', 'saturation', 'lightness', 'alpha']];
protected function libHsla($args, $kwargs)
{
return $this->libHsl($args, $kwargs, 'hsla');
$color = $this->assertColor($args[0]);
$hsl = $this->toHSL($color[1], $color[2], $color[3]);
- return new Node\Number($hsl[1], 'deg');
+ return new Number($hsl[1], 'deg');
}
protected static $libSaturation = ['color'];
$color = $this->assertColor($args[0]);
$hsl = $this->toHSL($color[1], $color[2], $color[3]);
- return new Node\Number($hsl[2], '%');
+ return new Number($hsl[2], '%');
}
protected static $libLightness = ['color'];
$color = $this->assertColor($args[0]);
$hsl = $this->toHSL($color[1], $color[2], $color[3]);
- return new Node\Number($hsl[3], '%');
+ return new Number($hsl[3], '%');
}
protected function adjustHsl($color, $idx, $amount)
protected function libAdjustHue($args)
{
$color = $this->assertColor($args[0]);
- $degrees = $this->assertNumber($args[1]);
+ $degrees = $this->assertNumber($args[1])->getDimension();
return $this->adjustHsl($color, 1, $degrees);
}
return $this->adjustHsl($color, 3, -$amount);
}
- protected static $libSaturate = [['color', 'amount'], ['number']];
+ protected static $libSaturate = [['color', 'amount'], ['amount']];
protected function libSaturate($args)
{
$value = $args[0];
- if ($value[0] === Type::T_NUMBER) {
+ if ($value instanceof Number) {
return null;
}
+ if (count($args) === 1) {
+ $val = $this->compileValue($value);
+ throw $this->error("\$amount: $val is not a number");
+ }
+
$color = $this->assertColor($value);
$amount = 100 * $this->coercePercent($args[1]);
{
$value = $args[0];
- if ($value[0] === Type::T_NUMBER) {
+ if ($value instanceof Number) {
return null;
}
$weight = $this->coercePercent($weight);
}
- if ($value[0] === Type::T_NUMBER) {
+ if ($value instanceof Number) {
return null;
}
$inverted[3] = 255 - $inverted[3];
if ($weight < 1) {
- return $this->libMix([$inverted, $color, [Type::T_NUMBER, $weight]]);
+ return $this->libMix([$inverted, $color, new Number($weight, '')]);
}
return $inverted;
$value = $args[0];
if ($value[0] === Type::T_STRING && ! empty($value[1])) {
+ $value[1] = '"';
return $value;
}
protected static $libPercentage = ['number'];
protected function libPercentage($args)
{
- return new Node\Number($this->coercePercent($args[0]) * 100, '%');
+ $num = $this->assertNumber($args[0], 'number');
+ $num->assertNoUnits('number');
+
+ return new Number($num->getDimension() * 100, '%');
}
protected static $libRound = ['number'];
protected function libRound($args)
{
- $num = $args[0];
+ $num = $this->assertNumber($args[0], 'number');
- return new Node\Number(round($num[1]), $num[2]);
+ return new Number(round($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
}
protected static $libFloor = ['number'];
protected function libFloor($args)
{
- $num = $args[0];
+ $num = $this->assertNumber($args[0], 'number');
- return new Node\Number(floor($num[1]), $num[2]);
+ return new Number(floor($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
}
protected static $libCeil = ['number'];
protected function libCeil($args)
{
- $num = $args[0];
+ $num = $this->assertNumber($args[0], 'number');
- return new Node\Number(ceil($num[1]), $num[2]);
+ return new Number(ceil($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
}
protected static $libAbs = ['number'];
protected function libAbs($args)
{
- $num = $args[0];
+ $num = $this->assertNumber($args[0], 'number');
- return new Node\Number(abs($num[1]), $num[2]);
+ return new Number(abs($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
}
protected function libMin($args)
{
- $numbers = $this->getNormalizedNumbers($args);
- $minOriginal = null;
- $minNormalized = null;
+ /**
+ * @var Number|null
+ */
+ $min = null;
- foreach ($numbers as $key => $pair) {
- list($original, $normalized) = $pair;
+ foreach ($args as $arg) {
+ $number = $this->assertNumber($arg);
- if (\is_null($normalized) or \is_null($minNormalized)) {
- if (\is_null($minOriginal) || $original[1] <= $minOriginal[1]) {
- $minOriginal = $original;
- $minNormalized = $normalized;
- }
- } elseif ($normalized[1] <= $minNormalized[1]) {
- $minOriginal = $original;
- $minNormalized = $normalized;
+ if (\is_null($min) || $min->greaterThan($number)) {
+ $min = $number;
}
}
- return $minOriginal;
- }
-
- protected function libMax($args)
- {
- $numbers = $this->getNormalizedNumbers($args);
- $maxOriginal = null;
- $maxNormalized = null;
-
- foreach ($numbers as $key => $pair) {
- list($original, $normalized) = $pair;
-
- if (\is_null($normalized) or \is_null($maxNormalized)) {
- if (\is_null($maxOriginal) || $original[1] >= $maxOriginal[1]) {
- $maxOriginal = $original;
- $maxNormalized = $normalized;
- }
- } elseif ($normalized[1] >= $maxNormalized[1]) {
- $maxOriginal = $original;
- $maxNormalized = $normalized;
- }
+ if (!\is_null($min)) {
+ return $min;
}
- return $maxOriginal;
+ throw $this->error('At least one argument must be passed.');
}
- /**
- * Helper to normalize args containing numbers
- *
- * @param array $args
- *
- * @return array
- */
- protected function getNormalizedNumbers($args)
+ protected function libMax($args)
{
- $unit = null;
- $originalUnit = null;
- $numbers = [];
-
- foreach ($args as $key => $item) {
- if ($item[0] !== Type::T_NUMBER) {
- $this->throwError('%s is not a number', $item[0]);
- break;
- }
+ /**
+ * @var Number|null
+ */
+ $max = null;
- $number = $item->normalize();
+ foreach ($args as $arg) {
+ $number = $this->assertNumber($arg);
- if (empty($unit)) {
- $unit = $number[2];
- $originalUnit = $item->unitStr();
- } elseif ($number[1] && $unit !== $number[2] && ! empty($number[2])) {
- $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr());
- break;
+ if (\is_null($max) || $max->lessThan($number)) {
+ $max = $number;
}
+ }
- $numbers[$key] = [$args[$key], empty($number[2]) ? null : $number];
+ if (!\is_null($max)) {
+ return $max;
}
- return $numbers;
+ throw $this->error('At least one argument must be passed.');
}
protected static $libLength = ['list'];
return 'comma';
}
+ if (! \in_array($args[0][0], [Type::T_LIST, Type::T_MAP])) {
+ return 'space';
+ }
+
$list = $this->coerceList($args[0]);
- if (\count($list[2]) <= 1) {
+ if (\count($list[2]) <= 1 && empty($list['enclosing'])) {
return 'space';
}
protected function libNth($args)
{
$list = $this->coerceList($args[0], ',', false);
- $n = $this->assertNumber($args[1]);
+ $n = $this->assertNumber($args[1])->getDimension();
if ($n > 0) {
$n--;
protected function libSetNth($args)
{
$list = $this->coerceList($args[0]);
- $n = $this->assertNumber($args[1]);
+ $n = $this->assertNumber($args[1])->getDimension();
if ($n > 0) {
$n--;
}
if (! isset($list[2][$n])) {
- $this->throwError('Invalid argument for "n"');
-
- return null;
+ throw $this->error('Invalid argument for "n"');
}
$list[2][$n] = $args[2];
return [Type::T_LIST, ',', $values];
}
- protected static $libMapRemove = ['map', 'key'];
+ protected static $libMapRemove = ['map', 'key...'];
protected function libMapRemove($args)
{
$map = $this->assertMap($args[0]);
- $key = $this->compileStringContent($this->coerceString($args[1]));
+ $keyList = $this->assertList($args[1]);
+
+ $keys = [];
+
+ foreach ($keyList[2] as $key) {
+ $keys[] = $this->compileStringContent($this->coerceString($key));
+ }
for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
- if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
+ if (in_array($this->compileStringContent($this->coerceString($map[1][$i])), $keys)) {
array_splice($map[1], $i, 1);
array_splice($map[2], $i, 1);
}
return false;
}
- protected static $libMapMerge = ['map-1', 'map-2'];
+ protected static $libMapMerge = [
+ ['map1', 'map2'],
+ ['map-1', 'map-2']
+ ];
protected function libMapMerge($args)
{
$map1 = $this->assertMap($args[0]);
$lists = [];
$firstList = array_shift($args);
- foreach ($firstList[2] as $key => $item) {
- $list = [Type::T_LIST, '', [$item]];
+ $result = [Type::T_LIST, ',', $lists];
+ if (! \is_null($firstList)) {
+ foreach ($firstList[2] as $key => $item) {
+ $list = [Type::T_LIST, '', [$item]];
- foreach ($args as $arg) {
- if (isset($arg[2][$key])) {
- $list[2][] = $arg[2][$key];
- } else {
- break 2;
+ foreach ($args as $arg) {
+ if (isset($arg[2][$key])) {
+ $list[2][] = $arg[2][$key];
+ } else {
+ break 2;
+ }
}
+
+ $lists[] = $list;
}
- $lists[] = $list;
+ $result[2] = $lists;
+ } else {
+ $result['enclosing'] = 'parent';
}
- return [Type::T_LIST, ',', $lists];
+ return $result;
}
protected static $libTypeOf = ['value'];
case Type::T_FUNCTION:
return 'string';
+ case Type::T_FUNCTION_REFERENCE:
+ return 'function';
+
case Type::T_LIST:
if (isset($value[3]) && $value[3]) {
return 'arglist';
{
$num = $args[0];
- if ($num[0] === Type::T_NUMBER) {
+ if ($num instanceof Number) {
return [Type::T_STRING, '"', [$num->unitStr()]];
}
{
$value = $args[0];
- return $value[0] === Type::T_NUMBER && $value->unitless();
+ return $value instanceof Number && $value->unitless();
}
- protected static $libComparable = ['number-1', 'number-2'];
+ protected static $libComparable = [
+ ['number1', 'number2'],
+ ['number-1', 'number-2']
+ ];
protected function libComparable($args)
{
list($number1, $number2) = $args;
- if (! isset($number1[0]) || $number1[0] !== Type::T_NUMBER ||
- ! isset($number2[0]) || $number2[0] !== Type::T_NUMBER
+ if (
+ ! $number1 instanceof Number ||
+ ! $number2 instanceof Number
) {
- $this->throwError('Invalid argument(s) for "comparable"');
-
- return null;
+ throw $this->error('Invalid argument(s) for "comparable"');
}
- $number1 = $number1->normalize();
- $number2 = $number2->normalize();
-
- return $number1[2] === $number2[2] || $number1->unitless() || $number2->unitless();
+ return $number1->isComparableTo($number2);
}
protected static $libStrIndex = ['string', 'substring'];
protected function libStrIndex($args)
{
- $string = $this->coerceString($args[0]);
+ $string = $this->assertString($args[0], 'string');
$stringContent = $this->compileStringContent($string);
- $substring = $this->coerceString($args[1]);
+ $substring = $this->assertString($args[1], 'substring');
$substringContent = $this->compileStringContent($substring);
- $result = strpos($stringContent, $substringContent);
+ if (! \strlen($substringContent)) {
+ $result = 0;
+ } else {
+ $result = strpos($stringContent, $substringContent);
+ }
- return $result === false ? static::$null : new Node\Number($result + 1, '');
+ return $result === false ? static::$null : new Number($result + 1, '');
}
protected static $libStrInsert = ['string', 'insert', 'index'];
protected function libStrInsert($args)
{
- $string = $this->coerceString($args[0]);
+ $string = $this->assertString($args[0], 'string');
$stringContent = $this->compileStringContent($string);
- $insert = $this->coerceString($args[1]);
+ $insert = $this->assertString($args[1], 'insert');
$insertContent = $this->compileStringContent($insert);
- list(, $index) = $args[2];
+ $index = $this->assertInteger($args[2], 'index');
+ if ($index > 0) {
+ $index = $index - 1;
+ }
+ if ($index < 0) {
+ $index = Util::mbStrlen($stringContent) + 1 + $index;
+ }
- $string[2] = [substr_replace($stringContent, $insertContent, $index - 1, 0)];
+ $string[2] = [
+ Util::mbSubstr($stringContent, 0, $index),
+ $insertContent,
+ Util::mbSubstr($stringContent, $index)
+ ];
return $string;
}
protected static $libStrLength = ['string'];
protected function libStrLength($args)
{
- $string = $this->coerceString($args[0]);
+ $string = $this->assertString($args[0], 'string');
$stringContent = $this->compileStringContent($string);
- return new Node\Number(\strlen($stringContent), '');
+ return new Number(Util::mbStrlen($stringContent), '');
}
protected static $libStrSlice = ['string', 'start-at', 'end-at:-1'];
return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']];
}
- protected static $libRandom = ['limit:1'];
+ protected static $libRandom = ['limit:null'];
protected function libRandom($args)
{
- if (isset($args[0])) {
- $n = $this->assertNumber($args[0]);
+ if (isset($args[0]) & $args[0] !== static::$null) {
+ $n = $this->assertNumber($args[0])->getDimension();
if ($n < 1) {
- $this->throwError("\$limit must be greater than or equal to 1");
-
- return null;
+ throw $this->error("\$limit must be greater than or equal to 1");
}
- if ($n - \intval($n) > 0) {
- $this->throwError("Expected \$limit to be an integer but got $n for `random`");
-
- return null;
+ if (round($n - \intval($n), Number::PRECISION) > 0) {
+ throw $this->error("Expected \$limit to be an integer but got $n for `random`");
}
- return new Node\Number(mt_rand(1, \intval($n)), '');
+ return new Number(mt_rand(1, \intval($n)), '');
}
- return new Node\Number(mt_rand(1, mt_getrandmax()), '');
+ $max = mt_getrandmax();
+ return new Number(mt_rand(0, $max - 1) / $max, '');
}
protected function libUniqueId()
$force_enclosing_display = true;
}
- if (! empty($value['enclosing']) &&
+ if (
+ ! empty($value['enclosing']) &&
($force_enclosing_display ||
($value['enclosing'] === 'bracket') ||
! \count($value[2]))
) {
- $value['enclosing'] = 'forced_'.$value['enclosing'];
+ $value['enclosing'] = 'forced_' . $value['enclosing'];
$force_enclosing_display = true;
}
*
* @return array|boolean
*/
- protected function getSelectorArg($arg)
+ protected function getSelectorArg($arg, $varname = null, $allowParent = false)
{
static $parser = null;
$parser = $this->parserFactory(__METHOD__);
}
+ if (! $this->checkSelectorArgType($arg)) {
+ $var_display = ($varname ? ' $' . $varname . ':' : '');
+ $var_value = $this->compileValue($arg);
+ throw $this->error("Error:{$var_display} $var_value is not a valid selector: it must be a string,"
+ . " a list of strings, or a list of lists of strings");
+ }
+
$arg = $this->libUnquote([$arg]);
$arg = $this->compileValue($arg);
$parsedSelector = [];
- if ($parser->parseSelector($arg, $parsedSelector)) {
+ if ($parser->parseSelector($arg, $parsedSelector, true)) {
$selector = $this->evalSelectors($parsedSelector);
$gluedSelector = $this->glueFunctionSelectors($selector);
+ if (! $allowParent) {
+ foreach ($gluedSelector as $selector) {
+ foreach ($selector as $s) {
+ if (in_array(static::$selfSelector, $s)) {
+ $var_display = ($varname ? ' $' . $varname . ':' : '');
+ throw $this->error("Error:{$var_display} Parent selectors aren't allowed here.");
+ }
+ }
+ }
+ }
+
return $gluedSelector;
}
- return false;
+ $var_display = ($varname ? ' $' . $varname . ':' : '');
+ throw $this->error("Error:{$var_display} expected more input, invalid selector.");
+ }
+
+ /**
+ * Check variable type for getSelectorArg() function
+ * @param array $arg
+ * @param int $maxDepth
+ * @return bool
+ */
+ protected function checkSelectorArgType($arg, $maxDepth = 2)
+ {
+ if ($arg[0] === Type::T_LIST && $maxDepth > 0) {
+ foreach ($arg[2] as $elt) {
+ if (! $this->checkSelectorArgType($elt, $maxDepth - 1)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ if (!in_array($arg[0], [Type::T_STRING, Type::T_KEYWORD])) {
+ return false;
+ }
+ return true;
}
/**
{
list($super, $sub) = $args;
- $super = $this->getSelectorArg($super);
- $sub = $this->getSelectorArg($sub);
+ $super = $this->getSelectorArg($super, 'super');
+ $sub = $this->getSelectorArg($sub, 'sub');
return $this->isSuperSelector($super, $sub);
}
protected function isSuperSelector($super, $sub)
{
// one and only one selector for each arg
- if (! $super || \count($super) !== 1) {
- $this->throwError("Invalid super selector for isSuperSelector()");
+ if (! $super) {
+ throw $this->error('Invalid super selector for isSuperSelector()');
}
- if (! $sub || \count($sub) !== 1) {
- $this->throwError("Invalid sub selector for isSuperSelector()");
+ if (! $sub) {
+ throw $this->error('Invalid sub selector for isSuperSelector()');
+ }
+
+ if (count($sub) > 1) {
+ foreach ($sub as $s) {
+ if (! $this->isSuperSelector($super, [$s])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ if (count($super) > 1) {
+ foreach ($super as $s) {
+ if ($this->isSuperSelector([$s], $sub)) {
+ return true;
+ }
+ }
+ return false;
}
$super = reset($super);
$args = $args[2];
if (\count($args) < 1) {
- $this->throwError("selector-append() needs at least 1 argument");
+ throw $this->error('selector-append() needs at least 1 argument');
}
- $selectors = array_map([$this, 'getSelectorArg'], $args);
+ $selectors = [];
+ foreach ($args as $arg) {
+ $selectors[] = $this->getSelectorArg($arg, 'selector');
+ }
return $this->formatOutputSelector($this->selectorAppend($selectors));
}
$lastSelectors = array_pop($selectors);
if (! $lastSelectors) {
- $this->throwError("Invalid selector list in selector-append()");
+ throw $this->error('Invalid selector list in selector-append()');
}
while (\count($selectors)) {
$previousSelectors = array_pop($selectors);
if (! $previousSelectors) {
- $this->throwError("Invalid selector list in selector-append()");
+ throw $this->error('Invalid selector list in selector-append()');
}
// do the trick, happening $lastSelector to $previousSelector
return $lastSelectors;
}
- protected static $libSelectorExtend = ['selectors', 'extendee', 'extender'];
+ protected static $libSelectorExtend = [
+ ['selector', 'extendee', 'extender'],
+ ['selectors', 'extendee', 'extender']
+ ];
protected function libSelectorExtend($args)
{
list($selectors, $extendee, $extender) = $args;
- $selectors = $this->getSelectorArg($selectors);
- $extendee = $this->getSelectorArg($extendee);
- $extender = $this->getSelectorArg($extender);
+ $selectors = $this->getSelectorArg($selectors, 'selector');
+ $extendee = $this->getSelectorArg($extendee, 'extendee');
+ $extender = $this->getSelectorArg($extender, 'extender');
if (! $selectors || ! $extendee || ! $extender) {
- $this->throwError("selector-extend() invalid arguments");
+ throw $this->error('selector-extend() invalid arguments');
}
$extended = $this->extendOrReplaceSelectors($selectors, $extendee, $extender);
return $this->formatOutputSelector($extended);
}
- protected static $libSelectorReplace = ['selectors', 'original', 'replacement'];
+ protected static $libSelectorReplace = [
+ ['selector', 'original', 'replacement'],
+ ['selectors', 'original', 'replacement']
+ ];
protected function libSelectorReplace($args)
{
list($selectors, $original, $replacement) = $args;
- $selectors = $this->getSelectorArg($selectors);
- $original = $this->getSelectorArg($original);
- $replacement = $this->getSelectorArg($replacement);
+ $selectors = $this->getSelectorArg($selectors, 'selector');
+ $original = $this->getSelectorArg($original, 'original');
+ $replacement = $this->getSelectorArg($replacement, 'replacement');
if (! $selectors || ! $original || ! $replacement) {
- $this->throwError("selector-replace() invalid arguments");
+ throw $this->error('selector-replace() invalid arguments');
}
$replaced = $this->extendOrReplaceSelectors($selectors, $original, $replacement, true);
$this->matchExtends($selector, $extended);
// if didnt match, keep the original selector if we are in a replace operation
- if ($replace and \count($extended) === $n) {
+ if ($replace && \count($extended) === $n) {
$extended[] = $selector;
}
}
$args = $args[2];
if (\count($args) < 1) {
- $this->throwError("selector-nest() needs at least 1 argument");
+ throw $this->error('selector-nest() needs at least 1 argument');
+ }
+
+ $selectorsMap = [];
+ foreach ($args as $arg) {
+ $selectorsMap[] = $this->getSelectorArg($arg, 'selector', true);
}
- $selectorsMap = array_map([$this, 'getSelectorArg'], $args);
$envs = [];
foreach ($selectorsMap as $selectors) {
return $this->formatOutputSelector($outputSelectors);
}
- protected static $libSelectorParse = ['selectors'];
+ protected static $libSelectorParse = [
+ ['selector'],
+ ['selectors']
+ ];
protected function libSelectorParse($args)
{
$selectors = reset($args);
- $selectors = $this->getSelectorArg($selectors);
+ $selectors = $this->getSelectorArg($selectors, 'selector');
return $this->formatOutputSelector($selectors);
}
{
list($selectors1, $selectors2) = $args;
- $selectors1 = $this->getSelectorArg($selectors1);
- $selectors2 = $this->getSelectorArg($selectors2);
+ $selectors1 = $this->getSelectorArg($selectors1, 'selectors1');
+ $selectors2 = $this->getSelectorArg($selectors2, 'selectors2');
if (! $selectors1 || ! $selectors2) {
- $this->throwError("selector-unify() invalid arguments");
+ throw $this->error('selector-unify() invalid arguments');
}
// only consider the first compound of each
protected function libSimpleSelectors($args)
{
$selector = reset($args);
- $selector = $this->getSelectorArg($selector);
+ $selector = $this->getSelectorArg($selector, 'selector');
// remove selectors list layer, keeping the first one
$selector = reset($selector);
<?php
+
/**
* SCSSPHP
*
'and' => 2,
'==' => 3,
'!=' => 3,
- '<=>' => 3,
'<=' => 4,
'>=' => 4,
'<' => 4,
$this->cssOnly = $cssOnly;
if (empty(static::$operatorPattern)) {
- static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
+ static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)';
$commentSingle = '\/\/';
$commentMultiLeft = '\/\*';
* @param string $msg
*
* @throws \ScssPhp\ScssPhp\Exception\ParserException
+ *
+ * @deprecated use "parseError" and throw the exception in the caller instead.
*/
public function throwParseError($msg = 'parse error')
+ {
+ @trigger_error(
+ 'The method "throwParseError" is deprecated. Use "parseError" and throw the exception in the caller instead',
+ E_USER_DEPRECATED
+ );
+
+ throw $this->parseError($msg);
+ }
+
+ /**
+ * Creates a parser error
+ *
+ * @api
+ *
+ * @param string $msg
+ *
+ * @return ParserException
+ */
+ public function parseError($msg = 'parse error')
{
list($line, $column) = $this->getSourcePosition($this->count);
? "line: $line, column: $column"
: "$this->sourceName on line $line, at column $column";
- if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
+ if ($this->peek('(.*?)(\n|$)', $m, $this->count)) {
$this->restoreEncoding();
- throw new ParserException("$msg: failed at `$m[1]` $loc");
+ $e = new ParserException("$msg: failed at `$m[1]` $loc");
+ $e->setSourcePosition([$this->sourceName, $line, $column]);
+
+ return $e;
}
$this->restoreEncoding();
- throw new ParserException("$msg: $loc");
+ $e = new ParserException("$msg: $loc");
+ $e->setSourcePosition([$this->sourceName, $line, $column]);
+
+ return $e;
}
/**
public function parse($buffer)
{
if ($this->cache) {
- $cacheKey = $this->sourceName . ":" . md5($buffer);
+ $cacheKey = $this->sourceName . ':' . md5($buffer);
$parseOptions = [
'charset' => $this->charset,
'utf8' => $this->utf8,
];
- $v = $this->cache->getCache("parse", $cacheKey, $parseOptions);
+ $v = $this->cache->getCache('parse', $cacheKey, $parseOptions);
if (! \is_null($v)) {
return $v;
}
if ($this->count !== \strlen($this->buffer)) {
- $this->throwParseError();
+ throw $this->parseError();
}
if (! empty($this->env->parent)) {
- $this->throwParseError('unclosed block');
+ throw $this->parseError('unclosed block');
}
if ($this->charset) {
$this->restoreEncoding();
if ($this->cache) {
- $this->cache->setCache("parse", $cacheKey, $this->env, $parseOptions);
+ $this->cache->setCache('parse', $cacheKey, $this->env, $parseOptions);
}
return $this->env;
$this->buffer = (string) $buffer;
$this->saveEncoding();
+ $this->extractLineNumbers($this->buffer);
$list = $this->valueList($out);
*
* @param string $buffer
* @param string|array $out
+ * @param bool $shouldValidate
*
* @return boolean
*/
- public function parseSelector($buffer, &$out)
+ public function parseSelector($buffer, &$out, $shouldValidate = true)
{
$this->count = 0;
$this->env = null;
$this->buffer = (string) $buffer;
$this->saveEncoding();
+ $this->extractLineNumbers($this->buffer);
$selector = $this->selectors($out);
$this->restoreEncoding();
+ if ($shouldValidate && $this->count !== strlen($buffer)) {
+ throw $this->parseError("`" . substr($buffer, $this->count) . "` is not a valid Selector in `$buffer`");
+ }
+
return $selector;
}
$this->buffer = (string) $buffer;
$this->saveEncoding();
+ $this->extractLineNumbers($this->buffer);
$isMediaQuery = $this->mediaQueryList($out);
// the directives
if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
- if ($this->literal('@at-root', 8) &&
+ if (
+ $this->literal('@at-root', 8) &&
($this->selectors($selector) || true) &&
($this->map($with) || true) &&
- (($this->matchChar('(')
- && $this->interpolation($with)
- && $this->matchChar(')')) || true) &&
+ (($this->matchChar('(') &&
+ $this->interpolation($with) &&
+ $this->matchChar(')')) || true) &&
$this->matchChar('{', false)
) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
$atRoot->selector = $selector;
$this->seek($s);
- if ($this->literal('@media', 6) && $this->mediaQueryList($mediaQueryList) && $this->matchChar('{', false)) {
+ if (
+ $this->literal('@media', 6) &&
+ $this->mediaQueryList($mediaQueryList) &&
+ $this->matchChar('{', false)
+ ) {
$media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
$media->queryList = $mediaQueryList[2];
$this->seek($s);
- if ($this->literal('@mixin', 6) &&
+ if (
+ $this->literal('@mixin', 6) &&
$this->keyword($mixinName) &&
($this->argumentDef($args) || true) &&
$this->matchChar('{', false)
) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
$mixin->name = $mixinName;
$this->seek($s);
- if ($this->literal('@include', 8) &&
- $this->keyword($mixinName) &&
- ($this->matchChar('(') &&
+ if (
+ ($this->literal('@include', 8) &&
+ $this->keyword($mixinName) &&
+ ($this->matchChar('(') &&
($this->argValues($argValues) || true) &&
$this->matchChar(')') || true) &&
- ($this->end() ||
- ($this->literal('using', 5) &&
- $this->argumentDef($argUsing) &&
- ($this->end() || $this->matchChar('{') && $hasBlock = true)) ||
- $this->matchChar('{') && $hasBlock = true)
+ ($this->end()) ||
+ ($this->literal('using', 5) &&
+ $this->argumentDef($argUsing) &&
+ ($this->end() || $this->matchChar('{') && $hasBlock = true)) ||
+ $this->matchChar('{') && $hasBlock = true)
) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$child = [
Type::T_INCLUDE,
$this->seek($s);
- if ($this->literal('@scssphp-import-once', 20) &&
+ if (
+ $this->literal('@scssphp-import-once', 20) &&
$this->valueList($importPath) &&
$this->end()
) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s);
$this->seek($s);
- if ($this->literal('@import', 7) &&
+ if (
+ $this->literal('@import', 7) &&
$this->valueList($importPath) &&
+ $importPath[0] !== Type::T_FUNCTION_CALL &&
$this->end()
) {
+ if ($this->cssOnly) {
+ $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s);
+ $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]);
+ return true;
+ }
+
$this->append([Type::T_IMPORT, $importPath], $s);
return true;
$this->seek($s);
- if ($this->literal('@import', 7) &&
+ if (
+ $this->literal('@import', 7) &&
$this->url($importPath) &&
$this->end()
) {
if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
+ $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s);
+ $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]);
+ return true;
}
$this->append([Type::T_IMPORT, $importPath], $s);
$this->seek($s);
- if ($this->literal('@extend', 7) &&
+ if (
+ $this->literal('@extend', 7) &&
$this->selectors($selectors) &&
$this->end()
) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
// check for '!flag'
$optional = $this->stripOptionalFlag($selectors);
$this->seek($s);
- if ($this->literal('@function', 9) &&
+ if (
+ $this->literal('@function', 9) &&
$this->keyword($fnName) &&
$this->argumentDef($args) &&
$this->matchChar('{', false)
) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
$func->name = $fnName;
$this->seek($s);
- if ($this->literal('@break', 6) && $this->end()) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
-
- $this->append([Type::T_BREAK], $s);
-
- return true;
- }
-
- $this->seek($s);
-
- if ($this->literal('@continue', 9) && $this->end()) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
-
- $this->append([Type::T_CONTINUE], $s);
-
- return true;
- }
-
- $this->seek($s);
-
- if ($this->literal('@return', 7) && ($this->valueList($retVal) || true) && $this->end()) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
+ if (
+ $this->literal('@return', 7) &&
+ ($this->valueList($retVal) || true) &&
+ $this->end()
+ ) {
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s);
$this->seek($s);
- if ($this->literal('@each', 5) &&
+ if (
+ $this->literal('@each', 5) &&
$this->genericList($varNames, 'variable', ',', false) &&
$this->literal('in', 2) &&
$this->valueList($list) &&
$this->matchChar('{', false)
) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$each = $this->pushSpecialBlock(Type::T_EACH, $s);
$this->seek($s);
- if ($this->literal('@while', 6) &&
+ if (
+ $this->literal('@while', 6) &&
$this->expression($cond) &&
$this->matchChar('{', false)
) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
+
+ while (
+ $cond[0] === Type::T_LIST &&
+ ! empty($cond['enclosing']) &&
+ $cond['enclosing'] === 'parent' &&
+ \count($cond[2]) == 1
+ ) {
+ $cond = reset($cond[2]);
}
$while = $this->pushSpecialBlock(Type::T_WHILE, $s);
$this->seek($s);
- if ($this->literal('@for', 4) &&
+ if (
+ $this->literal('@for', 4) &&
$this->variable($varName) &&
$this->literal('from', 4) &&
$this->expression($start) &&
$this->expression($end) &&
$this->matchChar('{', false)
) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$for = $this->pushSpecialBlock(Type::T_FOR, $s);
$for->var = $varName[1];
$this->seek($s);
- if ($this->literal('@if', 3) && $this->valueList($cond) && $this->matchChar('{', false)) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
+ if (
+ $this->literal('@if', 3) &&
+ $this->functionCallArgumentsList($cond, false, '{', false)
+ ) {
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$if = $this->pushSpecialBlock(Type::T_IF, $s);
- while ($cond[0] === Type::T_LIST
- && ! empty($cond['enclosing'])
- && $cond['enclosing'] === 'parent'
- && \count($cond[2]) == 1) {
+ while (
+ $cond[0] === Type::T_LIST &&
+ ! empty($cond['enclosing']) &&
+ $cond['enclosing'] === 'parent' &&
+ \count($cond[2]) == 1
+ ) {
$cond = reset($cond[2]);
}
$this->seek($s);
- if ($this->literal('@debug', 6) &&
- $this->valueList($value) &&
- $this->end()
+ if (
+ $this->literal('@debug', 6) &&
+ $this->functionCallArgumentsList($value, false)
) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$this->append([Type::T_DEBUG, $value], $s);
$this->seek($s);
- if ($this->literal('@warn', 5) &&
- $this->valueList($value) &&
- $this->end()
+ if (
+ $this->literal('@warn', 5) &&
+ $this->functionCallArgumentsList($value, false)
) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$this->append([Type::T_WARN, $value], $s);
$this->seek($s);
- if ($this->literal('@error', 6) &&
- $this->valueList($value) &&
- $this->end()
+ if (
+ $this->literal('@error', 6) &&
+ $this->functionCallArgumentsList($value, false)
) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$this->append([Type::T_ERROR, $value], $s);
$this->seek($s);
- if ($this->literal('@content', 8) &&
+ if (
+ $this->literal('@content', 8) &&
($this->end() ||
$this->matchChar('(') &&
$this->argValues($argContent) &&
$this->matchChar(')') &&
$this->end())
) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$this->append([Type::T_MIXIN_CONTENT, isset($argContent) ? $argContent : null], $s);
if ($this->literal('@else', 5)) {
if ($this->matchChar('{', false)) {
$else = $this->pushSpecialBlock(Type::T_ELSE, $s);
- } elseif ($this->literal('if', 2) && $this->valueList($cond) && $this->matchChar('{', false)) {
+ } elseif (
+ $this->literal('if', 2) &&
+ $this->functionCallArgumentsList($cond, false, '{', false)
+ ) {
$else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
$else->cond = $cond;
}
}
// only retain the first @charset directive encountered
- if ($this->literal('@charset', 8) &&
+ if (
+ $this->literal('@charset', 8) &&
$this->valueList($charset) &&
$this->end()
) {
$this->seek($s);
- if ($this->literal('@supports', 9) &&
- ($t1=$this->supportsQuery($supportQuery)) &&
- ($t2=$this->matchChar('{', false))
+ if (
+ $this->literal('@supports', 9) &&
+ ($t1 = $this->supportsQuery($supportQuery)) &&
+ ($t2 = $this->matchChar('{', false))
) {
$directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
$directive->name = 'supports';
$this->seek($s);
// doesn't match built in directive, do generic one
- if ($this->matchChar('@', false) &&
- $this->keyword($dirName) &&
+ if (
+ $this->matchChar('@', false) &&
+ $this->mixedKeyword($dirName) &&
$this->directiveValue($dirValue, '{')
) {
+ if (count($dirName) === 1 && is_string(reset($dirName))) {
+ $dirName = reset($dirName);
+ } else {
+ $dirName = [Type::T_STRING, '', $dirName];
+ }
if ($dirName === 'media') {
$directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
} else {
}
if (isset($dirValue)) {
+ ! $this->cssOnly || ($dirValue = $this->assertPlainCssValid($dirValue));
$directive->value = $dirValue;
}
$this->seek($s);
// maybe it's a generic blockless directive
- if ($this->matchChar('@', false) &&
- $this->keyword($dirName) &&
- $this->directiveValue($dirValue) &&
- $this->end()
+ if (
+ $this->matchChar('@', false) &&
+ $this->mixedKeyword($dirName) &&
+ ! $this->isKnownGenericDirective($dirName) &&
+ ($this->end(false) || ($this->directiveValue($dirValue, '') && $this->end(false)))
) {
- $this->append([Type::T_DIRECTIVE, [$dirName, $dirValue]], $s);
+ if (\count($dirName) === 1 && \is_string(\reset($dirName))) {
+ $dirName = \reset($dirName);
+ } else {
+ $dirName = [Type::T_STRING, '', $dirName];
+ }
+ if (
+ ! empty($this->env->parent) &&
+ $this->env->type &&
+ ! \in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA])
+ ) {
+ $plain = \trim(\substr($this->buffer, $s, $this->count - $s));
+ throw $this->parseError(
+ "Unknown directive `{$plain}` not allowed in `" . $this->env->type . "` block"
+ );
+ }
+ // blockless directives with a blank line after keeps their blank lines after
+ // sass-spec compliance purpose
+ $s = $this->count;
+ $hasBlankLine = false;
+ if ($this->match('\s*?\n\s*\n', $out, false)) {
+ $hasBlankLine = true;
+ $this->seek($s);
+ }
+ $isNotRoot = ! empty($this->env->parent);
+ $this->append([Type::T_DIRECTIVE, [$dirName, $dirValue, $hasBlankLine, $isNotRoot]], $s);
+ $this->whitespace();
return true;
}
return false;
}
+ $inCssSelector = null;
+ if ($this->cssOnly) {
+ $inCssSelector = (! empty($this->env->parent) &&
+ ! in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA]));
+ }
// custom properties : right part is static
- if (($this->customProperty($name) || ($this->cssOnly && $this->propertyName($name))) &&
- $this->matchChar(':', false)
- ) {
+ if (($this->customProperty($name) ) && $this->matchChar(':', false)) {
$start = $this->count;
// but can be complex and finish with ; or }
foreach ([';','}'] as $ending) {
- if ($this->openString($ending, $stringValue, '(', ')', false) &&
+ if (
+ $this->openString($ending, $stringValue, '(', ')', false) &&
$this->end()
) {
$end = $this->count;
if ($p && $p < $end) {
$this->seek($start);
- if ($this->openString($ending, $stringValue, $nestingPair[0], $nestingPair[1], false) &&
+ if (
+ $this->openString($ending, $stringValue, $nestingPair[0], $nestingPair[1], false) &&
$this->end() &&
$this->count > $end
) {
// property shortcut
// captures most properties before having to parse a selector
- if ($this->keyword($name, false) &&
+ if (
+ $this->keyword($name, false) &&
$this->literal(': ', 2) &&
$this->valueList($value) &&
$this->end()
$this->seek($s);
// variable assigns
- if ($this->variable($name) &&
+ if (
+ $this->variable($name) &&
$this->matchChar(':') &&
$this->valueList($value) &&
$this->end()
) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
// check for '!flag'
$assignmentFlags = $this->stripAssignmentFlags($value);
}
// opening css block
- if ($this->selectors($selectors) && $this->matchChar('{', false)) {
- if ($this->cssOnly) {
- if (! empty($this->env->parent)) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
- }
+ if (
+ $this->selectors($selectors) &&
+ $this->matchChar('{', false)
+ ) {
+ ! $this->cssOnly || ! $inCssSelector || $this->assertPlainCssValid(false);
$this->pushBlock($selectors, $s);
$this->seek($s);
// property assign, or nested assign
- if ($this->propertyName($name) && $this->matchChar(':')) {
+ if (
+ $this->propertyName($name) &&
+ $this->matchChar(':')
+ ) {
$foundSomething = false;
if ($this->valueList($value)) {
if (empty($this->env->parent)) {
- $this->throwParseError('expected "{"');
+ throw $this->parseError('expected "{"');
}
$this->append([Type::T_ASSIGN, $name, $value], $s);
}
if ($this->matchChar('{', false)) {
- if ($this->cssOnly) {
- $this->throwParseError("SCSS syntax not allowed in CSS file");
- }
+ ! $this->cssOnly || $this->assertPlainCssValid(false);
$propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
$propBlock->prefix = $name;
}
// extra stuff
- if ($this->matchChar(';') ||
+ if (
+ $this->matchChar(';') ||
$this->literal('<!--', 4)
) {
return true;
{
list($line, $column) = $this->getSourcePosition($pos);
- $b = new Block;
+ $b = new Block();
$b->sourceName = $this->sourceName;
$b->sourceLine = $line;
$b->sourceColumn = $column;
$block = $this->env;
if (empty($block->parent)) {
- $this->throwParseError('unexpected }');
+ throw $this->parseError('unexpected }');
}
if ($block->type == Type::T_AT_ROOT) {
$this->count = $where;
}
+ /**
+ * Assert a parsed part is plain CSS Valid
+ *
+ * @param array $parsed
+ * @param int $startPos
+ * @throws ParserException
+ */
+ protected function assertPlainCssValid($parsed, $startPos = null)
+ {
+ $type = '';
+ if ($parsed) {
+ $type = $parsed[0];
+ $parsed = $this->isPlainCssValidElement($parsed);
+ }
+ if (! $parsed) {
+ if (! \is_null($startPos)) {
+ $plain = rtrim(substr($this->buffer, $startPos, $this->count - $startPos));
+ $message = "Error : `{$plain}` isn't allowed in plain CSS";
+ } else {
+ $message = 'Error: SCSS syntax not allowed in CSS file';
+ }
+ if ($type) {
+ $message .= " ($type)";
+ }
+ throw $this->parseError($message);
+ }
+
+ return $parsed;
+ }
+
+ /**
+ * Check a parsed element is plain CSS Valid
+ * @param array $parsed
+ * @return bool|array
+ */
+ protected function isPlainCssValidElement($parsed, $allowExpression = false)
+ {
+ // keep string as is
+ if (is_string($parsed)) {
+ return $parsed;
+ }
+
+ if (
+ \in_array($parsed[0], [Type::T_FUNCTION, Type::T_FUNCTION_CALL]) &&
+ !\in_array($parsed[1], [
+ 'alpha',
+ 'attr',
+ 'calc',
+ 'cubic-bezier',
+ 'env',
+ 'grayscale',
+ 'hsl',
+ 'hsla',
+ 'invert',
+ 'linear-gradient',
+ 'min',
+ 'max',
+ 'radial-gradient',
+ 'repeating-linear-gradient',
+ 'repeating-radial-gradient',
+ 'rgb',
+ 'rgba',
+ 'rotate',
+ 'saturate',
+ 'var',
+ ]) &&
+ Compiler::isNativeFunction($parsed[1])
+ ) {
+ return false;
+ }
+
+ switch ($parsed[0]) {
+ case Type::T_BLOCK:
+ case Type::T_KEYWORD:
+ case Type::T_NULL:
+ case Type::T_NUMBER:
+ case Type::T_MEDIA:
+ return $parsed;
+
+ case Type::T_COMMENT:
+ if (isset($parsed[2])) {
+ return false;
+ }
+ return $parsed;
+
+ case Type::T_DIRECTIVE:
+ if (\is_array($parsed[1])) {
+ $parsed[1][1] = $this->isPlainCssValidElement($parsed[1][1]);
+ if (! $parsed[1][1]) {
+ return false;
+ }
+ }
+
+ return $parsed;
+
+ case Type::T_IMPORT:
+ if ($parsed[1][0] === Type::T_LIST) {
+ return false;
+ }
+ $parsed[1] = $this->isPlainCssValidElement($parsed[1]);
+ if ($parsed[1] === false) {
+ return false;
+ }
+ return $parsed;
+
+ case Type::T_STRING:
+ foreach ($parsed[2] as $k => $substr) {
+ if (\is_array($substr)) {
+ $parsed[2][$k] = $this->isPlainCssValidElement($substr);
+ if (! $parsed[2][$k]) {
+ return false;
+ }
+ }
+ }
+ return $parsed;
+
+ case Type::T_LIST:
+ if (!empty($parsed['enclosing'])) {
+ return false;
+ }
+ foreach ($parsed[2] as $k => $listElement) {
+ $parsed[2][$k] = $this->isPlainCssValidElement($listElement);
+ if (! $parsed[2][$k]) {
+ return false;
+ }
+ }
+ return $parsed;
+
+ case Type::T_ASSIGN:
+ foreach ([1, 2, 3] as $k) {
+ if (! empty($parsed[$k])) {
+ $parsed[$k] = $this->isPlainCssValidElement($parsed[$k]);
+ if (! $parsed[$k]) {
+ return false;
+ }
+ }
+ }
+ return $parsed;
+
+ case Type::T_EXPRESSION:
+ list( ,$op, $lhs, $rhs, $inParens, $whiteBefore, $whiteAfter) = $parsed;
+ if (! $allowExpression && ! \in_array($op, ['and', 'or', '/'])) {
+ return false;
+ }
+ $lhs = $this->isPlainCssValidElement($lhs, true);
+ if (! $lhs) {
+ return false;
+ }
+ $rhs = $this->isPlainCssValidElement($rhs, true);
+ if (! $rhs) {
+ return false;
+ }
+
+ return [
+ Type::T_STRING,
+ '', [
+ $this->inParens ? '(' : '',
+ $lhs,
+ ($whiteBefore ? ' ' : '') . $op . ($whiteAfter ? ' ' : ''),
+ $rhs,
+ $this->inParens ? ')' : ''
+ ]
+ ];
+
+ case Type::T_UNARY:
+ $parsed[2] = $this->isPlainCssValidElement($parsed[2]);
+ if (! $parsed[2]) {
+ return false;
+ }
+ return $parsed;
+
+ case Type::T_FUNCTION:
+ $argsList = $parsed[2];
+ foreach ($argsList[2] as $argElement) {
+ if (! $this->isPlainCssValidElement($argElement)) {
+ return false;
+ }
+ }
+ return $parsed;
+
+ case Type::T_FUNCTION_CALL:
+ $parsed[0] = Type::T_FUNCTION;
+ $argsList = [Type::T_LIST, ',', []];
+ foreach ($parsed[2] as $arg) {
+ if ($arg[0] || ! empty($arg[2])) {
+ // no named arguments possible in a css function call
+ // nor ... argument
+ return false;
+ }
+ $arg = $this->isPlainCssValidElement($arg[1], $parsed[1] === 'calc');
+ if (! $arg) {
+ return false;
+ }
+ $argsList[2][] = $arg;
+ }
+ $parsed[2] = $argsList;
+ return $parsed;
+ }
+
+ return false;
+ }
+
/**
* 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
+ * @param string $delim Delimiter
*
* @return boolean True if match; false otherwise
*/
$end = \strlen($this->buffer);
// look for either ending delim, escape, or string interpolation
- foreach (['#{', '\\', $delim] as $lookahead) {
+ foreach (['#{', '\\', "\r", $delim] as $lookahead) {
$pos = strpos($this->buffer, $lookahead, $this->count);
if ($pos !== false && $pos < $end) {
if ($this->interpolation($out)) {
// keep right spaces in the following string part
if ($out[3]) {
- while ($this->buffer[$this->count-1] !== '}') {
+ while ($this->buffer[$this->count - 1] !== '}') {
$this->count--;
}
} else {
// comment that are ignored and not kept in the output css
$this->count += \strlen($m[0]);
+ // silent comments are not allowed in plain CSS files
+ ! $this->cssOnly
+ || ! \strlen(trim($m[0]))
+ || $this->assertPlainCssValid(false, $this->count - \strlen($m[0]));
}
$gotWhite = true;
protected function appendComment($comment)
{
if (! $this->discardComments) {
- if ($comment[0] === Type::T_COMMENT) {
- if (\is_string($comment[1])) {
- $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
- }
-
- if (isset($comment[2]) and \is_array($comment[2]) and $comment[2][0] === Type::T_STRING) {
- foreach ($comment[2][2] as $k => $v) {
- if (\is_string($v)) {
- $p = strpos($v, "\n");
-
- if ($p !== false) {
- $comment[2][2][$k] = substr($v, 0, $p + 1)
- . preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], substr($v, $p+1));
- }
- }
- }
- }
- }
-
$this->env->comments[] = $comment;
}
}
protected function append($statement, $pos = null)
{
if (! \is_null($statement)) {
+ ! $this->cssOnly || ($statement = $this->assertPlainCssValid($statement, $pos));
+
if (! \is_null($pos)) {
list($line, $column) = $this->getSourcePosition($pos);
$expressions = null;
$parts = [];
- if (($this->literal('only', 4) && ($only = true) || $this->literal('not', 3) && ($not = true) || true) &&
+ if (
+ ($this->literal('only', 4) && ($only = true) ||
+ $this->literal('not', 3) && ($not = true) || true) &&
$this->mixedKeyword($mediaType)
) {
$prop = [Type::T_MEDIA_TYPE];
$not = false;
- if (($this->literal('not', 3) && ($not = true) || true) &&
+ if (
+ ($this->literal('not', 3) && ($not = true) || true) &&
$this->matchChar('(') &&
($this->expression($property)) &&
$this->literal(': ', 2) &&
$this->seek($s);
}
- if ($this->matchChar('(') &&
+ if (
+ $this->matchChar('(') &&
$this->supportsQuery($subQuery) &&
$this->matchChar(')')
) {
$this->seek($s);
}
- if ($this->literal('not', 3) &&
+ if (
+ $this->literal('not', 3) &&
$this->supportsQuery($subQuery)
) {
$parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, 'not '], $subQuery]];
$this->seek($s);
}
- if ($this->literal('selector(', 9) &&
+ if (
+ $this->literal('selector(', 9) &&
$this->selector($selector) &&
$this->matchChar(')')
) {
$this->seek($s);
}
- if ($this->literal('and', 3) &&
- $this->genericList($expressions, 'supportsQuery', ' and', false)) {
+ if (
+ $this->literal('and', 3) &&
+ $this->genericList($expressions, 'supportsQuery', ' and', false)
+ ) {
array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
$parts = [$expressions];
$this->seek($s);
}
- if ($this->literal('or', 2) &&
- $this->genericList($expressions, 'supportsQuery', ' or', false)) {
+ if (
+ $this->literal('or', 2) &&
+ $this->genericList($expressions, 'supportsQuery', ' or', false)
+ ) {
array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
$parts = [$expressions];
$s = $this->count;
$value = null;
- if ($this->matchChar('(') &&
+ if (
+ $this->matchChar('(') &&
$this->expression($feature) &&
- ($this->matchChar(':') && $this->expression($value) || true) &&
+ ($this->matchChar(':') &&
+ $this->expression($value) || true) &&
$this->matchChar(')')
) {
$out = [Type::T_MEDIA_EXPRESSION, $feature];
*/
protected function argValues(&$out)
{
+ $discardComments = $this->discardComments;
+ $this->discardComments = true;
+
if ($this->genericList($list, 'argValue', ',', false)) {
$out = $list[2];
+ $this->discardComments = $discardComments;
+
return true;
}
+ $this->discardComments = $discardComments;
+
return false;
}
$keyword = null;
}
- if ($this->genericList($value, 'expression')) {
+ if ($this->genericList($value, 'expression', '', true)) {
$out = [$keyword, $value, false];
$s = $this->count;
return false;
}
+ /**
+ * Check if a generic directive is known to be able to allow almost any syntax or not
+ * @param $directiveName
+ * @return bool
+ */
+ protected function isKnownGenericDirective($directiveName)
+ {
+ if (\is_array($directiveName) && \is_string(reset($directiveName))) {
+ $directiveName = reset($directiveName);
+ }
+ if (! \is_string($directiveName)) {
+ return false;
+ }
+ if (
+ \in_array($directiveName, [
+ 'at-root',
+ 'media',
+ 'mixin',
+ 'include',
+ 'scssphp-import-once',
+ 'import',
+ 'extend',
+ 'function',
+ 'break',
+ 'continue',
+ 'return',
+ 'each',
+ 'while',
+ 'for',
+ 'if',
+ 'debug',
+ 'warn',
+ 'error',
+ 'content',
+ 'else',
+ 'charset',
+ 'supports',
+ // Todo
+ 'use',
+ 'forward',
+ ])
+ ) {
+ return true;
+ }
+ return false;
+ }
+
/**
* Parse directive value list that considers $vars as keyword
*
$this->seek($s);
- if ($endChar and $this->openString($endChar, $out)) {
- if ($this->matchChar($endChar, false)) {
+ if (\is_string($endChar) && $this->openString($endChar ? $endChar : ';', $out, null, null, true, ";}{")) {
+ if ($endChar && $this->matchChar($endChar, false)) {
+ return true;
+ }
+ $ss = $this->count;
+ if (!$endChar && $this->end()) {
+ $this->seek($ss);
return true;
}
}
return $res;
}
+ /**
+ * Parse a function call, where externals () are part of the call
+ * and not of the value list
+ *
+ * @param $out
+ * @param bool $mandatoryEnclos
+ * @param null|string $charAfter
+ * @param null|bool $eatWhiteSp
+ * @return bool
+ */
+ protected function functionCallArgumentsList(&$out, $mandatoryEnclos = true, $charAfter = null, $eatWhiteSp = null)
+ {
+ $s = $this->count;
+
+ if (
+ $this->matchChar('(') &&
+ $this->valueList($out) &&
+ $this->matchChar(')') &&
+ ($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end())
+ ) {
+ return true;
+ }
+
+ if (! $mandatoryEnclos) {
+ $this->seek($s);
+
+ if (
+ $this->valueList($out) &&
+ ($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end())
+ ) {
+ return true;
+ }
+ }
+
+ $this->seek($s);
+
+ return false;
+ }
+
/**
* Parse space separated value list
*
}
$trailing_delim = true;
+ } else {
+ // if no delim watch that a keyword didn't eat the single/double quote
+ // from the following starting string
+ if ($value[0] === Type::T_KEYWORD) {
+ $word = $value[1];
+
+ $last_char = substr($word, -1);
+
+ if (
+ strlen($word) > 1 &&
+ in_array($last_char, [ "'", '"']) &&
+ substr($word, -2, 1) !== '\\'
+ ) {
+ // if there is a non escaped opening quote in the keyword, this seems unlikely a mistake
+ $word = str_replace('\\' . $last_char, '\\\\', $word);
+ if (strpos($word, $last_char) < strlen($word) - 1) {
+ continue;
+ }
+
+ $currentCount = $this->count;
+
+ // let's try to rewind to previous char and try a parse
+ $this->count--;
+ // in case the keyword also eat spaces
+ while (substr($this->buffer, $this->count, 1) !== $last_char) {
+ $this->count--;
+ }
+
+ $nextValue = null;
+ if ($this->$parseItem($nextValue)) {
+ if ($nextValue[0] === Type::T_KEYWORD && $nextValue[1] === $last_char) {
+ // bad try, forget it
+ $this->seek($currentCount);
+ continue;
+ }
+ if ($nextValue[0] !== Type::T_STRING) {
+ // bad try, forget it
+ $this->seek($currentCount);
+ continue;
+ }
+
+ // OK it was a good idea
+ $value[1] = substr($value[1], 0, -1);
+ array_pop($items);
+ $items[] = $value;
+ $items[] = $nextValue;
+ } else {
+ // bad try, forget it
+ $this->seek($currentCount);
+ continue;
+ }
+ }
+ }
}
}
$allowedTypes = ($listOnly ? [Type::T_LIST] : [Type::T_LIST, Type::T_MAP]);
if ($this->matchChar('(')) {
- if ($this->enclosedExpression($lhs, $s, ")", $allowedTypes)) {
+ if ($this->enclosedExpression($lhs, $s, ')', $allowedTypes)) {
if ($lookForExp) {
$out = $this->expHelper($lhs, 0);
} else {
}
if (\in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) {
- if ($this->enclosedExpression($lhs, $s, "]", [Type::T_LIST])) {
+ if ($this->enclosedExpression($lhs, $s, ']', [Type::T_LIST])) {
if ($lookForExp) {
$out = $this->expHelper($lhs, 0);
} else {
*
* @return boolean
*/
- protected function enclosedExpression(&$out, $s, $closingParen = ")", $allowedTypes = [Type::T_LIST, Type::T_MAP])
+ protected function enclosedExpression(&$out, $s, $closingParen = ')', $allowedTypes = [Type::T_LIST, Type::T_MAP])
{
if ($this->matchChar($closingParen) && \in_array(Type::T_LIST, $allowedTypes)) {
$out = [Type::T_LIST, '', []];
switch ($closingParen) {
- case ")":
+ case ')':
$out['enclosing'] = 'parent'; // parenthesis list
break;
- case "]":
+ case ']':
$out['enclosing'] = 'bracket'; // bracketed list
break;
}
return true;
}
- if ($this->valueList($out) && $this->matchChar($closingParen) &&
- \in_array($out[0], [Type::T_LIST, Type::T_KEYWORD]) &&
+ if (
+ $this->valueList($out) &&
+ $this->matchChar($closingParen) && ! ($closingParen === ')' &&
+ \in_array($out[0], [Type::T_EXPRESSION, Type::T_UNARY])) &&
\in_array(Type::T_LIST, $allowedTypes)
) {
if ($out[0] !== Type::T_LIST || ! empty($out['enclosing'])) {
}
switch ($closingParen) {
- case ")":
+ case ')':
$out['enclosing'] = 'parent'; // parenthesis list
break;
- case "]":
+ case ']':
$out['enclosing'] = 'bracket'; // bracketed list
break;
}
break;
}
+ if ($op === '-' && ! $whiteAfter && $rhs[0] === Type::T_KEYWORD) {
+ break;
+ }
+
// peek and see if rhs belongs to next operator
if ($this->peek($operators, $next) && static::$precedence[$next[1]] > static::$precedence[$op]) {
$rhs = $this->expHelper($rhs, static::$precedence[$next[1]]);
}
$lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter];
+
$ss = $this->count;
$whiteBefore = isset($this->buffer[$this->count - 1]) &&
ctype_space($this->buffer[$this->count - 1]);
$s = $this->count;
$char = $this->buffer[$this->count];
- if ($this->literal('url(', 4) && $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)) {
+ if (
+ $this->literal('url(', 4) &&
+ $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)
+ ) {
$len = strspn(
$this->buffer,
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=',
$this->seek($s);
- if ($this->literal('url(', 4, false) && $this->match('\s*(\/\/[^\s\)]+)\s*', $m)) {
+ if (
+ $this->literal('url(', 4, false) &&
+ $this->match('\s*(\/\/[^\s\)]+)\s*', $m)
+ ) {
$content = 'url(' . $m[1];
if ($this->matchChar(')')) {
// not
if ($char === 'n' && $this->literal('not', 3, false)) {
- if ($this->whitespace() && $this->value($inner)) {
+ if (
+ $this->whitespace() &&
+ $this->value($inner)
+ ) {
$out = [Type::T_UNARY, 'not', $inner, $this->inParens];
return true;
return true;
}
- if ($this->keyword($inner) && ! $this->func($inner, $out)) {
+ if (
+ $this->keyword($inner) &&
+ ! $this->func($inner, $out)
+ ) {
$out = [Type::T_UNARY, '-', $inner, $this->inParens];
return true;
$this->count++;
if ($this->keyword($keyword)) {
- $out = [Type::T_KEYWORD, "#" . $keyword];
+ $out = [Type::T_KEYWORD, '#' . $keyword];
return true;
}
}
// unicode range with wildcards
- if ($this->literal('U+', 2) && $this->match('([0-9A-F]+\?*)(-([0-9A-F]+))?', $m, false)) {
- $out = [Type::T_KEYWORD, 'U+' . $m[0]];
+ if (
+ $this->literal('U+', 2) &&
+ $this->match('\?+|([0-9A-F]+(\?+|(-[0-9A-F]+))?)', $m, false)
+ ) {
+ $unicode = explode('-', $m[0]);
+ if (strlen(reset($unicode)) <= 6 && strlen(end($unicode)) <= 6) {
+ $out = [Type::T_KEYWORD, 'U+' . $m[0]];
- return true;
+ return true;
+ }
+ $this->count -= strlen($m[0]) + 2;
}
if ($this->keyword($keyword, false)) {
$this->inParens = true;
- if ($this->expression($exp) && $this->matchChar(')')) {
+ if (
+ $this->expression($exp) &&
+ $this->matchChar(')')
+ ) {
$out = $exp;
$this->inParens = $inParens;
{
$s = $this->count;
- if ($this->literal('progid:', 7, false) &&
+ if (
+ $this->literal('progid:', 7, false) &&
$this->openString('(', $fn) &&
$this->matchChar('(')
) {
if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
$ss = $this->count;
- if ($this->argValues($args) && $this->matchChar(')')) {
+ if (
+ $this->argValues($args) &&
+ $this->matchChar(')')
+ ) {
$func = [Type::T_FUNCTION_CALL, $name, $args];
return true;
$this->seek($ss);
}
- if (($this->openString(')', $str, '(') || true) &&
+ if (
+ ($this->openString(')', $str, '(') || true) &&
$this->matchChar(')')
) {
$args = [];
$args = [];
while ($this->keyword($var)) {
- if ($this->matchChar('=') && $this->expression($exp)) {
+ if (
+ $this->matchChar('=') &&
+ $this->expression($exp)
+ ) {
$args[] = [Type::T_STRING, '', [$var . '=']];
$arg = $exp;
} else {
$ss = $this->count;
- if ($this->matchChar(':') && $this->genericList($defaultVal, 'expression')) {
+ if (
+ $this->matchChar(':') &&
+ $this->genericList($defaultVal, 'expression', '', true)
+ ) {
$arg[1] = $defaultVal;
} else {
$this->seek($ss);
$sss = $this->count;
if (! $this->matchChar(')')) {
- $this->throwParseError('... has to be after the final argument');
+ throw $this->parseError('... has to be after the final argument');
}
$arg[2] = true;
$keys = [];
$values = [];
- while ($this->genericList($key, 'expression') && $this->matchChar(':') &&
- $this->genericList($value, 'expression')
+ while (
+ $this->genericList($key, 'expression', '', true) &&
+ $this->matchChar(':') &&
+ $this->genericList($value, 'expression', '', true)
) {
$keys[] = $key;
$values[] = $value;
{
$s = $this->count;
- if ($this->match('(#([0-9a-f]+))', $m)) {
+ if ($this->match('(#([0-9a-f]+)\b)', $m)) {
if (\in_array(\strlen($m[2]), [3,4,6,8])) {
$out = [Type::T_KEYWORD, $m[0]];
*
* @return boolean
*/
- protected function string(&$out)
+ protected function string(&$out, $keepDelimWithInterpolation = false)
{
$s = $this->count;
$this->count += \strlen($m[2]);
$content[] = '#{'; // ignore it
}
+ } elseif ($m[2] === "\r") {
+ $content[] = chr(10);
+ // TODO : warning
+ # DEPRECATION WARNING on line x, column y of zzz:
+ # Unescaped multiline strings are deprecated and will be removed in a future version of Sass.
+ # To include a newline in a string, use "\a" or "\a " as in CSS.
+ if ($this->matchChar("\n", false)) {
+ $content[] = ' ';
+ }
} elseif ($m[2] === '\\') {
- if ($this->matchChar('"', false)) {
- $content[] = $m[2] . '"';
- } elseif ($this->matchChar("'", false)) {
- $content[] = $m[2] . "'";
- } elseif ($this->literal("\\", 1, false)) {
- $content[] = $m[2] . "\\";
- } elseif ($this->literal("\r\n", 2, false) ||
+ if (
+ $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
+ } elseif ($this->matchEscapeCharacter($c)) {
+ $content[] = $c;
} else {
- $content[] = $m[2];
+ throw $this->parseError('Unterminated escape sequence');
}
} else {
$this->count -= \strlen($delim);
$this->eatWhiteDefault = $oldWhite;
if ($this->literal($delim, \strlen($delim))) {
- if ($hasInterpolation) {
+ if ($hasInterpolation && ! $keepDelimWithInterpolation) {
$delim = '"';
-
- foreach ($content as &$string) {
- if ($string === "\\\\") {
- $string = "\\";
- } elseif ($string === "\\'") {
- $string = "'";
- } elseif ($string === '\\"') {
- $string = '"';
- }
- }
}
$out = [Type::T_STRING, $delim, $content];
return false;
}
+ protected function matchEscapeCharacter(&$out, $inKeywords = false)
+ {
+ $s = $this->count;
+ if ($this->match('[a-f0-9]', $m, false)) {
+ $hex = $m[0];
+
+ for ($i = 5; $i--;) {
+ if ($this->match('[a-f0-9]', $m, false)) {
+ $hex .= $m[0];
+ } else {
+ break;
+ }
+ }
+
+ $value = hexdec($hex);
+
+ if (!$inKeywords && ($value == 0 || ($value >= 0xD800 && $value <= 0xDFFF) || $value >= 0x10FFFF)) {
+ $out = "\xEF\xBF\xBD"; // "\u{FFFD}" but with a syntax supported on PHP 5
+ } elseif ($value < 0x20) {
+ $out = Util::mbChr($value);
+ } else {
+ $out = Util::mbChr($value);
+ }
+
+ return true;
+ }
+
+ if ($this->match('.', $m, false)) {
+ if ($inKeywords && in_array($m[0], ["'",'"','@','&',' ','\\',':','/','%'])) {
+ $this->seek($s);
+ return false;
+ }
+ $out = $m[0];
+
+ return true;
+ }
+
+ return false;
+ }
+
/**
* Parse keyword or interpolation
*
*
* @param string $end
* @param array $out
- * @param string $nestingOpen
- * @param string $nestingClose
- * @param boolean $trimEnd
+ * @param string $nestOpen
+ * @param string $nestClose
+ * @param boolean $rtrim
+ * @param string $disallow
*
* @return boolean
*/
- protected function openString($end, &$out, $nestingOpen = null, $nestingClose = null, $trimEnd = true)
+ protected function openString($end, &$out, $nestOpen = null, $nestClose = null, $rtrim = true, $disallow = null)
{
$oldWhite = $this->eatWhiteDefault;
$this->eatWhiteDefault = false;
- if ($nestingOpen && ! $nestingClose) {
- $nestingClose = $end;
+ if ($nestOpen && ! $nestClose) {
+ $nestClose = $end;
}
- $patt = '(.*?)([\'"]|#\{|'
+ $patt = ($disallow ? '[^' . $this->pregQuote($disallow) . ']' : '.');
+ $patt = '(' . $patt . '*?)([\'"]|#\{|'
. $this->pregQuote($end) . '|'
- . (($nestingClose && $nestingClose !== $end) ? $this->pregQuote($nestingClose) . '|' : '')
+ . (($nestClose && $nestClose !== $end) ? $this->pregQuote($nestClose) . '|' : '')
. static::$commentPattern . ')';
$nestingLevel = 0;
if (isset($m[1]) && $m[1] !== '') {
$content[] = $m[1];
- if ($nestingOpen) {
- $nestingLevel += substr_count($m[1], $nestingOpen);
+ if ($nestOpen) {
+ $nestingLevel += substr_count($m[1], $nestOpen);
}
}
$tok = $m[2];
- $this->count-= \strlen($tok);
+ $this->count -= \strlen($tok);
if ($tok === $end && ! $nestingLevel) {
break;
}
- if ($tok === $nestingClose) {
+ if ($tok === $nestClose) {
$nestingLevel--;
}
- if (($tok === "'" || $tok === '"') && $this->string($str)) {
+ if (($tok === "'" || $tok === '"') && $this->string($str, true)) {
$content[] = $str;
continue;
}
}
$content[] = $tok;
- $this->count+= \strlen($tok);
+ $this->count += \strlen($tok);
}
$this->eatWhiteDefault = $oldWhite;
}
// trim the end
- if ($trimEnd && \is_string(end($content))) {
+ if ($rtrim && \is_string(end($content))) {
$content[\count($content) - 1] = rtrim(end($content));
}
$s = $this->count;
- if ($this->literal('#{', 2) && $this->valueList($value) && $this->matchChar('}', false)) {
+ if (
+ $this->literal('#{', 2) &&
+ $this->valueList($value) &&
+ $this->matchChar('}', false)
+ ) {
if ($value === [Type::T_SELF]) {
$out = $value;
} else {
if ($lookWhite) {
$left = ($s > 0 && preg_match('/\s/', $this->buffer[$s - 1])) ? ' ' : '';
- $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
+ $right = (
+ ! empty($this->buffer[$this->count]) &&
+ preg_match('/\s/', $this->buffer[$this->count])
+ ) ? ' ' : '';
} else {
$left = $right = false;
}
continue;
}
+ if ($this->matchChar('&', false)) {
+ $parts[] = [Type::T_SELF];
+ continue;
+ }
+
if ($this->variable($var)) {
$parts[] = $var;
continue;
{
$selector = [];
+ $discardComments = $this->discardComments;
+ $this->discardComments = true;
+
for (;;) {
$s = $this->count;
if ($this->match('[>+~]+', $m, true)) {
- if ($subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0 &&
+ if (
+ $subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0 &&
$m[0] === '+' && $this->match("(\d+|n\b)", $counter)
) {
$this->seek($s);
if ($this->selectorSingle($part, $subSelector)) {
$selector[] = $part;
- $this->match('\s+', $m);
- continue;
- }
-
- if ($this->match('\/[^\/]+\/', $m, true)) {
- $selector[] = [$m[0]];
+ $this->whitespace();
continue;
}
break;
}
+ $this->discardComments = $discardComments;
+
if (! $selector) {
return false;
}
case '&':
$parts[] = Compiler::$selfSelector;
$this->count++;
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
continue 2;
case '.':
if ($this->placeholder($placeholder)) {
$parts[] = '%';
$parts[] = $placeholder;
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
continue;
}
if ($char === '#') {
if ($this->interpolation($inter)) {
$parts[] = $inter;
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
continue;
}
$ss = $this->count;
- if ($nameParts === ['not'] || $nameParts === ['is'] ||
- $nameParts === ['has'] || $nameParts === ['where'] ||
+ if (
+ $nameParts === ['not'] ||
+ $nameParts === ['is'] ||
+ $nameParts === ['has'] ||
+ $nameParts === ['where'] ||
$nameParts === ['slotted'] ||
- $nameParts === ['nth-child'] || $nameParts == ['nth-last-child'] ||
- $nameParts === ['nth-of-type'] || $nameParts == ['nth-last-of-type']
+ $nameParts === ['nth-child'] ||
+ $nameParts === ['nth-last-child'] ||
+ $nameParts === ['nth-of-type'] ||
+ $nameParts === ['nth-last-of-type']
) {
- if ($this->matchChar('(', true) &&
+ if (
+ $this->matchChar('(', true) &&
($this->selectors($subs, reset($nameParts)) || true) &&
$this->matchChar(')')
) {
} else {
$this->seek($ss);
}
- } else {
- if ($this->matchChar('(') &&
- ($this->openString(')', $str, '(') || true) &&
- $this->matchChar(')')
- ) {
- $parts[] = '(';
-
- if (! empty($str)) {
- $parts[] = $str;
- }
+ } elseif (
+ $this->matchChar('(', true) &&
+ ($this->openString(')', $str, '(') || true) &&
+ $this->matchChar(')')
+ ) {
+ $parts[] = '(';
- $parts[] = ')';
- } else {
- $this->seek($ss);
+ if (! empty($str)) {
+ $parts[] = $str;
}
+
+ $parts[] = ')';
+ } else {
+ $this->seek($ss);
}
continue;
$this->seek($s);
// attribute selector
- if ($char === '[' &&
+ if (
+ $char === '[' &&
$this->matchChar('[') &&
($this->openString(']', $str, '[') || true) &&
$this->matchChar(']')
{
$s = $this->count;
- if ($this->matchChar('$', false) && $this->keyword($name)) {
+ if (
+ $this->matchChar('$', false) &&
+ $this->keyword($name)
+ ) {
if ($this->allowVars) {
$out = [Type::T_VARIABLE, $name];
} else {
*/
protected function keyword(&$word, $eatWhitespace = null)
{
- if ($this->match(
+ $s = $this->count;
+ $match = $this->match(
$this->utf8
? '(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|[\\\\].)*)'
: '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
$m,
- $eatWhitespace
- )) {
+ false
+ );
+
+ if ($match) {
$word = $m[1];
+ // handling of escaping in keyword : get the escaped char
+ if (strpos($word, '\\') !== false) {
+ $send = $this->count;
+ $escapedWord = [];
+ $this->seek($s);
+ $previousEscape = false;
+ while ($this->count < $send) {
+ $char = $this->buffer[$this->count];
+ $this->count++;
+ if (
+ $this->count < $send
+ && $char === '\\'
+ && !$previousEscape
+ && $this->matchEscapeCharacter($out, true)
+ ) {
+ $escapedWord[] = $out;
+ } else {
+ if ($previousEscape) {
+ $previousEscape = false;
+ } elseif ($char === '\\') {
+ $previousEscape = true;
+ }
+ $escapedWord[] = $char;
+ }
+ }
+
+ $word = implode('', $escapedWord);
+ }
+
+ if (is_null($eatWhitespace) ? $this->eatWhiteDefault : $eatWhitespace) {
+ $this->whitespace();
+ }
+
return true;
}
*/
protected function placeholder(&$placeholder)
{
- if ($this->match(
+ $match = $this->match(
$this->utf8
? '([\pL\w\-_]+)'
: '([\w\-_]+)',
$m
- )) {
+ );
+
+ if ($match) {
$placeholder = $m[1];
return true;
*/
protected function url(&$out)
{
- if ($this->match('(url\(\s*(["\']?)([^)]+)\2\s*\))', $m)) {
- $out = [Type::T_STRING, '', ['url(' . $m[2] . $m[3] . $m[2] . ')']];
+ if ($this->literal('url(', 4)) {
+ $s = $this->count;
- return true;
+ if (
+ ($this->string($out) || $this->spaceList($out)) &&
+ $this->matchChar(')')
+ ) {
+ $out = [Type::T_STRING, '', ['url(', $out, ')']];
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ if (
+ $this->openString(')', $out) &&
+ $this->matchChar(')')
+ ) {
+ $out = [Type::T_STRING, '', ['url(', $out, ')']];
+
+ return true;
+ }
}
return false;
/**
* Consume an end of statement delimiter
+ * @param bool $eatWhitespace
*
* @return boolean
*/
- protected function end()
+ protected function end($eatWhitespace = null)
{
- if ($this->matchChar(';')) {
+ if ($this->matchChar(';', $eatWhitespace)) {
return true;
}