Scssphp 1.11

Update scssphp 1.11.0
This commit is contained in:
gtbu 2023-04-25 19:36:18 +02:00
parent bd025632d4
commit 4ea7ce8c2f
54 changed files with 6427 additions and 1998 deletions

View file

@ -16,17 +16,26 @@ namespace ScssPhp\ScssPhp\Base;
* Range
*
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @internal
*/
class Range
{
/**
* @var float|int
*/
public $first;
/**
* @var float|int
*/
public $last;
/**
* Initialize range
*
* @param integer|float $first
* @param integer|float $last
* @param int|float $first
* @param int|float $last
*/
public function __construct($first, $last)
{
@ -37,9 +46,9 @@ class Range
/**
* Test for inclusion in range
*
* @param integer|float $value
* @param int|float $value
*
* @return boolean
* @return bool
*/
public function includes($value)
{

View file

@ -16,16 +16,18 @@ namespace ScssPhp\ScssPhp;
* Block
*
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @internal
*/
class Block
{
/**
* @var string
* @var string|null
*/
public $type;
/**
* @var \ScssPhp\ScssPhp\Block
* @var Block|null
*/
public $parent;
@ -35,22 +37,22 @@ class Block
public $sourceName;
/**
* @var integer
* @var int
*/
public $sourceIndex;
/**
* @var integer
* @var int
*/
public $sourceLine;
/**
* @var integer
* @var int
*/
public $sourceColumn;
/**
* @var array
* @var array|null
*/
public $selectors;
@ -65,7 +67,7 @@ class Block
public $children;
/**
* @var \ScssPhp\ScssPhp\Block
* @var Block|null
*/
public $selfParent;
}

View file

@ -0,0 +1,37 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Type;
/**
* @internal
*/
class AtRootBlock extends Block
{
/**
* @var array|null
*/
public $selector;
/**
* @var array|null
*/
public $with;
public function __construct()
{
$this->type = Type::T_AT_ROOT;
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Compiler\Environment;
/**
* @internal
*/
class CallableBlock extends Block
{
/**
* @var string
*/
public $name;
/**
* @var array|null
*/
public $args;
/**
* @var Environment|null
*/
public $parentEnv;
/**
* @param string $type
*/
public function __construct($type)
{
$this->type = $type;
}
}

View file

@ -0,0 +1,38 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Compiler\Environment;
use ScssPhp\ScssPhp\Type;
/**
* @internal
*/
class ContentBlock extends Block
{
/**
* @var array|null
*/
public $child;
/**
* @var Environment|null
*/
public $scope;
public function __construct()
{
$this->type = Type::T_INCLUDE;
}
}

View file

@ -0,0 +1,37 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Type;
/**
* @internal
*/
class DirectiveBlock extends Block
{
/**
* @var string|array
*/
public $name;
/**
* @var string|array|null
*/
public $value;
public function __construct()
{
$this->type = Type::T_DIRECTIVE;
}
}

View file

@ -0,0 +1,37 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Type;
/**
* @internal
*/
class EachBlock extends Block
{
/**
* @var string[]
*/
public $vars = [];
/**
* @var array
*/
public $list;
public function __construct()
{
$this->type = Type::T_EACH;
}
}

View file

@ -0,0 +1,27 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Type;
/**
* @internal
*/
class ElseBlock extends Block
{
public function __construct()
{
$this->type = Type::T_ELSE;
}
}

View file

@ -0,0 +1,32 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Type;
/**
* @internal
*/
class ElseifBlock extends Block
{
/**
* @var array
*/
public $cond;
public function __construct()
{
$this->type = Type::T_ELSEIF;
}
}

View file

@ -0,0 +1,47 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Type;
/**
* @internal
*/
class ForBlock extends Block
{
/**
* @var string
*/
public $var;
/**
* @var array
*/
public $start;
/**
* @var array
*/
public $end;
/**
* @var bool
*/
public $until;
public function __construct()
{
$this->type = Type::T_FOR;
}
}

View file

@ -0,0 +1,37 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Type;
/**
* @internal
*/
class IfBlock extends Block
{
/**
* @var array
*/
public $cond;
/**
* @var array<ElseifBlock|ElseBlock>
*/
public $cases = [];
public function __construct()
{
$this->type = Type::T_IF;
}
}

View file

@ -0,0 +1,37 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Type;
/**
* @internal
*/
class MediaBlock extends Block
{
/**
* @var string|array|null
*/
public $value;
/**
* @var array|null
*/
public $queryList;
public function __construct()
{
$this->type = Type::T_MEDIA;
}
}

View file

@ -0,0 +1,37 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Type;
/**
* @internal
*/
class NestedPropertyBlock extends Block
{
/**
* @var bool
*/
public $hasValue;
/**
* @var array
*/
public $prefix;
public function __construct()
{
$this->type = Type::T_NESTED_PROPERTY;
}
}

View file

@ -0,0 +1,32 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Type;
/**
* @internal
*/
class WhileBlock extends Block
{
/**
* @var array
*/
public $cond;
public function __construct()
{
$this->type = Type::T_WHILE;
}
}

View file

@ -13,6 +13,7 @@
namespace ScssPhp\ScssPhp;
use Exception;
use ScssPhp\ScssPhp\Version;
/**
* The scss cache manager.
@ -29,30 +30,54 @@ use Exception;
* SCSS cache
*
* @author Cedric Morin <cedric@yterium.com>
*
* @internal
*/
class Cache
{
const CACHE_VERSION = 1;
// directory used for storing data
/**
* directory used for storing data
*
* @var string|false
*/
public static $cacheDir = false;
// prefix for the storing data
/**
* prefix for the storing data
*
* @var string
*/
public static $prefix = 'scssphp_';
// force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit
/**
* force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit
*
* @var bool|string
*/
public static $forceRefresh = false;
// specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up
/**
* specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up
*
* @var int
*/
public static $gcLifetime = 604800;
// array of already refreshed cache if $forceRefresh==='once'
/**
* array of already refreshed cache if $forceRefresh==='once'
*
* @var array<string, bool>
*/
protected static $refreshed = [];
/**
* Constructor
*
* @param array $options
*
* @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string} $options
*/
public function __construct($options)
{
@ -84,10 +109,10 @@ class Cache
* Get the cached result of $operation on $what,
* which is known as dependant from the content of $options
*
* @param string $operation parse, compile...
* @param mixed $what content key (e.g., filename to be treated)
* @param array $options any option that affect the operation result on the content
* @param integer $lastModified last modified timestamp
* @param string $operation parse, compile...
* @param mixed $what content key (e.g., filename to be treated)
* @param array $options any option that affect the operation result on the content
* @param int|null $lastModified last modified timestamp
*
* @return mixed
*
@ -127,6 +152,8 @@ class Cache
* @param mixed $what
* @param mixed $value
* @param array $options
*
* @return void
*/
public function setCache($operation, $what, $value, $options = [])
{
@ -156,6 +183,7 @@ class Cache
{
$t = [
'version' => self::CACHE_VERSION,
'scssphpVersion' => Version::VERSION,
'operation' => $operation,
'what' => $what,
'options' => $options
@ -172,6 +200,8 @@ class Cache
/**
* Check that the cache dir exists and is writeable
*
* @return void
*
* @throws \Exception
*/
public static function checkCacheDir()
@ -190,6 +220,8 @@ class Cache
/**
* Delete unused cached files
*
* @return void
*/
public static function cleanCache()
{

View file

@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp;
* CSS Colors
*
* @author Leaf Corcoran <leafot@gmail.com>
*
* @internal
*/
class Colors
{
@ -24,13 +26,13 @@ class Colors
*
* @see http://www.w3.org/TR/css3-color
*
* @var array
* @var array<string, string>
*/
protected static $cssColors = [
'aliceblue' => '240,248,255',
'antiquewhite' => '250,235,215',
'cyan' => '0,255,255',
'aqua' => '0,255,255',
'cyan' => '0,255,255',
'aquamarine' => '127,255,212',
'azure' => '240,255,255',
'beige' => '245,245,220',
@ -75,8 +77,8 @@ class Colors
'firebrick' => '178,34,34',
'floralwhite' => '255,250,240',
'forestgreen' => '34,139,34',
'magenta' => '255,0,255',
'fuchsia' => '255,0,255',
'magenta' => '255,0,255',
'gainsboro' => '220,220,220',
'ghostwhite' => '248,248,255',
'gold' => '255,215,0',
@ -183,7 +185,7 @@ class Colors
*
* @param string $colorName
*
* @return array|null
* @return int[]|null
*/
public static function colorNameToRGBa($colorName)
{
@ -202,10 +204,10 @@ class Colors
/**
* Reverse conversion : from RGBA to a color name if possible
*
* @param integer $r
* @param integer $g
* @param integer $b
* @param integer $a
* @param int $r
* @param int $g
* @param int $b
* @param int|float $a
*
* @return string|null
*/

View file

@ -0,0 +1,69 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp;
class CompilationResult
{
/**
* @var string
*/
private $css;
/**
* @var string|null
*/
private $sourceMap;
/**
* @var string[]
*/
private $includedFiles;
/**
* @param string $css
* @param string|null $sourceMap
* @param string[] $includedFiles
*/
public function __construct($css, $sourceMap, array $includedFiles)
{
$this->css = $css;
$this->sourceMap = $sourceMap;
$this->includedFiles = $includedFiles;
}
/**
* @return string
*/
public function getCss()
{
return $this->css;
}
/**
* @return string[]
*/
public function getIncludedFiles()
{
return $this->includedFiles;
}
/**
* The sourceMap content, if it was generated
*
* @return null|string
*/
public function getSourceMap()
{
return $this->sourceMap;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,77 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Compiler;
use ScssPhp\ScssPhp\CompilationResult;
/**
* @internal
*/
class CachedResult
{
/**
* @var CompilationResult
*/
private $result;
/**
* @var array<string, int>
*/
private $parsedFiles;
/**
* @var array
* @phpstan-var list<array{currentDir: string|null, path: string, filePath: string}>
*/
private $resolvedImports;
/**
* @param CompilationResult $result
* @param array<string, int> $parsedFiles
* @param array $resolvedImports
*
* @phpstan-param list<array{currentDir: string|null, path: string, filePath: string}> $resolvedImports
*/
public function __construct(CompilationResult $result, array $parsedFiles, array $resolvedImports)
{
$this->result = $result;
$this->parsedFiles = $parsedFiles;
$this->resolvedImports = $resolvedImports;
}
/**
* @return CompilationResult
*/
public function getResult()
{
return $this->result;
}
/**
* @return array<string, int>
*/
public function getParsedFiles()
{
return $this->parsedFiles;
}
/**
* @return array
*
* @phpstan-return list<array{currentDir: string|null, path: string, filePath: string}>
*/
public function getResolvedImports()
{
return $this->resolvedImports;
}
}

View file

@ -16,19 +16,41 @@ namespace ScssPhp\ScssPhp\Compiler;
* Compiler environment
*
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @internal
*/
class Environment
{
/**
* @var \ScssPhp\ScssPhp\Block
* @var \ScssPhp\ScssPhp\Block|null
*/
public $block;
/**
* @var \ScssPhp\ScssPhp\Compiler\Environment
* @var \ScssPhp\ScssPhp\Compiler\Environment|null
*/
public $parent;
/**
* @var Environment|null
*/
public $declarationScopeParent;
/**
* @var Environment|null
*/
public $parentStore;
/**
* @var array|null
*/
public $selectors;
/**
* @var string|null
*/
public $marker;
/**
* @var array
*/
@ -40,7 +62,7 @@ class Environment
public $storeUnreduced;
/**
* @var integer
* @var int
*/
public $depth;
}

View file

@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp\Exception;
* Compiler exception
*
* @author Oleksandr Savchenko <traveltino@gmail.com>
*
* @internal
*/
class CompilerException extends \Exception implements SassException
{

View file

@ -16,11 +16,14 @@ namespace ScssPhp\ScssPhp\Exception;
* Parser Exception
*
* @author Oleksandr Savchenko <traveltino@gmail.com>
*
* @internal
*/
class ParserException extends \Exception implements SassException
{
/**
* @var array
* @var array|null
* @phpstan-var array{string, int, int}|null
*/
private $sourcePosition;
@ -28,6 +31,9 @@ class ParserException extends \Exception implements SassException
* Get source position
*
* @api
*
* @return array|null
* @phpstan-return array{string, int, int}|null
*/
public function getSourcePosition()
{
@ -40,6 +46,10 @@ class ParserException extends \Exception implements SassException
* @api
*
* @param array $sourcePosition
*
* @return void
*
* @phpstan-param array{string, int, int} $sourcePosition
*/
public function setSourcePosition($sourcePosition)
{

View file

@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp\Exception;
* Range exception
*
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @internal
*/
class RangeException extends \Exception implements SassException
{

View file

@ -0,0 +1,32 @@
<?php
namespace ScssPhp\ScssPhp\Exception;
/**
* An exception thrown by SassScript.
*
* This class does not implement SassException on purpose, as it should
* never be returned to the outside code. The compilation will catch it
* and replace it with a SassException reporting the location of the
* error.
*/
class SassScriptException extends \Exception
{
/**
* Creates a SassScriptException with support for an argument name.
*
* This helper ensures a consistent handling of argument names in the
* error message, without duplicating it.
*
* @param string $message
* @param string|null $name The argument name, without $
*
* @return SassScriptException
*/
public static function forArgument($message, $name = null)
{
$varDisplay = !\is_null($name) ? "\${$name}: " : '';
return new self($varDisplay . $message);
}
}

View file

@ -12,10 +12,14 @@
namespace ScssPhp\ScssPhp\Exception;
@trigger_error(sprintf('The "%s" class is deprecated.', ServerException::class), E_USER_DEPRECATED);
/**
* Server Exception
*
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @deprecated The Scssphp server should define its own exception instead.
*/
class ServerException extends \Exception implements SassException
{

View file

@ -19,11 +19,13 @@ use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
* Base formatter
*
* @author Leaf Corcoran <leafot@gmail.com>
*
* @internal
*/
abstract class Formatter
{
/**
* @var integer
* @var int
*/
public $indentLevel;
@ -58,7 +60,7 @@ abstract class Formatter
public $assignSeparator;
/**
* @var boolean
* @var bool
*/
public $keepSemicolons;
@ -68,17 +70,17 @@ abstract class Formatter
protected $currentBlock;
/**
* @var integer
* @var int
*/
protected $currentLine;
/**
* @var integer
* @var int
*/
protected $currentColumn;
/**
* @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator
* @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null
*/
protected $sourceMapGenerator;
@ -139,6 +141,8 @@ abstract class Formatter
* Output lines inside a block
*
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
*
* @return void
*/
protected function blockLines(OutputBlock $block)
{
@ -156,9 +160,13 @@ abstract class Formatter
* Output block selectors
*
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
*
* @return void
*/
protected function blockSelectors(OutputBlock $block)
{
assert(! empty($block->selectors));
$inner = $this->indentStr();
$this->write($inner
@ -170,6 +178,8 @@ abstract class Formatter
* Output block children
*
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
*
* @return void
*/
protected function blockChildren(OutputBlock $block)
{
@ -182,6 +192,8 @@ abstract class Formatter
* Output non-empty block
*
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
*
* @return void
*/
protected function block(OutputBlock $block)
{
@ -227,7 +239,7 @@ abstract class Formatter
*
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
*
* @return boolean
* @return bool
*/
protected function testEmptyChildren($block)
{
@ -274,9 +286,18 @@ abstract class Formatter
ob_start();
$this->block($block);
try {
$this->block($block);
} catch (\Exception $e) {
ob_end_clean();
throw $e;
} catch (\Throwable $e) {
ob_end_clean();
throw $e;
}
$out = ob_get_clean();
assert($out !== false);
return $out;
}
@ -285,6 +306,8 @@ abstract class Formatter
* Output content
*
* @param string $str
*
* @return void
*/
protected function write($str)
{
@ -310,22 +333,43 @@ abstract class Formatter
}
if ($this->sourceMapGenerator) {
$this->sourceMapGenerator->addMapping(
$this->currentLine,
$this->currentColumn,
$this->currentBlock->sourceLine,
//columns from parser are off by one
$this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0,
$this->currentBlock->sourceName
);
$lines = explode("\n", $str);
$lineCount = \count($lines);
$this->currentLine += $lineCount - 1;
$lastLine = array_pop($lines);
$this->currentColumn = ($lineCount === 1 ? $this->currentColumn : 0) + \strlen($lastLine);
foreach ($lines as $line) {
// If the written line starts is empty, adding a mapping would add it for
// a non-existent column as we are at the end of the line
if ($line !== '') {
assert($this->currentBlock->sourceLine !== null);
assert($this->currentBlock->sourceName !== null);
$this->sourceMapGenerator->addMapping(
$this->currentLine,
$this->currentColumn,
$this->currentBlock->sourceLine,
//columns from parser are off by one
$this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0,
$this->currentBlock->sourceName
);
}
$this->currentLine++;
$this->currentColumn = 0;
}
if ($lastLine !== '') {
assert($this->currentBlock->sourceLine !== null);
assert($this->currentBlock->sourceName !== null);
$this->sourceMapGenerator->addMapping(
$this->currentLine,
$this->currentColumn,
$this->currentBlock->sourceLine,
//columns from parser are off by one
$this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0,
$this->currentBlock->sourceName
);
}
$this->currentColumn += \strlen($lastLine);
}
echo $str;

View file

@ -18,6 +18,10 @@ use ScssPhp\ScssPhp\Formatter;
* Compact formatter
*
* @author Leaf Corcoran <leafot@gmail.com>
*
* @deprecated since 1.4.0. Use the Compressed formatter instead.
*
* @internal
*/
class Compact extends Formatter
{
@ -26,6 +30,8 @@ class Compact extends Formatter
*/
public function __construct()
{
@trigger_error('The Compact formatter is deprecated since 1.4.0. Use the Compressed formatter instead.', E_USER_DEPRECATED);
$this->indentLevel = 0;
$this->indentChar = '';
$this->break = '';

View file

@ -13,12 +13,13 @@
namespace ScssPhp\ScssPhp\Formatter;
use ScssPhp\ScssPhp\Formatter;
use ScssPhp\ScssPhp\Formatter\OutputBlock;
/**
* Compressed formatter
*
* @author Leaf Corcoran <leafot@gmail.com>
*
* @internal
*/
class Compressed extends Formatter
{
@ -49,8 +50,6 @@ class Compressed extends Formatter
foreach ($block->lines as $index => $line) {
if (substr($line, 0, 2) === '/*' && substr($line, 2, 1) !== '!') {
unset($block->lines[$index]);
} elseif (substr($line, 0, 3) === '/*!') {
$block->lines[$index] = '/*' . substr($line, 3);
}
}
@ -68,6 +67,8 @@ class Compressed extends Formatter
*/
protected function blockSelectors(OutputBlock $block)
{
assert(! empty($block->selectors));
$inner = $this->indentStr();
$this->write(

View file

@ -13,12 +13,15 @@
namespace ScssPhp\ScssPhp\Formatter;
use ScssPhp\ScssPhp\Formatter;
use ScssPhp\ScssPhp\Formatter\OutputBlock;
/**
* Crunched formatter
*
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @deprecated since 1.4.0. Use the Compressed formatter instead.
*
* @internal
*/
class Crunched extends Formatter
{
@ -27,6 +30,8 @@ class Crunched extends Formatter
*/
public function __construct()
{
@trigger_error('The Crunched formatter is deprecated since 1.4.0. Use the Compressed formatter instead.', E_USER_DEPRECATED);
$this->indentLevel = 0;
$this->indentChar = ' ';
$this->break = '';
@ -66,6 +71,8 @@ class Crunched extends Formatter
*/
protected function blockSelectors(OutputBlock $block)
{
assert(! empty($block->selectors));
$inner = $this->indentStr();
$this->write(

View file

@ -13,12 +13,15 @@
namespace ScssPhp\ScssPhp\Formatter;
use ScssPhp\ScssPhp\Formatter;
use ScssPhp\ScssPhp\Formatter\OutputBlock;
/**
* Debug formatter
*
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @deprecated since 1.4.0.
*
* @internal
*/
class Debug extends Formatter
{
@ -27,6 +30,8 @@ class Debug extends Formatter
*/
public function __construct()
{
@trigger_error('The Debug formatter is deprecated since 1.4.0.', E_USER_DEPRECATED);
$this->indentLevel = 0;
$this->indentChar = '';
$this->break = "\n";

View file

@ -13,12 +13,13 @@
namespace ScssPhp\ScssPhp\Formatter;
use ScssPhp\ScssPhp\Formatter;
use ScssPhp\ScssPhp\Formatter\OutputBlock;
/**
* Expanded formatter
*
* @author Leaf Corcoran <leafot@gmail.com>
*
* @internal
*/
class Expanded extends Formatter
{
@ -56,7 +57,9 @@ class Expanded extends Formatter
foreach ($block->lines as $index => $line) {
if (substr($line, 0, 2) === '/*') {
$block->lines[$index] = preg_replace('/\r\n?|\n|\f/', $this->break, $line);
$replacedLine = preg_replace('/\r\n?|\n|\f/', $this->break, $line);
assert($replacedLine !== null);
$block->lines[$index] = $replacedLine;
}
}

View file

@ -13,18 +13,21 @@
namespace ScssPhp\ScssPhp\Formatter;
use ScssPhp\ScssPhp\Formatter;
use ScssPhp\ScssPhp\Formatter\OutputBlock;
use ScssPhp\ScssPhp\Type;
/**
* Nested formatter
*
* @author Leaf Corcoran <leafot@gmail.com>
*
* @deprecated since 1.4.0. Use the Expanded formatter instead.
*
* @internal
*/
class Nested extends Formatter
{
/**
* @var integer
* @var int
*/
private $depth;
@ -33,6 +36,8 @@ class Nested extends Formatter
*/
public function __construct()
{
@trigger_error('The Nested formatter is deprecated since 1.4.0. Use the Expanded formatter instead.', E_USER_DEPRECATED);
$this->indentLevel = 0;
$this->indentChar = ' ';
$this->break = "\n";
@ -63,7 +68,9 @@ class Nested extends Formatter
foreach ($block->lines as $index => $line) {
if (substr($line, 0, 2) === '/*') {
$block->lines[$index] = preg_replace('/\r\n?|\n|\f/', $this->break, $line);
$replacedLine = preg_replace('/\r\n?|\n|\f/', $this->break, $line);
assert($replacedLine !== null);
$block->lines[$index] = $replacedLine;
}
}
@ -216,7 +223,7 @@ class Nested extends Formatter
*
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
*
* @return boolean
* @return bool
*/
private function hasFlatChild($block)
{

View file

@ -16,51 +16,53 @@ namespace ScssPhp\ScssPhp\Formatter;
* Output block
*
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @internal
*/
class OutputBlock
{
/**
* @var string
* @var string|null
*/
public $type;
/**
* @var integer
* @var int
*/
public $depth;
/**
* @var array
* @var array|null
*/
public $selectors;
/**
* @var array
* @var string[]
*/
public $lines;
/**
* @var array
* @var OutputBlock[]
*/
public $children;
/**
* @var \ScssPhp\ScssPhp\Formatter\OutputBlock
* @var OutputBlock|null
*/
public $parent;
/**
* @var string
* @var string|null
*/
public $sourceName;
/**
* @var integer
* @var int|null
*/
public $sourceLine;
/**
* @var integer
* @var int|null
*/
public $sourceColumn;
}

View file

@ -0,0 +1,48 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Logger;
/**
* Interface implemented by loggers for warnings and debug messages.
*
* The official Sass implementation recommends that loggers report the
* messages immediately rather than waiting for the end of the
* compilation, to provide a better debugging experience when the
* compilation does not end (error or infinite loop after the warning
* for instance).
*/
interface LoggerInterface
{
/**
* Emits a warning with the given message.
*
* If $deprecation is true, it indicates that this is a deprecation
* warning. Implementations should surface all this information to
* the end user.
*
* @param string $message
* @param bool $deprecation
*
* @return void
*/
public function warn($message, $deprecation = false);
/**
* Emits a debugging message.
*
* @param string $message
*
* @return void
*/
public function debug($message);
}

View file

@ -0,0 +1,29 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Logger;
/**
* A logger that silently ignores all messages.
*
* @final
*/
class QuietLogger implements LoggerInterface
{
public function warn($message, $deprecation = false)
{
}
public function debug($message)
{
}
}

View file

@ -0,0 +1,62 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Logger;
/**
* A logger that prints to a PHP stream (for instance stderr)
*
* @final
*/
class StreamLogger implements LoggerInterface
{
private $stream;
private $closeOnDestruct;
/**
* @param resource $stream A stream resource
* @param bool $closeOnDestruct If true, takes ownership of the stream and close it on destruct to avoid leaks.
*/
public function __construct($stream, $closeOnDestruct = false)
{
$this->stream = $stream;
$this->closeOnDestruct = $closeOnDestruct;
}
/**
* @internal
*/
public function __destruct()
{
if ($this->closeOnDestruct) {
fclose($this->stream);
}
}
/**
* @inheritDoc
*/
public function warn($message, $deprecation = false)
{
$prefix = ($deprecation ? 'DEPRECATION ' : '') . 'WARNING: ';
fwrite($this->stream, $prefix . $message . "\n\n");
}
/**
* @inheritDoc
*/
public function debug($message)
{
fwrite($this->stream, $message . "\n");
}
}

View file

@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp;
* Base node
*
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @internal
*/
abstract class Node
{
@ -25,17 +27,17 @@ abstract class Node
public $type;
/**
* @var integer
* @var int
*/
public $sourceIndex;
/**
* @var integer
* @var int|null
*/
public $sourceLine;
/**
* @var integer
* @var int|null
*/
public $sourceColumn;
}

View file

@ -12,9 +12,13 @@
namespace ScssPhp\ScssPhp\Node;
use ScssPhp\ScssPhp\Base\Range;
use ScssPhp\ScssPhp\Compiler;
use ScssPhp\ScssPhp\Exception\RangeException;
use ScssPhp\ScssPhp\Exception\SassScriptException;
use ScssPhp\ScssPhp\Node;
use ScssPhp\ScssPhp\Type;
use ScssPhp\ScssPhp\Util;
/**
* Dimension + optional units
@ -26,13 +30,15 @@ use ScssPhp\ScssPhp\Type;
* }}
*
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @template-implements \ArrayAccess<int, mixed>
*/
class Number extends Node implements \ArrayAccess
{
const PRECISION = 10;
/**
* @var integer
* @var int
* @deprecated use {Number::PRECISION} instead to read the precision. Configuring it is not supported anymore.
*/
public static $precision = self::PRECISION;
@ -41,6 +47,7 @@ class Number extends Node implements \ArrayAccess
* @see http://www.w3.org/TR/2012/WD-css3-values-20120308/
*
* @var array
* @phpstan-var array<string, array<string, float|int>>
*/
protected static $unitTable = [
'in' => [
@ -74,82 +81,75 @@ class Number extends Node implements \ArrayAccess
];
/**
* @var integer|float
* @var int|float
*/
public $dimension;
private $dimension;
/**
* @var array
* @var string[]
* @phpstan-var list<string>
*/
public $units;
private $numeratorUnits;
/**
* @var string[]
* @phpstan-var list<string>
*/
private $denominatorUnits;
/**
* Initialize number
*
* @param mixed $dimension
* @param mixed $initialUnit
* @param int|float $dimension
* @param string[]|string $numeratorUnits
* @param string[] $denominatorUnits
*
* @phpstan-param list<string>|string $numeratorUnits
* @phpstan-param list<string> $denominatorUnits
*/
public function __construct($dimension, $initialUnit)
public function __construct($dimension, $numeratorUnits, array $denominatorUnits = [])
{
$this->type = Type::T_NUMBER;
if (is_string($numeratorUnits)) {
$numeratorUnits = $numeratorUnits ? [$numeratorUnits] : [];
} elseif (isset($numeratorUnits['numerator_units'], $numeratorUnits['denominator_units'])) {
// TODO get rid of this once `$number[2]` is not used anymore
$denominatorUnits = $numeratorUnits['denominator_units'];
$numeratorUnits = $numeratorUnits['numerator_units'];
}
$this->dimension = $dimension;
$this->units = \is_array($initialUnit)
? $initialUnit
: ($initialUnit ? [$initialUnit => 1]
: []);
$this->numeratorUnits = $numeratorUnits;
$this->denominatorUnits = $denominatorUnits;
}
/**
* Coerce number to target units
*
* @param array $units
*
* @return \ScssPhp\ScssPhp\Node\Number
* @return float|int
*/
public function coerce($units)
public function getDimension()
{
if ($this->unitless()) {
return new Number($this->dimension, $units);
}
$dimension = $this->dimension;
if (\count($units)) {
$baseUnit = array_keys($units);
$baseUnit = reset($baseUnit);
$baseUnit = $this->findBaseUnit($baseUnit);
if ($baseUnit && isset(static::$unitTable[$baseUnit])) {
foreach (static::$unitTable[$baseUnit] as $unit => $conv) {
$from = isset($this->units[$unit]) ? $this->units[$unit] : 0;
$to = isset($units[$unit]) ? $units[$unit] : 0;
$factor = pow($conv, $from - $to);
$dimension /= $factor;
}
}
}
return new Number($dimension, $units);
return $this->dimension;
}
/**
* Normalize number
*
* @return \ScssPhp\ScssPhp\Node\Number
* @return string[]
*/
public function normalize()
public function getNumeratorUnits()
{
$dimension = $this->dimension;
$units = [];
$this->normalizeUnits($dimension, $units);
return new Number($dimension, $units);
return $this->numeratorUnits;
}
/**
* {@inheritdoc}
* @return string[]
*/
#[\ReturnTypeWillChange]
public function getDenominatorUnits()
{
return $this->denominatorUnits;
}
/**
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
if ($offset === -3) {
@ -173,9 +173,9 @@ class Number extends Node implements \ArrayAccess
}
/**
* {@inheritdoc}
* @return mixed
*/
#[\ReturnTypeWillChange]
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
switch ($offset) {
@ -189,91 +189,64 @@ class Number extends Node implements \ArrayAccess
return $this->sourceIndex;
case 0:
return $this->type;
return Type::T_NUMBER;
case 1:
return $this->dimension;
case 2:
return $this->units;
return array('numerator_units' => $this->numeratorUnits, 'denominator_units' => $this->denominatorUnits);
}
}
/**
* {@inheritdoc}
* @return void
*/
#[\ReturnTypeWillChange]
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
{
if ($offset === 1) {
$this->dimension = $value;
} elseif ($offset === 2) {
$this->units = $value;
} elseif ($offset == -1) {
$this->sourceIndex = $value;
} elseif ($offset == -2) {
$this->sourceLine = $value;
} elseif ($offset == -3) {
$this->sourceColumn = $value;
}
throw new \BadMethodCallException('Number is immutable');
}
/**
* {@inheritdoc}
* @return void
*/
#[\ReturnTypeWillChange]
#[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
if ($offset === 1) {
$this->dimension = null;
} elseif ($offset === 2) {
$this->units = null;
} elseif ($offset === -1) {
$this->sourceIndex = null;
} elseif ($offset === -2) {
$this->sourceLine = null;
} elseif ($offset === -3) {
$this->sourceColumn = null;
}
throw new \BadMethodCallException('Number is immutable');
}
/**
* Returns true if the number is unitless
*
* @return boolean
* @return bool
*/
public function unitless()
{
return ! array_sum($this->units);
return \count($this->numeratorUnits) === 0 && \count($this->denominatorUnits) === 0;
}
/**
* Test if a number can be normalized in a base unit
* ie if its units are homogeneous
* Returns true if the number has any units
*
* @return boolean
* @return bool
*/
public function isNormalizable()
public function hasUnits()
{
if ($this->unitless()) {
return false;
}
return !$this->unitless();
}
$baseUnit = null;
foreach ($this->units as $unit => $exp) {
$b = $this->findBaseUnit($unit);
if (\is_null($baseUnit)) {
$baseUnit = $b;
}
if (\is_null($b) or $b !== $baseUnit) {
return false;
}
}
return $baseUnit;
/**
* Checks whether the number has exactly this unit
*
* @param string $unit
*
* @return bool
*/
public function hasUnit($unit)
{
return \count($this->numeratorUnits) === 1 && \count($this->denominatorUnits) === 0 && $this->numeratorUnits[0] === $unit;
}
/**
@ -283,22 +256,309 @@ class Number extends Node implements \ArrayAccess
*/
public function unitStr()
{
$numerators = [];
$denominators = [];
foreach ($this->units as $unit => $unitSize) {
if ($unitSize > 0) {
$numerators = array_pad($numerators, \count($numerators) + $unitSize, $unit);
continue;
}
if ($unitSize < 0) {
$denominators = array_pad($denominators, \count($denominators) - $unitSize, $unit);
continue;
}
if ($this->unitless()) {
return '';
}
return implode('*', $numerators) . (\count($denominators) ? '/' . implode('*', $denominators) : '');
return self::getUnitString($this->numeratorUnits, $this->denominatorUnits);
}
/**
* @param float|int $min
* @param float|int $max
* @param string|null $name
*
* @return float|int
* @throws SassScriptException
*/
public function valueInRange($min, $max, $name = null)
{
try {
return Util::checkRange('', new Range($min, $max), $this);
} catch (RangeException $e) {
throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s.', $this, $min, $this->unitStr(), $max), $name);
}
}
/**
* @param float|int $min
* @param float|int $max
* @param string $name
* @param string $unit
*
* @return float|int
* @throws SassScriptException
*
* @internal
*/
public function valueInRangeWithUnit($min, $max, $name, $unit)
{
try {
return Util::checkRange('', new Range($min, $max), $this);
} catch (RangeException $e) {
throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s.', $this, $min, $unit, $max), $name);
}
}
/**
* @param string|null $varName
*
* @return void
*/
public function assertNoUnits($varName = null)
{
if ($this->unitless()) {
return;
}
throw SassScriptException::forArgument(sprintf('Expected %s to have no units.', $this), $varName);
}
/**
* @param string $unit
* @param string|null $varName
*
* @return void
*/
public function assertUnit($unit, $varName = null)
{
if ($this->hasUnit($unit)) {
return;
}
throw SassScriptException::forArgument(sprintf('Expected %s to have unit "%s".', $this, $unit), $varName);
}
/**
* @param Number $other
*
* @return void
*/
public function assertSameUnitOrUnitless(Number $other)
{
if ($other->unitless()) {
return;
}
if ($this->numeratorUnits === $other->numeratorUnits && $this->denominatorUnits === $other->denominatorUnits) {
return;
}
throw new SassScriptException(sprintf(
'Incompatible units %s and %s.',
self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
self::getUnitString($other->numeratorUnits, $other->denominatorUnits)
));
}
/**
* Returns a copy of this number, converted to the units represented by $newNumeratorUnits and $newDenominatorUnits.
*
* This does not throw an error if this number is unitless and
* $newNumeratorUnits/$newDenominatorUnits are not empty, or vice versa. Instead,
* it treats all unitless numbers as convertible to and from all units without
* changing the value.
*
* @param string[] $newNumeratorUnits
* @param string[] $newDenominatorUnits
*
* @return Number
*
* @phpstan-param list<string> $newNumeratorUnits
* @phpstan-param list<string> $newDenominatorUnits
*
* @throws SassScriptException if this number's units are not compatible with $newNumeratorUnits and $newDenominatorUnits
*/
public function coerce(array $newNumeratorUnits, array $newDenominatorUnits)
{
return new Number($this->valueInUnits($newNumeratorUnits, $newDenominatorUnits), $newNumeratorUnits, $newDenominatorUnits);
}
/**
* @param Number $other
*
* @return bool
*/
public function isComparableTo(Number $other)
{
if ($this->unitless() || $other->unitless()) {
return true;
}
try {
$this->greaterThan($other);
return true;
} catch (SassScriptException $e) {
return false;
}
}
/**
* @param Number $other
*
* @return bool
*/
public function lessThan(Number $other)
{
return $this->coerceUnits($other, function ($num1, $num2) {
return $num1 < $num2;
});
}
/**
* @param Number $other
*
* @return bool
*/
public function lessThanOrEqual(Number $other)
{
return $this->coerceUnits($other, function ($num1, $num2) {
return $num1 <= $num2;
});
}
/**
* @param Number $other
*
* @return bool
*/
public function greaterThan(Number $other)
{
return $this->coerceUnits($other, function ($num1, $num2) {
return $num1 > $num2;
});
}
/**
* @param Number $other
*
* @return bool
*/
public function greaterThanOrEqual(Number $other)
{
return $this->coerceUnits($other, function ($num1, $num2) {
return $num1 >= $num2;
});
}
/**
* @param Number $other
*
* @return Number
*/
public function plus(Number $other)
{
return $this->coerceNumber($other, function ($num1, $num2) {
return $num1 + $num2;
});
}
/**
* @param Number $other
*
* @return Number
*/
public function minus(Number $other)
{
return $this->coerceNumber($other, function ($num1, $num2) {
return $num1 - $num2;
});
}
/**
* @return Number
*/
public function unaryMinus()
{
return new Number(-$this->dimension, $this->numeratorUnits, $this->denominatorUnits);
}
/**
* @param Number $other
*
* @return Number
*/
public function modulo(Number $other)
{
return $this->coerceNumber($other, function ($num1, $num2) {
if ($num2 == 0) {
return NAN;
}
$result = fmod($num1, $num2);
if ($result == 0) {
return 0;
}
if ($num2 < 0 xor $num1 < 0) {
$result += $num2;
}
return $result;
});
}
/**
* @param Number $other
*
* @return Number
*/
public function times(Number $other)
{
return $this->multiplyUnits($this->dimension * $other->dimension, $this->numeratorUnits, $this->denominatorUnits, $other->numeratorUnits, $other->denominatorUnits);
}
/**
* @param Number $other
*
* @return Number
*/
public function dividedBy(Number $other)
{
if ($other->dimension == 0) {
if ($this->dimension == 0) {
$value = NAN;
} elseif ($this->dimension > 0) {
$value = INF;
} else {
$value = -INF;
}
} else {
$value = $this->dimension / $other->dimension;
}
return $this->multiplyUnits($value, $this->numeratorUnits, $this->denominatorUnits, $other->denominatorUnits, $other->numeratorUnits);
}
/**
* @param Number $other
*
* @return bool
*/
public function equals(Number $other)
{
// Unitless numbers are convertable to unit numbers, but not equal, so we special-case unitless here.
if ($this->unitless() !== $other->unitless()) {
return false;
}
// In Sass, neither NaN nor Infinity are equal to themselves, while PHP defines INF==INF
if (is_nan($this->dimension) || is_nan($other->dimension) || !is_finite($this->dimension) || !is_finite($other->dimension)) {
return false;
}
if ($this->unitless()) {
return round($this->dimension, self::PRECISION) == round($other->dimension, self::PRECISION);
}
try {
return $this->coerceUnits($other, function ($num1, $num2) {
return round($num1,self::PRECISION) == round($num2, self::PRECISION);
});
} catch (SassScriptException $e) {
return false;
}
}
/**
@ -312,35 +572,29 @@ class Number extends Node implements \ArrayAccess
{
$dimension = round($this->dimension, self::PRECISION);
$units = array_filter($this->units, function ($unitSize) {
return $unitSize;
});
if (\count($units) > 1 && array_sum($units) === 0) {
$dimension = $this->dimension;
$units = [];
$this->normalizeUnits($dimension, $units);
$dimension = round($dimension, self::PRECISION);
$units = array_filter($units, function ($unitSize) {
return $unitSize;
});
if (is_nan($dimension)) {
return 'NaN';
}
$unitSize = array_sum($units);
if ($dimension === INF) {
return 'Infinity';
}
if ($compiler && ($unitSize > 1 || $unitSize < 0 || \count($units) > 1)) {
$this->units = $units;
if ($dimension === -INF) {
return '-Infinity';
}
if ($compiler) {
$unit = $this->unitStr();
} elseif (isset($this->numeratorUnits[0])) {
$unit = $this->numeratorUnits[0];
} else {
reset($units);
$unit = key($units);
$unit = '';
}
$dimension = number_format($dimension, self::PRECISION, '.', '');
return (self::PRECISION ? rtrim(rtrim($dimension, '0'), '.') : $dimension) . $unit;
return rtrim(rtrim($dimension, '0'), '.') . $unit;
}
/**
@ -352,48 +606,229 @@ class Number extends Node implements \ArrayAccess
}
/**
* Normalize units
* @param Number $other
* @param callable $operation
*
* @param integer|float $dimension
* @param array $units
* @param string $baseUnit
* @return Number
*
* @phpstan-param callable(int|float, int|float): (int|float) $operation
*/
private function normalizeUnits(&$dimension, &$units, $baseUnit = null)
private function coerceNumber(Number $other, $operation)
{
$dimension = $this->dimension;
$units = [];
$result = $this->coerceUnits($other, $operation);
foreach ($this->units as $unit => $exp) {
if (! $baseUnit) {
$baseUnit = $this->findBaseUnit($unit);
}
if ($baseUnit && isset(static::$unitTable[$baseUnit][$unit])) {
$factor = pow(static::$unitTable[$baseUnit][$unit], $exp);
$unit = $baseUnit;
$dimension /= $factor;
}
$units[$unit] = $exp + (isset($units[$unit]) ? $units[$unit] : 0);
if (!$this->unitless()) {
return new Number($result, $this->numeratorUnits, $this->denominatorUnits);
}
return new Number($result, $other->numeratorUnits, $other->denominatorUnits);
}
/**
* Find the base unit family for a given unit
* @param Number $other
* @param callable $operation
*
* @param string $unit
* @return mixed
*
* @return string|null
* @phpstan-template T
* @phpstan-param callable(int|float, int|float): T $operation
* @phpstan-return T
*/
private function findBaseUnit($unit)
private function coerceUnits(Number $other, $operation)
{
foreach (static::$unitTable as $baseUnit => $unitVariants) {
if (isset($unitVariants[$unit])) {
return $baseUnit;
if (!$this->unitless()) {
$num1 = $this->dimension;
$num2 = $other->valueInUnits($this->numeratorUnits, $this->denominatorUnits);
} else {
$num1 = $this->valueInUnits($other->numeratorUnits, $other->denominatorUnits);
$num2 = $other->dimension;
}
return \call_user_func($operation, $num1, $num2);
}
/**
* @param string[] $numeratorUnits
* @param string[] $denominatorUnits
*
* @return int|float
*
* @phpstan-param list<string> $numeratorUnits
* @phpstan-param list<string> $denominatorUnits
*
* @throws SassScriptException if this number's units are not compatible with $numeratorUnits and $denominatorUnits
*/
private function valueInUnits(array $numeratorUnits, array $denominatorUnits)
{
if (
$this->unitless()
|| (\count($numeratorUnits) === 0 && \count($denominatorUnits) === 0)
|| ($this->numeratorUnits === $numeratorUnits && $this->denominatorUnits === $denominatorUnits)
) {
return $this->dimension;
}
$value = $this->dimension;
$oldNumerators = $this->numeratorUnits;
foreach ($numeratorUnits as $newNumerator) {
foreach ($oldNumerators as $key => $oldNumerator) {
$conversionFactor = self::getConversionFactor($newNumerator, $oldNumerator);
if (\is_null($conversionFactor)) {
continue;
}
$value *= $conversionFactor;
unset($oldNumerators[$key]);
continue 2;
}
throw new SassScriptException(sprintf(
'Incompatible units %s and %s.',
self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
self::getUnitString($numeratorUnits, $denominatorUnits)
));
}
$oldDenominators = $this->denominatorUnits;
foreach ($denominatorUnits as $newDenominator) {
foreach ($oldDenominators as $key => $oldDenominator) {
$conversionFactor = self::getConversionFactor($newDenominator, $oldDenominator);
if (\is_null($conversionFactor)) {
continue;
}
$value /= $conversionFactor;
unset($oldDenominators[$key]);
continue 2;
}
throw new SassScriptException(sprintf(
'Incompatible units %s and %s.',
self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
self::getUnitString($numeratorUnits, $denominatorUnits)
));
}
if (\count($oldNumerators) || \count($oldDenominators)) {
throw new SassScriptException(sprintf(
'Incompatible units %s and %s.',
self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
self::getUnitString($numeratorUnits, $denominatorUnits)
));
}
return $value;
}
/**
* @param int|float $value
* @param string[] $numerators1
* @param string[] $denominators1
* @param string[] $numerators2
* @param string[] $denominators2
*
* @return Number
*
* @phpstan-param list<string> $numerators1
* @phpstan-param list<string> $denominators1
* @phpstan-param list<string> $numerators2
* @phpstan-param list<string> $denominators2
*/
private function multiplyUnits($value, array $numerators1, array $denominators1, array $numerators2, array $denominators2)
{
$newNumerators = array();
foreach ($numerators1 as $numerator) {
foreach ($denominators2 as $key => $denominator) {
$conversionFactor = self::getConversionFactor($numerator, $denominator);
if (\is_null($conversionFactor)) {
continue;
}
$value /= $conversionFactor;
unset($denominators2[$key]);
continue 2;
}
$newNumerators[] = $numerator;
}
foreach ($numerators2 as $numerator) {
foreach ($denominators1 as $key => $denominator) {
$conversionFactor = self::getConversionFactor($numerator, $denominator);
if (\is_null($conversionFactor)) {
continue;
}
$value /= $conversionFactor;
unset($denominators1[$key]);
continue 2;
}
$newNumerators[] = $numerator;
}
$newDenominators = array_values(array_merge($denominators1, $denominators2));
return new Number($value, $newNumerators, $newDenominators);
}
/**
* Returns the number of [unit1]s per [unit2].
*
* Equivalently, `1unit1 * conversionFactor(unit1, unit2) = 1unit2`.
*
* @param string $unit1
* @param string $unit2
*
* @return float|int|null
*/
private static function getConversionFactor($unit1, $unit2)
{
if ($unit1 === $unit2) {
return 1;
}
foreach (static::$unitTable as $unitVariants) {
if (isset($unitVariants[$unit1]) && isset($unitVariants[$unit2])) {
return $unitVariants[$unit1] / $unitVariants[$unit2];
}
}
return null;
}
/**
* Returns unit(s) as the product of numerator units divided by the product of denominator units
*
* @param string[] $numerators
* @param string[] $denominators
*
* @phpstan-param list<string> $numerators
* @phpstan-param list<string> $denominators
*
* @return string
*/
private static function getUnitString(array $numerators, array $denominators)
{
if (!\count($numerators)) {
if (\count($denominators) === 0) {
return 'no units';
}
if (\count($denominators) === 1) {
return $denominators[0] . '^-1';
}
return '(' . implode('*', $denominators) . ')^-1';
}
return implode('*', $numerators) . (\count($denominators) ? '/' . implode('*', $denominators) : '');
}
}

View file

@ -0,0 +1,9 @@
<?php
namespace ScssPhp\ScssPhp;
final class OutputStyle
{
const EXPANDED = 'expanded';
const COMPRESSED = 'compressed';
}

File diff suppressed because it is too large Load diff

View file

@ -16,11 +16,13 @@ namespace ScssPhp\ScssPhp\SourceMap;
* Base 64 Encode/Decode
*
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @internal
*/
class Base64
{
/**
* @var array
* @var array<int, string>
*/
private static $encodingMap = [
0 => 'A',
@ -90,7 +92,7 @@ class Base64
];
/**
* @var array
* @var array<string|int, int>
*/
private static $decodingMap = [
'A' => 0,
@ -162,7 +164,7 @@ class Base64
/**
* Convert to base64
*
* @param integer $value
* @param int $value
*
* @return string
*/
@ -176,7 +178,7 @@ class Base64
*
* @param string $value
*
* @return integer
* @return int
*/
public static function decode($value)
{

View file

@ -12,8 +12,6 @@
namespace ScssPhp\ScssPhp\SourceMap;
use ScssPhp\ScssPhp\SourceMap\Base64;
/**
* Base 64 VLQ
*
@ -36,6 +34,8 @@ use ScssPhp\ScssPhp\SourceMap\Base64;
*
* @author John Lenz <johnlenz@google.com>
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @internal
*/
class Base64VLQ
{
@ -51,7 +51,7 @@ class Base64VLQ
/**
* Returns the VLQ encoded value.
*
* @param integer $value
* @param int $value
*
* @return string
*/
@ -80,9 +80,9 @@ class Base64VLQ
* Decodes VLQValue.
*
* @param string $str
* @param integer $index
* @param int $index
*
* @return integer
* @return int
*/
public static function decode($str, &$index)
{
@ -107,9 +107,9 @@ class Base64VLQ
* 1 becomes 2 (10 binary), -1 becomes 3 (11 binary)
* 2 becomes 4 (100 binary), -2 becomes 5 (101 binary)
*
* @param integer $value
* @param int $value
*
* @return integer
* @return int
*/
private static function toVLQSigned($value)
{
@ -126,9 +126,9 @@ class Base64VLQ
* 2 (10 binary) becomes 1, 3 (11 binary) becomes -1
* 4 (100 binary) becomes 2, 5 (101 binary) becomes -2
*
* @param integer $value
* @param int $value
*
* @return integer
* @return int
*/
private static function fromVLQSigned($value)
{

View file

@ -21,6 +21,8 @@ use ScssPhp\ScssPhp\Exception\CompilerException;
*
* @author Josh Schmidt <oyejorge@gmail.com>
* @author Nicolas FRANÇOIS <nicolas.francois@frog-labs.com>
*
* @internal
*/
class SourceMapGenerator
{
@ -33,6 +35,7 @@ class SourceMapGenerator
* Array of default options
*
* @var array
* @phpstan-var array{sourceRoot: string, sourceMapFilename: string|null, sourceMapURL: string|null, sourceMapWriteTo: string|null, outputSourceFiles: bool, sourceMapRootpath: string, sourceMapBasepath: string}
*/
protected $defaultOptions = [
// an optional source root, useful for relocating source files
@ -70,6 +73,7 @@ class SourceMapGenerator
* Array of mappings
*
* @var array
* @phpstan-var list<array{generated_line: int, generated_column: int, original_line: int, original_column: int, source_file: string}>
*/
protected $mappings = [];
@ -83,30 +87,40 @@ class SourceMapGenerator
/**
* File to content map
*
* @var array
* @var array<string, string>
*/
protected $sources = [];
/**
* @var array<string, int>
*/
protected $sourceKeys = [];
/**
* @var array
* @phpstan-var array{sourceRoot: string, sourceMapFilename: string|null, sourceMapURL: string|null, sourceMapWriteTo: string|null, outputSourceFiles: bool, sourceMapRootpath: string, sourceMapBasepath: string}
*/
private $options;
/**
* @phpstan-param array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} $options
*/
public function __construct(array $options = [])
{
$this->options = array_merge($this->defaultOptions, $options);
$this->options = array_replace($this->defaultOptions, $options);
$this->encoder = new Base64VLQ();
}
/**
* Adds a mapping
*
* @param integer $generatedLine The line number in generated file
* @param integer $generatedColumn The column number in generated file
* @param integer $originalLine The line number in original file
* @param integer $originalColumn The column number in original file
* @param string $sourceFile The original source file
* @param int $generatedLine The line number in generated file
* @param int $generatedColumn The column number in generated file
* @param int $originalLine The line number in original file
* @param int $originalColumn The column number in original file
* @param string $sourceFile The original source file
*
* @return void
*/
public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $sourceFile)
{
@ -126,13 +140,15 @@ class SourceMapGenerator
*
* @param string $content The content to write
*
* @return string
* @return string|null
*
* @throws \ScssPhp\ScssPhp\Exception\CompilerException If the file could not be saved
* @deprecated
*/
public function saveMap($content)
{
$file = $this->options['sourceMapWriteTo'];
assert($file !== null);
$dir = \dirname($file);
// directory does not exist
@ -154,14 +170,16 @@ class SourceMapGenerator
/**
* Generates the JSON source map
*
* @param string $prefix A prefix added in the output file, which needs to shift mappings
*
* @return string
*
* @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#
*/
public function generateJson()
public function generateJson($prefix = '')
{
$sourceMap = [];
$mappings = $this->generateMappings();
$mappings = $this->generateMappings($prefix);
// File version (always the first entry in the object) and must be a positive integer.
$sourceMap['version'] = self::VERSION;
@ -184,7 +202,7 @@ class SourceMapGenerator
// A list of original sources used by the 'mappings' entry.
$sourceMap['sources'] = [];
foreach ($this->sources as $sourceUri => $sourceFilename) {
foreach ($this->sources as $sourceFilename) {
$sourceMap['sources'][] = $this->normalizeFilename($sourceFilename);
}
@ -206,13 +224,21 @@ class SourceMapGenerator
unset($sourceMap['sourceRoot']);
}
return json_encode($sourceMap, JSON_UNESCAPED_SLASHES);
$jsonSourceMap = json_encode($sourceMap, JSON_UNESCAPED_SLASHES);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException(json_last_error_msg());
}
assert($jsonSourceMap !== false);
return $jsonSourceMap;
}
/**
* Returns the sources contents
*
* @return array|null
* @return string[]|null
*/
protected function getSourcesContent()
{
@ -232,14 +258,21 @@ class SourceMapGenerator
/**
* Generates the mappings string
*
* @param string $prefix A prefix added in the output file, which needs to shift mappings
*
* @return string
*/
public function generateMappings()
public function generateMappings($prefix = '')
{
if (! \count($this->mappings)) {
return '';
}
$prefixLines = substr_count($prefix, "\n");
$lastPrefixNewLine = strrpos($prefix, "\n");
$lastPrefixLineStart = false === $lastPrefixNewLine ? 0 : $lastPrefixNewLine + 1;
$prefixColumn = strlen($prefix) - $lastPrefixLineStart;
$this->sourceKeys = array_flip(array_keys($this->sources));
// group mappings by generated line number.
@ -254,6 +287,12 @@ class SourceMapGenerator
$lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
foreach ($groupedMap as $lineNumber => $lineMap) {
if ($lineNumber > 1) {
// The prefix only impacts the column for the first line of the original output
$prefixColumn = 0;
}
$lineNumber += $prefixLines;
while (++$lastGeneratedLine < $lineNumber) {
$groupedMapEncoded[] = ';';
}
@ -262,8 +301,10 @@ class SourceMapGenerator
$lastGeneratedColumn = 0;
foreach ($lineMap as $m) {
$mapEncoded = $this->encoder->encode($m['generated_column'] - $lastGeneratedColumn);
$lastGeneratedColumn = $m['generated_column'];
$generatedColumn = $m['generated_column'] + $prefixColumn;
$mapEncoded = $this->encoder->encode($generatedColumn - $lastGeneratedColumn);
$lastGeneratedColumn = $generatedColumn;
// find the index
if ($m['source_file']) {
@ -294,7 +335,7 @@ class SourceMapGenerator
*
* @param string $filename
*
* @return integer|false
* @return int|false
*/
protected function findFileIndex($filename)
{
@ -330,8 +371,8 @@ class SourceMapGenerator
/**
* Fix windows paths
*
* @param string $path
* @param boolean $addEndSlash
* @param string $path
* @param bool $addEndSlash
*
* @return string
*/

View file

@ -19,54 +19,190 @@ namespace ScssPhp\ScssPhp;
*/
class Type
{
/**
* @internal
*/
const T_ASSIGN = 'assign';
/**
* @internal
*/
const T_AT_ROOT = 'at-root';
/**
* @internal
*/
const T_BLOCK = 'block';
/**
* @deprecated
* @internal
*/
const T_BREAK = 'break';
/**
* @internal
*/
const T_CHARSET = 'charset';
const T_COLOR = 'color';
/**
* @internal
*/
const T_COMMENT = 'comment';
/**
* @deprecated
* @internal
*/
const T_CONTINUE = 'continue';
/**
* @deprecated
* @internal
*/
const T_CONTROL = 'control';
/**
* @internal
*/
const T_CUSTOM_PROPERTY = 'custom';
/**
* @internal
*/
const T_DEBUG = 'debug';
/**
* @internal
*/
const T_DIRECTIVE = 'directive';
/**
* @internal
*/
const T_EACH = 'each';
/**
* @internal
*/
const T_ELSE = 'else';
/**
* @internal
*/
const T_ELSEIF = 'elseif';
/**
* @internal
*/
const T_ERROR = 'error';
/**
* @internal
*/
const T_EXPRESSION = 'exp';
/**
* @internal
*/
const T_EXTEND = 'extend';
/**
* @internal
*/
const T_FOR = 'for';
const T_FUNCTION = 'function';
/**
* @internal
*/
const T_FUNCTION_REFERENCE = 'function-reference';
/**
* @internal
*/
const T_FUNCTION_CALL = 'fncall';
/**
* @internal
*/
const T_HSL = 'hsl';
/**
* @internal
*/
const T_HWB = 'hwb';
/**
* @internal
*/
const T_IF = 'if';
/**
* @internal
*/
const T_IMPORT = 'import';
/**
* @internal
*/
const T_INCLUDE = 'include';
/**
* @internal
*/
const T_INTERPOLATE = 'interpolate';
/**
* @internal
*/
const T_INTERPOLATED = 'interpolated';
/**
* @internal
*/
const T_KEYWORD = 'keyword';
const T_LIST = 'list';
const T_MAP = 'map';
/**
* @internal
*/
const T_MEDIA = 'media';
/**
* @internal
*/
const T_MEDIA_EXPRESSION = 'mediaExp';
/**
* @internal
*/
const T_MEDIA_TYPE = 'mediaType';
/**
* @internal
*/
const T_MEDIA_VALUE = 'mediaValue';
/**
* @internal
*/
const T_MIXIN = 'mixin';
/**
* @internal
*/
const T_MIXIN_CONTENT = 'mixin_content';
/**
* @internal
*/
const T_NESTED_PROPERTY = 'nestedprop';
/**
* @internal
*/
const T_NOT = 'not';
const T_NULL = 'null';
const T_NUMBER = 'number';
/**
* @internal
*/
const T_RETURN = 'return';
/**
* @internal
*/
const T_ROOT = 'root';
/**
* @internal
*/
const T_SCSSPHP_IMPORT_ONCE = 'scssphp-import-once';
/**
* @internal
*/
const T_SELF = 'self';
const T_STRING = 'string';
/**
* @internal
*/
const T_UNARY = 'unary';
/**
* @internal
*/
const T_VARIABLE = 'var';
/**
* @internal
*/
const T_WARN = 'warn';
/**
* @internal
*/
const T_WHILE = 'while';
}

View file

@ -14,11 +14,14 @@ namespace ScssPhp\ScssPhp;
use ScssPhp\ScssPhp\Base\Range;
use ScssPhp\ScssPhp\Exception\RangeException;
use ScssPhp\ScssPhp\Node\Number;
/**
* Utilty functions
* Utility functions
*
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @internal
*/
class Util
{
@ -26,10 +29,10 @@ class Util
* Asserts that `value` falls within `range` (inclusive), leaving
* room for slight floating-point errors.
*
* @param string $name The name of the value. Used in the error message.
* @param \ScssPhp\ScssPhp\Base\Range $range Range of values.
* @param array $value The value to check.
* @param string $unit The unit of the value. Used in error reporting.
* @param string $name The name of the value. Used in the error message.
* @param Range $range Range of values.
* @param array|Number $value The value to check.
* @param string $unit The unit of the value. Used in error reporting.
*
* @return mixed `value` adjusted to fall within range, if it was outside by a floating-point margin.
*
@ -76,14 +79,14 @@ class Util
/**
* mb_chr() wrapper
*
* @param integer $code
* @param int $code
*
* @return string
*/
public static function mbChr($code)
{
// Use the native implementation if available.
if (\function_exists('mb_chr')) {
// Use the native implementation if available, but not on PHP 7.2 as mb_chr(0) is buggy there
if (\PHP_VERSION_ID > 70300 && \function_exists('mb_chr')) {
return mb_chr($code, 'UTF-8');
}
@ -105,7 +108,7 @@ class Util
* mb_strlen() wrapper
*
* @param string $string
* @return false|int
* @return int
*/
public static function mbStrlen($string)
{
@ -115,10 +118,10 @@ class Util
}
if (\function_exists('iconv_strlen')) {
return @iconv_strlen($string, 'UTF-8');
return (int) @iconv_strlen($string, 'UTF-8');
}
return strlen($string);
throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.');
}
/**
@ -155,6 +158,27 @@ class Util
return (string)iconv_substr($string, $start, $length, 'UTF-8');
}
return substr($string, $start, $length);
throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.');
}
/**
* mb_strpos wrapper
* @param string $haystack
* @param string $needle
* @param int $offset
*
* @return int|false
*/
public static function mbStrpos($haystack, $needle, $offset = 0)
{
if (\function_exists('mb_strpos')) {
return mb_strpos($haystack, $needle, $offset, 'UTF-8');
}
if (\function_exists('iconv_strpos')) {
return iconv_strpos($haystack, $needle, $offset, 'UTF-8');
}
throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.');
}
}

View file

@ -0,0 +1,77 @@
<?php
namespace ScssPhp\ScssPhp\Util;
/**
* @internal
*/
class Path
{
/**
* @param string $path
*
* @return bool
*/
public static function isAbsolute($path)
{
if ($path === '') {
return false;
}
if ($path[0] === '/') {
return true;
}
if (\DIRECTORY_SEPARATOR !== '\\') {
return false;
}
if ($path[0] === '\\') {
return true;
}
if (\strlen($path) < 3) {
return false;
}
if ($path[1] !== ':') {
return false;
}
if ($path[2] !== '/' && $path[2] !== '\\') {
return false;
}
if (!preg_match('/^[A-Za-z]$/', $path[0])) {
return false;
}
return true;
}
/**
* @param string $part1
* @param string $part2
*
* @return string
*/
public static function join($part1, $part2)
{
if ($part1 === '' || self::isAbsolute($part2)) {
return $part2;
}
if ($part2 === '') {
return $part1;
}
$last = $part1[\strlen($part1) - 1];
$separator = \DIRECTORY_SEPARATOR;
if ($last === '/' || $last === \DIRECTORY_SEPARATOR) {
$separator = '';
}
return $part1 . $separator . $part2;
}
}

View file

@ -0,0 +1,95 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp;
use ScssPhp\ScssPhp\Node\Number;
final class ValueConverter
{
// Prevent instantiating it
private function __construct()
{
}
/**
* Parses a value from a Scss source string.
*
* The returned value is guaranteed to be supported by the
* Compiler methods for registering custom variables. No other
* guarantee about it is provided. It should be considered
* opaque values by the caller.
*
* @param string $source
*
* @return mixed
*/
public static function parseValue($source)
{
$parser = new Parser(__CLASS__);
if (!$parser->parseValue($source, $value)) {
throw new \InvalidArgumentException(sprintf('Invalid value source "%s".', $source));
}
return $value;
}
/**
* Converts a PHP value to a Sass value
*
* The returned value is guaranteed to be supported by the
* Compiler methods for registering custom variables. No other
* guarantee about it is provided. It should be considered
* opaque values by the caller.
*
* @param mixed $value
*
* @return mixed
*/
public static function fromPhp($value)
{
if ($value instanceof Number) {
return $value;
}
if (is_array($value) && isset($value[0]) && \in_array($value[0], [Type::T_NULL, Type::T_COLOR, Type::T_KEYWORD, Type::T_LIST, Type::T_MAP, Type::T_STRING])) {
return $value;
}
if ($value === null) {
return Compiler::$null;
}
if ($value === true) {
return Compiler::$true;
}
if ($value === false) {
return Compiler::$false;
}
if ($value === '') {
return Compiler::$emptyString;
}
if (\is_int($value) || \is_float($value)) {
return new Number($value, '');
}
if (\is_string($value)) {
return [Type::T_STRING, '"', [$value]];
}
throw new \InvalidArgumentException(sprintf('Cannot convert the value of type "%s" to a Sass value.', gettype($value)));
}
}

View file

@ -19,5 +19,5 @@ namespace ScssPhp\ScssPhp;
*/
class Version
{
const VERSION = '1.2.1';
const VERSION = '1.11.0';
}

View file

@ -0,0 +1,84 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp;
final class Warn
{
/**
* @var callable|null
* @phpstan-var (callable(string, bool): void)|null
*/
private static $callback;
/**
* Prints a warning message associated with the current `@import` or function call.
*
* This may only be called within a custom function or importer callback.
*
* @param string $message
*
* @return void
*/
public static function warning($message)
{
self::reportWarning($message, false);
}
/**
* Prints a deprecation warning message associated with the current `@import` or function call.
*
* This may only be called within a custom function or importer callback.
*
* @param string $message
*
* @return void
*/
public static function deprecation($message)
{
self::reportWarning($message, true);
}
/**
* @param callable|null $callback
*
* @return callable|null The previous warn callback
*
* @phpstan-param (callable(string, bool): void)|null $callback
*
* @phpstan-return (callable(string, bool): void)|null
*
* @internal
*/
public static function setCallback(callable $callback = null)
{
$previousCallback = self::$callback;
self::$callback = $callback;
return $previousCallback;
}
/**
* @param string $message
* @param bool $deprecation
*
* @return void
*/
private static function reportWarning($message, $deprecation)
{
if (self::$callback === null) {
throw new \BadMethodCallException('The warning Reporter may only be called within a custom function or importer callback.');
}
\call_user_func(self::$callback, $message, $deprecation);
}
}

View file

@ -0,0 +1,20 @@
Copyright (c) 2015 Leaf Corcoran, http://scssphp.github.io/scssphp
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,71 @@
# scssphp
### <https://scssphp.github.io/scssphp>
![Build](https://github.com/scssphp/scssphp/workflows/CI/badge.svg)
[![License](https://poser.pugx.org/scssphp/scssphp/license)](https://packagist.org/packages/scssphp/scssphp)
`scssphp` is a compiler for SCSS written in PHP.
Checkout the homepage, <https://scssphp.github.io/scssphp>, for directions on how to use.
## Running Tests
`scssphp` uses [PHPUnit](https://github.com/sebastianbergmann/phpunit) for testing.
Run the following command from the root directory to run every test:
vendor/bin/phpunit tests
There are several tests in the `tests/` directory:
* `ApiTest.php` contains various unit tests that test the PHP interface.
* `ExceptionTest.php` contains unit tests that test for exceptions thrown by the parser and compiler.
* `FailingTest.php` contains tests reported in Github issues that demonstrate compatibility bugs.
* `InputTest.php` compiles every `.scss` file in the `tests/inputs` directory
then compares to the respective `.css` file in the `tests/outputs` directory.
* `SassSpecTest.php` extracts tests from the `sass/sass-spec` repository.
When changing any of the tests in `tests/inputs`, the tests will most likely
fail because the output has changed. Once you verify that the output is correct
you can run the following command to rebuild all the tests:
BUILD=1 vendor/bin/phpunit tests
This will compile all the tests, and save results into `tests/outputs`. It also
updates the list of excluded specs from sass-spec.
To enable the full `sass-spec` compatibility tests:
TEST_SASS_SPEC=1 vendor/bin/phpunit tests
## Coding Standard
`scssphp` source conforms to [PSR12](https://www.php-fig.org/psr/psr-12/).
Run the following command from the root directory to check the code for "sniffs".
vendor/bin/phpcs --standard=PSR12 --extensions=php bin src tests *.php
## Static Analysis
`scssphp` uses [phpstan](https://phpstan.org/) for static analysis.
Run the following command from the root directory to analyse the codebase:
make phpstan
As most of the codebase is composed of legacy code which cannot be type-checked
fully, the setup contains a baseline file with all errors we want to ignore. In
particular, we ignore all errors related to not specifying the types inside arrays
when these arrays correspond to the representation of Sass values and Sass AST nodes
in the parser and compiler.
When contributing, the proper process to deal with static analysis is the following:
1. Make your change in the codebase
2. Run `make phpstan`
3. Fix errors reported by phpstan when possible
4. Repeat step 2 and 3 until nothing gets fixed anymore at step 3
5. Run `make phpstan-baseline` to regenerate the phpstan baseline
Additions to the baseline will be reviewed to avoid ignoring errors that should have
been fixed.

View file

@ -0,0 +1,117 @@
{
"name": "scssphp/scssphp",
"type": "library",
"description": "scssphp is a compiler for SCSS written in PHP.",
"keywords": ["css", "stylesheet", "scss", "sass", "less"],
"homepage": "http://scssphp.github.io/scssphp/",
"license": [
"MIT"
],
"authors": [
{
"name": "Anthon Pang",
"email": "apang@softwaredevelopment.ca",
"homepage": "https://github.com/robocoder"
},
{
"name": "Cédric Morin",
"email": "cedric@yterium.com",
"homepage": "https://github.com/Cerdic"
}
],
"autoload": {
"psr-4": { "ScssPhp\\ScssPhp\\": "src/" }
},
"autoload-dev": {
"psr-4": { "ScssPhp\\ScssPhp\\Tests\\": "tests/" }
},
"require": {
"php": ">=5.6.0",
"ext-json": "*",
"ext-ctype": "*"
},
"suggest": {
"ext-mbstring": "For best performance, mbstring should be installed as it is faster than ext-iconv",
"ext-iconv": "Can be used as fallback when ext-mbstring is not available"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4",
"phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4",
"sass/sass-spec": "*",
"squizlabs/php_codesniffer": "~3.5",
"symfony/phpunit-bridge": "^5.1",
"thoughtbot/bourbon": "^7.0",
"twbs/bootstrap": "~5.0",
"twbs/bootstrap4": "4.6.1",
"zurb/foundation": "~6.5"
},
"repositories": [
{
"type": "package",
"package": {
"name": "sass/sass-spec",
"version": "2022.08.19",
"source": {
"type": "git",
"url": "https://github.com/sass/sass-spec.git",
"reference": "2bdc199723a3445d5badac3ac774105698f08861"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sass/sass-spec/zipball/2bdc199723a3445d5badac3ac774105698f08861",
"reference": "2bdc199723a3445d5badac3ac774105698f08861",
"shasum": ""
}
}
},
{
"type": "package",
"package": {
"name": "thoughtbot/bourbon",
"version": "v7.0.0",
"source": {
"type": "git",
"url": "https://github.com/thoughtbot/bourbon.git",
"reference": "fbe338ee6807e7f7aa996d82c8a16f248bb149b3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thoughtbot/bourbon/zipball/fbe338ee6807e7f7aa996d82c8a16f248bb149b3",
"reference": "fbe338ee6807e7f7aa996d82c8a16f248bb149b3",
"shasum": ""
}
}
},
{
"type": "package",
"package": {
"name": "twbs/bootstrap4",
"version": "v4.6.1",
"source": {
"type": "git",
"url": "https://github.com/twbs/bootstrap.git",
"reference": "043a03c95a2ad6738f85b65e53b9dbdfb03b8d10"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twbs/bootstrap/zipball/043a03c95a2ad6738f85b65e53b9dbdfb03b8d10",
"reference": "043a03c95a2ad6738f85b65e53b9dbdfb03b8d10",
"shasum": ""
}
}
}
],
"bin": ["bin/pscss"],
"config": {
"sort-packages": true,
"allow-plugins": {
"bamarni/composer-bin-plugin": true
}
},
"extra": {
"bamarni-bin": {
"forward-command": false,
"bin-links": false
}
}
}

View file

@ -0,0 +1,244 @@
#!/usr/bin/env php
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
error_reporting(E_ALL);
if (version_compare(PHP_VERSION, '5.6') < 0) {
die('Requires PHP 5.6 or above');
}
include __DIR__ . '/../scss.inc.php';
use ScssPhp\ScssPhp\Compiler;
use ScssPhp\ScssPhp\Exception\SassException;
use ScssPhp\ScssPhp\OutputStyle;
use ScssPhp\ScssPhp\Parser;
use ScssPhp\ScssPhp\Version;
$style = null;
$loadPaths = [];
$dumpTree = false;
$inputFile = null;
$changeDir = false;
$encoding = false;
$sourceMap = false;
$embedSources = false;
$embedSourceMap = false;
/**
* Parse argument
*
* @param int $i
* @param string[] $options
*
* @return string|null
*/
function parseArgument(&$i, $options) {
global $argc;
global $argv;
if (! preg_match('/^(?:' . implode('|', (array) $options) . ')=?(.*)/', $argv[$i], $matches)) {
return;
}
if (strlen($matches[1])) {
return $matches[1];
}
if ($i + 1 < $argc) {
$i++;
return $argv[$i];
}
}
$arguments = [];
for ($i = 1; $i < $argc; $i++) {
if ($argv[$i] === '-?' || $argv[$i] === '-h' || $argv[$i] === '--help') {
$exe = $argv[0];
$HELP = <<<EOT
Usage: $exe [options] [input-file] [output-file]
Options include:
--help Show this message [-h, -?]
--continue-on-error [deprecated] Ignored
--debug-info [deprecated] Ignored [-g]
--dump-tree [deprecated] Dump formatted parse tree [-T]
--iso8859-1 Use iso8859-1 encoding instead of default utf-8
--line-numbers [deprecated] Ignored [--line-comments]
--load-path=PATH Set import path [-I]
--precision=N [deprecated] Ignored. (default 10) [-p]
--sourcemap Create source map file
--embed-sources Embed source file contents in source maps
--embed-source-map Embed the source map contents in CSS (default if writing to stdout)
--style=FORMAT Set the output style (compressed or expanded) [-s, -t]
--version Print the version [-v]
EOT;
exit($HELP);
}
if ($argv[$i] === '-v' || $argv[$i] === '--version') {
exit(Version::VERSION . "\n");
}
// Keep parsing --continue-on-error to avoid BC breaks for scripts using it
if ($argv[$i] === '--continue-on-error') {
// TODO report it as a warning ?
continue;
}
// Keep parsing it to avoid BC breaks for scripts using it
if ($argv[$i] === '-g' || $argv[$i] === '--debug-info') {
// TODO report it as a warning ?
continue;
}
if ($argv[$i] === '--iso8859-1') {
$encoding = 'iso8859-1';
continue;
}
// Keep parsing it to avoid BC breaks for scripts using it
if ($argv[$i] === '--line-numbers' || $argv[$i] === '--line-comments') {
// TODO report it as a warning ?
continue;
}
if ($argv[$i] === '--sourcemap') {
$sourceMap = true;
continue;
}
if ($argv[$i] === '--embed-sources') {
$embedSources = true;
continue;
}
if ($argv[$i] === '--embed-source-map') {
$embedSourceMap = true;
continue;
}
if ($argv[$i] === '-T' || $argv[$i] === '--dump-tree') {
$dumpTree = true;
continue;
}
$value = parseArgument($i, array('-t', '-s', '--style'));
if (isset($value)) {
$style = $value;
continue;
}
$value = parseArgument($i, array('-I', '--load-path'));
if (isset($value)) {
$loadPaths[] = $value;
continue;
}
// Keep parsing --precision to avoid BC breaks for scripts using it
$value = parseArgument($i, array('-p', '--precision'));
if (isset($value)) {
// TODO report it as a warning ?
continue;
}
$arguments[] = $argv[$i];
}
if (isset($arguments[0]) && file_exists($arguments[0])) {
$inputFile = $arguments[0];
$data = file_get_contents($inputFile);
} else {
$data = '';
while (! feof(STDIN)) {
$data .= fread(STDIN, 8192);
}
}
if ($dumpTree) {
$parser = new Parser($inputFile);
print_r(json_decode(json_encode($parser->parse($data)), true));
fwrite(STDERR, 'Warning: the --dump-tree option is deprecated. Use proper debugging tools instead.');
exit();
}
$scss = new Compiler();
if ($loadPaths) {
$scss->setImportPaths($loadPaths);
}
if ($style) {
if ($style === OutputStyle::COMPRESSED || $style === OutputStyle::EXPANDED) {
$scss->setOutputStyle($style);
} else {
fwrite(STDERR, "WARNING: the $style style is deprecated.\n");
$scss->setFormatter('ScssPhp\\ScssPhp\\Formatter\\' . ucfirst($style));
}
}
$outputFile = isset($arguments[1]) ? $arguments[1] : null;
$sourceMapFile = null;
if ($sourceMap) {
$sourceMapOptions = array(
'outputSourceFiles' => $embedSources,
);
if ($embedSourceMap || $outputFile === null) {
$scss->setSourceMap(Compiler::SOURCE_MAP_INLINE);
} else {
$sourceMapFile = $outputFile . '.map';
$sourceMapOptions['sourceMapWriteTo'] = $sourceMapFile;
$sourceMapOptions['sourceMapURL'] = basename($sourceMapFile);
$sourceMapOptions['sourceMapBasepath'] = getcwd();
$sourceMapOptions['sourceMapFilename'] = basename($outputFile);
$scss->setSourceMap(Compiler::SOURCE_MAP_FILE);
}
$scss->setSourceMapOptions($sourceMapOptions);
}
if ($encoding) {
$scss->setEncoding($encoding);
}
try {
$result = $scss->compileString($data, $inputFile);
} catch (SassException $e) {
fwrite(STDERR, 'Error: '.$e->getMessage()."\n");
exit(1);
}
if ($outputFile) {
file_put_contents($outputFile, $result->getCss());
if ($sourceMapFile !== null && $result->getSourceMap() !== null) {
file_put_contents($sourceMapFile, $result->getSourceMap());
}
} else {
echo $result->getCss();
}

View file

@ -0,0 +1,21 @@
<?php
if (version_compare(PHP_VERSION, '5.6') < 0) {
throw new \Exception('scssphp requires PHP 5.6 or above');
}
if (! class_exists('ScssPhp\ScssPhp\Version')) {
spl_autoload_register(function ($class) {
if (0 !== strpos($class, 'ScssPhp\ScssPhp\\')) {
// Not a ScssPhp class
return;
}
$subClass = substr($class, strlen('ScssPhp\ScssPhp\\'));
$path = __DIR__ . '/src/' . str_replace('\\', '/', $subClass) . '.php';
if (file_exists($path)) {
require $path;
}
});
}