Dep: update

주로 PHAN
This commit is contained in:
2021-08-06 22:39:09 +09:00
parent 5310c2e7f6
commit b306601c72
1098 changed files with 92137 additions and 33228 deletions
+34 -10
View File
@@ -1,12 +1,12 @@
# scssphp
### <http://scssphp.github.io/scssphp>
### <https://scssphp.github.io/scssphp>
[![Build](https://travis-ci.org/scssphp/scssphp.svg?branch=master)](http://travis-ci.org/scssphp/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, <http://scssphp.github.io/scssphp>, for directions on how to use.
Checkout the homepage, <https://scssphp.github.io/scssphp>, for directions on how to use.
## Running Tests
@@ -23,8 +23,7 @@ There are several tests in the `tests/` directory:
* `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.
* `ScssTest.php` extracts (ruby) `scss` tests from the `tests/scss_test.rb` file.
* `ServerTest.php` contains functional tests for the `Server` class.
* `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
@@ -32,16 +31,41 @@ 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`.
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 `scss` compatibility tests:
To enable the full `sass-spec` compatibility tests:
TEST_SCSS_COMPAT=1 vendor/bin/phpunit tests
TEST_SASS_SPEC=1 vendor/bin/phpunit tests
## Coding Standard
`scssphp` source conforms to [PSR2](http://www.php-fig.org/psr/psr-2/).
`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=PSR2 bin src tests
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.
Vendored Regular → Executable
+79 -50
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env php
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -19,20 +20,20 @@ if (version_compare(PHP_VERSION, '5.6') < 0) {
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 = null;
$precision = null;
$dumpTree = false;
$inputFile = null;
$changeDir = false;
$debugInfo = false;
$lineNumbers = false;
$ignoreErrors = false;
$encoding = false;
$sourceMap = false;
$embedSources = false;
$embedSourceMap = false;
/**
* Parse argument
@@ -61,25 +62,29 @@ function parseArgument(&$i, $options) {
}
}
$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]
Usage: $exe [options] [input-file] [output-file]
Options include:
--help Show this message [-h, -?]
--continue-on-error Continue compilation (as best as possible) when error encountered
--debug-info Annotate selectors with CSS referring to the source file and line number [-g]
--dump-tree Dump formatted parse tree [-T]
--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 Annotate selectors with comments referring to the source file and line number [--line-comments]
--line-numbers [deprecated] Ignored [--line-comments]
--load-path=PATH Set import path [-I]
--precision=N Set decimal number precision (default 10) [-p]
--precision=N [deprecated] Ignored. (default 10) [-p]
--sourcemap Create source map file
--style=FORMAT Set the output format (compact, compressed, crunched, expanded, or nested) [-s, -t]
--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;
@@ -90,13 +95,15 @@ EOT;
exit(Version::VERSION . "\n");
}
// Keep parsing --continue-on-error to avoid BC breaks for scripts using it
if ($argv[$i] === '--continue-on-error') {
$ignoreErrors = true;
// 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') {
$debugInfo = true;
// TODO report it as a warning ?
continue;
}
@@ -105,8 +112,9 @@ EOT;
continue;
}
// Keep parsing it to avoid BC breaks for scripts using it
if ($argv[$i] === '--line-numbers' || $argv[$i] === '--line-comments') {
$lineNumbers = true;
// TODO report it as a warning ?
continue;
}
@@ -115,6 +123,16 @@ EOT;
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;
@@ -134,30 +152,21 @@ EOT;
continue;
}
// Keep parsing --precision to avoid BC breaks for scripts using it
$value = parseArgument($i, array('-p', '--precision'));
if (isset($value)) {
$precision = $value;
// TODO report it as a warning ?
continue;
}
if (file_exists($argv[$i])) {
$inputFile = $argv[$i];
continue;
}
$arguments[] = $argv[$i];
}
if ($inputFile) {
if (isset($arguments[0]) && file_exists($arguments[0])) {
$inputFile = $arguments[0];
$data = file_get_contents($inputFile);
$newWorkingDir = dirname(realpath($inputFile));
$oldWorkingDir = getcwd();
if ($oldWorkingDir !== $newWorkingDir) {
$changeDir = chdir($newWorkingDir);
$inputFile = basename($inputFile);
}
} else {
$data = '';
@@ -171,45 +180,65 @@ if ($dumpTree) {
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 ($debugInfo) {
$scss->setLineNumberStyle(Compiler::DEBUG_INFO);
}
if ($lineNumbers) {
$scss->setLineNumberStyle(Compiler::LINE_COMMENTS);
}
if ($ignoreErrors) {
$scss->setIgnoreErrors($ignoreErrors);
}
if ($loadPaths) {
$scss->setImportPaths(explode(PATH_SEPARATOR, $loadPaths));
}
if ($precision) {
$scss->setNumberPrecision($precision);
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));
}
}
if ($style) {
$scss->setFormatter('ScssPhp\\ScssPhp\\Formatter\\' . ucfirst($style));
}
$outputFile = isset($arguments[1]) ? $arguments[1] : null;
$sourceMapFile = null;
if ($sourceMap) {
$scss->setSourceMap(Compiler::SOURCE_MAP_INLINE);
$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);
}
echo $scss->compile($data, $inputFile);
try {
$result = $scss->compileString($data, $inputFile);
} catch (SassException $e) {
fwrite(STDERR, 'Error: '.$e->getMessage()."\n");
exit(1);
}
if ($changeDir) {
chdir($oldWorkingDir);
if ($outputFile) {
file_put_contents($outputFile, $result->getCss());
if ($sourceMapFile !== null && $result->getSourceMap() !== null) {
file_put_contents($sourceMapFile, $result->getSourceMap());
}
} else {
echo $result->getCss();
}
+51 -13
View File
@@ -30,22 +30,60 @@
"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": {
"squizlabs/php_codesniffer": "~2.5",
"phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3",
"twbs/bootstrap": "~4.3",
"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",
"twbs/bootstrap": "~5.0",
"twbs/bootstrap4": "4.6.0",
"zurb/foundation": "~6.5"
},
"minimum-stability": "dev",
"repositories": [
{
"type": "package",
"package": {
"name": "sass/sass-spec",
"version": "2021.06.30",
"source": {
"type": "git",
"url": "https://github.com/sass/sass-spec.git",
"reference": "e348959657f1e274cef658283436a311a925a673"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sass/sass-spec/zipball/e348959657f1e274cef658283436a311a925a673",
"reference": "e348959657f1e274cef658283436a311a925a673",
"shasum": ""
}
}
},
{
"type": "package",
"package": {
"name": "twbs/bootstrap4",
"version": "v4.6.0",
"source": {
"type": "git",
"url": "https://github.com/twbs/bootstrap.git",
"reference": "6ffb0b48e455430f8a5359ed689ad64c1143fac2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twbs/bootstrap/zipball/6ffb0b48e455430f8a5359ed689ad64c1143fac2",
"reference": "6ffb0b48e455430f8a5359ed689ad64c1143fac2",
"shasum": ""
}
}
}
],
"bin": ["bin/pscss"],
"archive": {
"exclude": [
"/Makefile",
"/.gitattributes",
"/.gitignore",
"/.travis.yml",
"/phpunit.xml.dist",
"/tests"
]
"config": {
"sort-packages": true
}
}
-523
View File
@@ -1,523 +0,0 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp;
use ScssPhp\ScssPhp\Compiler;
use ScssPhp\ScssPhp\Exception\ServerException;
use ScssPhp\ScssPhp\Version;
/**
* Server
*
* @author Leaf Corcoran <leafot@gmail.com>
*/
class Server
{
/**
* @var boolean
*/
private $showErrorsAsCSS;
/**
* @var string
*/
private $dir;
/**
* @var string
*/
private $cacheDir;
/**
* @var \ScssPhp\ScssPhp\Compiler
*/
private $scss;
/**
* Join path components
*
* @param string $left Path component, left of the directory separator
* @param string $right Path component, right of the directory separator
*
* @return string
*/
protected function join($left, $right)
{
return rtrim($left, '/\\') . DIRECTORY_SEPARATOR . ltrim($right, '/\\');
}
/**
* Get name of requested .scss file
*
* @return string|null
*/
protected function inputName()
{
if (isset($_GET['p'])) {
return $_GET['p'];
}
if (isset($_SERVER['PATH_INFO'])) {
return $_SERVER['PATH_INFO'];
}
if (isset($_SERVER['DOCUMENT_URI'])) {
return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME']));
}
}
/**
* Get path to requested .scss file
*
* @return string
*/
protected function findInput()
{
if (($input = $this->inputName())
&& strpos($input, '..') === false
&& substr($input, -5) === '.scss'
) {
$name = $this->join($this->dir, $input);
if (is_file($name) && is_readable($name)) {
return $name;
}
}
return false;
}
/**
* Get path to cached .css file
*
* @param string $fname
*
* @return string
*/
protected function cacheName($fname)
{
return $this->join($this->cacheDir, md5($fname) . '.css');
}
/**
* Get path to meta data
*
* @param string $out
*
* @return string
*/
protected function metadataName($out)
{
return $out . '.meta';
}
/**
* Determine whether .scss file needs to be re-compiled.
*
* @param string $out Output path
* @param string $etag ETag
*
* @return boolean True if compile required.
*/
protected function needsCompile($out, &$etag)
{
if (! is_file($out)) {
return true;
}
$mtime = filemtime($out);
$metadataName = $this->metadataName($out);
if (is_readable($metadataName)) {
$metadata = unserialize(file_get_contents($metadataName));
foreach ($metadata['imports'] as $import => $originalMtime) {
$currentMtime = filemtime($import);
if ($currentMtime !== $originalMtime || $currentMtime > $mtime) {
return true;
}
}
$metaVars = crc32(serialize($this->scss->getVariables()));
if ($metaVars !== $metadata['vars']) {
return true;
}
$etag = $metadata['etag'];
return false;
}
return true;
}
/**
* Get If-Modified-Since header from client request
*
* @return string|null
*/
protected function getIfModifiedSinceHeader()
{
$modifiedSince = null;
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
if (false !== ($semicolonPos = strpos($modifiedSince, ';'))) {
$modifiedSince = substr($modifiedSince, 0, $semicolonPos);
}
}
return $modifiedSince;
}
/**
* Get If-None-Match header from client request
*
* @return string|null
*/
protected function getIfNoneMatchHeader()
{
$noneMatch = null;
if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
$noneMatch = $_SERVER['HTTP_IF_NONE_MATCH'];
}
return $noneMatch;
}
/**
* Compile .scss file
*
* @param string $in Input path (.scss)
* @param string $out Output path (.css)
*
* @return array
*/
protected function compile($in, $out)
{
$start = microtime(true);
$css = $this->scss->compile(file_get_contents($in), $in);
$elapsed = round((microtime(true) - $start), 4);
$v = Version::VERSION;
$t = gmdate('r');
$css = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css;
$etag = md5($css);
file_put_contents($out, $css);
file_put_contents(
$this->metadataName($out),
serialize([
'etag' => $etag,
'imports' => $this->scss->getParsedFiles(),
'vars' => crc32(serialize($this->scss->getVariables())),
])
);
return [$css, $etag];
}
/**
* Format error as a pseudo-element in CSS
*
* @param \Exception $error
*
* @return string
*/
protected function createErrorCSS(\Exception $error)
{
$message = str_replace(
["'", "\n"],
["\\'", "\\A"],
$error->getfile() . ":\n\n" . $error->getMessage()
);
return "body { display: none !important; }
html:after {
background: white;
color: black;
content: '$message';
display: block !important;
font-family: mono;
padding: 1em;
white-space: pre;
}";
}
/**
* Render errors as a pseudo-element within valid CSS, displaying the errors on any
* page that includes this CSS.
*
* @param boolean $show
*/
public function showErrorsAsCSS($show = true)
{
$this->showErrorsAsCSS = $show;
}
/**
* Compile .scss file
*
* @param string $in Input file (.scss)
* @param string $out Output file (.css) optional
*
* @return string|bool
*
* @throws \ScssPhp\ScssPhp\Exception\ServerException
*/
public function compileFile($in, $out = null)
{
if (! is_readable($in)) {
throw new ServerException('load error: failed to find ' . $in);
}
$pi = pathinfo($in);
$this->scss->addImportPath($pi['dirname'] . '/');
$compiled = $this->scss->compile(file_get_contents($in), $in);
if (is_null($out)) {
return $compiled;
}
return file_put_contents($out, $compiled);
}
/**
* Check if file need compiling
*
* @param string $in Input file (.scss)
* @param string $out Output file (.css)
*
* @return bool
*/
public function checkedCompile($in, $out)
{
if (! is_file($out) || filemtime($in) > filemtime($out)) {
$this->compileFile($in, $out);
return true;
}
return false;
}
/**
* Compile requested scss and serve css. Outputs HTTP response.
*
* @param string $salt Prefix a string to the filename for creating the cache name hash
*/
public function serve($salt = '')
{
$protocol = isset($_SERVER['SERVER_PROTOCOL'])
? $_SERVER['SERVER_PROTOCOL']
: 'HTTP/1.0';
if ($input = $this->findInput()) {
$output = $this->cacheName($salt . $input);
$etag = $noneMatch = trim($this->getIfNoneMatchHeader(), '"');
if ($this->needsCompile($output, $etag)) {
try {
list($css, $etag) = $this->compile($input, $output);
$lastModified = gmdate('r', filemtime($output));
header('Last-Modified: ' . $lastModified);
header('Content-type: text/css');
header('ETag: "' . $etag . '"');
echo $css;
} catch (\Exception $e) {
if ($this->showErrorsAsCSS) {
header('Content-type: text/css');
echo $this->createErrorCSS($e);
} else {
header($protocol . ' 500 Internal Server Error');
header('Content-type: text/plain');
echo 'Parse error: ' . $e->getMessage() . "\n";
}
}
return;
}
header('X-SCSS-Cache: true');
header('Content-type: text/css');
header('ETag: "' . $etag . '"');
if ($etag === $noneMatch) {
header($protocol . ' 304 Not Modified');
return;
}
$modifiedSince = $this->getIfModifiedSinceHeader();
$mtime = filemtime($output);
if (strtotime($modifiedSince) === $mtime) {
header($protocol . ' 304 Not Modified');
return;
}
$lastModified = gmdate('r', $mtime);
header('Last-Modified: ' . $lastModified);
echo file_get_contents($output);
return;
}
header($protocol . ' 404 Not Found');
header('Content-type: text/plain');
$v = Version::VERSION;
echo "/* INPUT NOT FOUND scss $v */\n";
}
/**
* Based on explicit input/output files does a full change check on cache before compiling.
*
* @param string $in
* @param string $out
* @param boolean $force
*
* @return string Compiled CSS results
*
* @throws \ScssPhp\ScssPhp\Exception\ServerException
*/
public function checkedCachedCompile($in, $out, $force = false)
{
if (! is_file($in) || ! is_readable($in)) {
throw new ServerException('Invalid or unreadable input file specified.');
}
if (is_dir($out) || ! is_writable(file_exists($out) ? $out : dirname($out))) {
throw new ServerException('Invalid or unwritable output file specified.');
}
if ($force || $this->needsCompile($out, $etag)) {
list($css, $etag) = $this->compile($in, $out);
} else {
$css = file_get_contents($out);
}
return $css;
}
/**
* Execute scssphp on a .scss file or a scssphp cache structure
*
* The scssphp cache structure contains information about a specific
* scss file having been parsed. It can be used as a hint for future
* calls to determine whether or not a rebuild is required.
*
* The cache structure contains two important keys that may be used
* externally:
*
* compiled: The final compiled CSS
* updated: The time (in seconds) the CSS was last compiled
*
* The cache structure is a plain-ol' PHP associative array and can
* be serialized and unserialized without a hitch.
*
* @param mixed $in Input
* @param boolean $force Force rebuild?
*
* @return array scssphp cache structure
*/
public function cachedCompile($in, $force = false)
{
// assume no root
$root = null;
if (is_string($in)) {
$root = $in;
} elseif (is_array($in) and isset($in['root'])) {
if ($force or ! isset($in['files'])) {
// If we are forcing a recompile or if for some reason the
// structure does not contain any file information we should
// specify the root to trigger a rebuild.
$root = $in['root'];
} elseif (isset($in['files']) and is_array($in['files'])) {
foreach ($in['files'] as $fname => $ftime) {
if (! file_exists($fname) or filemtime($fname) > $ftime) {
// One of the files we knew about previously has changed
// so we should look at our incoming root again.
$root = $in['root'];
break;
}
}
}
} else {
// TODO: Throw an exception? We got neither a string nor something
// that looks like a compatible lessphp cache structure.
return null;
}
if (is_null($root)) {
// No changes, pass back the structure
// we were given initially.
return $in;
}
// If we have a root value which means we should rebuild.
$out = [];
$out['root'] = $root;
$out['compiled'] = $this->compileFile($root);
$out['files'] = $this->scss->getParsedFiles();
$out['updated'] = time();
return $out;
}
/**
* Constructor
*
* @param string $dir Root directory to .scss files
* @param string $cacheDir Cache directory
* @param \ScssPhp\ScssPhp\Compiler|null $scss SCSS compiler instance
*/
public function __construct($dir, $cacheDir = null, $scss = null)
{
$this->dir = $dir;
if (! isset($cacheDir)) {
$cacheDir = $this->join($dir, 'scss_cache');
}
$this->cacheDir = $cacheDir;
if (! is_dir($this->cacheDir)) {
throw new ServerException('Cache directory doesn\'t exist: ' . $cacheDir);
}
if (! isset($scss)) {
$scss = new Compiler();
$scss->setImportPaths($this->dir);
}
$this->scss = $scss;
$this->showErrorsAsCSS = false;
date_default_timezone_set('UTC');
}
}
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<ruleset name="PSR12 (adapted for PHP 5.6+)">
<rule ref="PSR12">
<!-- Ignore this PHP 7.1+ sniff as long as we support PHP 5.6+ -->
<exclude name="PSR12.Properties.ConstantVisibility.NotFound"/>
<!-- This sniff doesn't ignore comment blocks -->
<!--
<exclude name="Generic.Files.LineLength"/>
-->
</rule>
</ruleset>
+15 -28
View File
@@ -1,34 +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', false)) {
include_once __DIR__ . '/src/Base/Range.php';
include_once __DIR__ . '/src/Block.php';
include_once __DIR__ . '/src/Cache.php';
include_once __DIR__ . '/src/Colors.php';
include_once __DIR__ . '/src/Compiler.php';
include_once __DIR__ . '/src/Compiler/Environment.php';
include_once __DIR__ . '/src/Exception/CompilerException.php';
include_once __DIR__ . '/src/Exception/ParserException.php';
include_once __DIR__ . '/src/Exception/RangeException.php';
include_once __DIR__ . '/src/Exception/ServerException.php';
include_once __DIR__ . '/src/Formatter.php';
include_once __DIR__ . '/src/Formatter/Compact.php';
include_once __DIR__ . '/src/Formatter/Compressed.php';
include_once __DIR__ . '/src/Formatter/Crunched.php';
include_once __DIR__ . '/src/Formatter/Debug.php';
include_once __DIR__ . '/src/Formatter/Expanded.php';
include_once __DIR__ . '/src/Formatter/Nested.php';
include_once __DIR__ . '/src/Formatter/OutputBlock.php';
include_once __DIR__ . '/src/Node.php';
include_once __DIR__ . '/src/Node/Number.php';
include_once __DIR__ . '/src/Parser.php';
include_once __DIR__ . '/src/SourceMap/Base64.php';
include_once __DIR__ . '/src/SourceMap/Base64VLQ.php';
include_once __DIR__ . '/src/SourceMap/SourceMapGenerator.php';
include_once __DIR__ . '/src/Type.php';
include_once __DIR__ . '/src/Util.php';
include_once __DIR__ . '/src/Version.php';
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;
}
});
}
+11 -1
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2015-2019 Leaf Corcoran
* @copyright 2015-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -15,10 +16,19 @@ 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;
/**
+6 -3
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -15,6 +16,8 @@ namespace ScssPhp\ScssPhp;
* Block
*
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @internal
*/
class Block
{
@@ -49,7 +52,7 @@ class Block
public $sourceColumn;
/**
* @var array
* @var array|null
*/
public $selectors;
@@ -64,7 +67,7 @@ class Block
public $children;
/**
* @var \ScssPhp\ScssPhp\Block
* @var \ScssPhp\ScssPhp\Block|null
*/
public $selfParent;
}
+51 -18
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -12,6 +13,7 @@
namespace ScssPhp\ScssPhp;
use Exception;
use ScssPhp\ScssPhp\Version;
/**
* The scss cache manager.
@@ -22,37 +24,60 @@ use Exception;
* taking in account options that affects the result
*
* The cache manager is agnostic about data format and only the operation is expected to be described by string
*
*/
/**
* SCSS cache
*
* @author Cedric Morin
* @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
*
@@ -97,18 +122,20 @@ class Cache
{
$fileCache = self::$cacheDir . self::cacheName($operation, $what, $options);
if (((self::$forceRefresh === false) || (self::$forceRefresh === 'once' &&
if (
((self::$forceRefresh === false) || (self::$forceRefresh === 'once' &&
isset(self::$refreshed[$fileCache]))) && file_exists($fileCache)
) {
$cacheTime = filemtime($fileCache);
if ((is_null($lastModified) || $cacheTime > $lastModified) &&
if (
(\is_null($lastModified) || $cacheTime > $lastModified) &&
$cacheTime + self::$gcLifetime > time()
) {
$c = file_get_contents($fileCache);
$c = unserialize($c);
if (is_array($c) && isset($c['value'])) {
if (\is_array($c) && isset($c['value'])) {
return $c['value'];
}
}
@@ -125,6 +152,8 @@ class Cache
* @param mixed $what
* @param mixed $value
* @param array $options
*
* @return void
*/
public function setCache($operation, $what, $value, $options = [])
{
@@ -132,6 +161,7 @@ class Cache
$c = ['value' => $value];
$c = serialize($c);
file_put_contents($fileCache, $c);
if (self::$forceRefresh === 'once') {
@@ -153,6 +183,7 @@ class Cache
{
$t = [
'version' => self::CACHE_VERSION,
'scssphpVersion' => Version::VERSION,
'operation' => $operation,
'what' => $what,
'options' => $options
@@ -169,6 +200,8 @@ class Cache
/**
* Check that the cache dir exists and is writeable
*
* @return void
*
* @throws \Exception
*/
public static function checkCacheDir()
@@ -177,9 +210,7 @@ class Cache
self::$cacheDir = rtrim(self::$cacheDir, '/') . '/';
if (! is_dir(self::$cacheDir)) {
if (! mkdir(self::$cacheDir)) {
throw new Exception('Cache directory couldn\'t be created: ' . self::$cacheDir);
}
throw new Exception('Cache directory doesn\'t exist: ' . self::$cacheDir);
}
if (! is_writable(self::$cacheDir)) {
@@ -189,6 +220,8 @@ class Cache
/**
* Delete unused cached files
*
* @return void
*/
public static function cleanCache()
{
+23 -22
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -15,6 +16,8 @@ namespace ScssPhp\ScssPhp;
* CSS Colors
*
* @author Leaf Corcoran <leafot@gmail.com>
*
* @internal
*/
class Colors
{
@@ -23,12 +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',
'aqua' => '0,255,255',
'cyan' => '0,255,255',
'aquamarine' => '127,255,212',
'azure' => '240,255,255',
'beige' => '245,245,220',
@@ -46,13 +50,12 @@ class Colors
'cornflowerblue' => '100,149,237',
'cornsilk' => '255,248,220',
'crimson' => '220,20,60',
'cyan' => '0,255,255',
'darkblue' => '0,0,139',
'darkcyan' => '0,139,139',
'darkgoldenrod' => '184,134,11',
'darkgray' => '169,169,169',
'darkgreen' => '0,100,0',
'darkgrey' => '169,169,169',
'darkgreen' => '0,100,0',
'darkkhaki' => '189,183,107',
'darkmagenta' => '139,0,139',
'darkolivegreen' => '85,107,47',
@@ -75,14 +78,15 @@ class Colors
'floralwhite' => '255,250,240',
'forestgreen' => '34,139,34',
'fuchsia' => '255,0,255',
'magenta' => '255,0,255',
'gainsboro' => '220,220,220',
'ghostwhite' => '248,248,255',
'gold' => '255,215,0',
'goldenrod' => '218,165,32',
'gray' => '128,128,128',
'grey' => '128,128,128',
'green' => '0,128,0',
'greenyellow' => '173,255,47',
'grey' => '128,128,128',
'honeydew' => '240,255,240',
'hotpink' => '255,105,180',
'indianred' => '205,92,92',
@@ -98,8 +102,8 @@ class Colors
'lightcyan' => '224,255,255',
'lightgoldenrodyellow' => '250,250,210',
'lightgray' => '211,211,211',
'lightgreen' => '144,238,144',
'lightgrey' => '211,211,211',
'lightgreen' => '144,238,144',
'lightpink' => '255,182,193',
'lightsalmon' => '255,160,122',
'lightseagreen' => '32,178,170',
@@ -111,7 +115,6 @@ class Colors
'lime' => '0,255,0',
'limegreen' => '50,205,50',
'linen' => '250,240,230',
'magenta' => '255,0,255',
'maroon' => '128,0,0',
'mediumaquamarine' => '102,205,170',
'mediumblue' => '0,0,205',
@@ -145,7 +148,6 @@ class Colors
'plum' => '221,160,221',
'powderblue' => '176,224,230',
'purple' => '128,0,128',
'rebeccapurple' => '102,51,153',
'red' => '255,0,0',
'rosybrown' => '188,143,143',
'royalblue' => '65,105,225',
@@ -167,7 +169,6 @@ class Colors
'teal' => '0,128,128',
'thistle' => '216,191,216',
'tomato' => '255,99,71',
'transparent' => '0,0,0,0',
'turquoise' => '64,224,208',
'violet' => '238,130,238',
'wheat' => '245,222,179',
@@ -175,6 +176,8 @@ class Colors
'whitesmoke' => '245,245,245',
'yellow' => '255,255,0',
'yellowgreen' => '154,205,50',
'rebeccapurple' => '102,51,153',
'transparent' => '0,0,0,0',
];
/**
@@ -182,11 +185,11 @@ class Colors
*
* @param string $colorName
*
* @return array|null
* @return int[]|null
*/
public static function colorNameToRGBa($colorName)
{
if (is_string($colorName) && isset(static::$cssColors[$colorName])) {
if (\is_string($colorName) && isset(static::$cssColors[$colorName])) {
$rgba = explode(',', static::$cssColors[$colorName]);
// only case with opacity is transparent, with opacity=0, so we can intval on opacity also
@@ -204,7 +207,7 @@ class Colors
* @param integer $r
* @param integer $g
* @param integer $b
* @param integer $a
* @param integer|float $a
*
* @return string|null
*/
@@ -217,28 +220,26 @@ class Colors
}
if ($a < 1) {
# specific case we dont' revert according to spec
#if (! $a && ! $r && ! $g && ! $b) {
# return 'transparent';
#}
return null;
}
if (is_null($reverseColorTable)) {
if (\is_null($reverseColorTable)) {
$reverseColorTable = [];
foreach (static::$cssColors as $name => $rgb_str) {
$rgb_str = explode(',', $rgb_str);
if (count($rgb_str) == 3) {
$reverseColorTable[intval($rgb_str[0])][intval($rgb_str[1])][intval($rgb_str[2])] = $name;
if (
\count($rgb_str) == 3 &&
! isset($reverseColorTable[\intval($rgb_str[0])][\intval($rgb_str[1])][\intval($rgb_str[2])])
) {
$reverseColorTable[\intval($rgb_str[0])][\intval($rgb_str[1])][\intval($rgb_str[2])] = $name;
}
}
}
if (isset($reverseColorTable[intval($r)][intval($g)][intval($b)])) {
return $reverseColorTable[intval($r)][intval($g)][intval($b)];
if (isset($reverseColorTable[\intval($r)][\intval($g)][\intval($b)])) {
return $reverseColorTable[\intval($r)][\intval($g)][\intval($b)];
}
return null;
+69
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;
}
}
+3904 -1495
View File
File diff suppressed because it is too large Load Diff
+77
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;
}
}
+6 -3
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -15,16 +16,18 @@ 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;
+5 -2
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -15,7 +16,9 @@ namespace ScssPhp\ScssPhp\Exception;
* Compiler exception
*
* @author Oleksandr Savchenko <traveltino@gmail.com>
*
* @internal
*/
class CompilerException extends \Exception
class CompilerException extends \Exception implements SassException
{
}
+31 -2
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -15,7 +16,35 @@ namespace ScssPhp\ScssPhp\Exception;
* Parser Exception
*
* @author Oleksandr Savchenko <traveltino@gmail.com>
*
* @internal
*/
class ParserException extends \Exception
class ParserException extends \Exception implements SassException
{
/**
* @var array
*/
private $sourcePosition;
/**
* Get source position
*
* @api
*/
public function getSourcePosition()
{
return $this->sourcePosition;
}
/**
* Set source position
*
* @api
*
* @param array $sourcePosition
*/
public function setSourcePosition($sourcePosition)
{
$this->sourcePosition = $sourcePosition;
}
}
+5 -2
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -15,7 +16,9 @@ namespace ScssPhp\ScssPhp\Exception;
* Range exception
*
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @internal
*/
class RangeException extends \Exception
class RangeException extends \Exception implements SassException
{
}
@@ -0,0 +1,7 @@
<?php
namespace ScssPhp\ScssPhp\Exception;
interface SassException
{
}
@@ -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);
}
}
+7 -2
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -11,11 +12,15 @@
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
class ServerException extends \Exception implements SassException
{
}
+66 -18
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -18,6 +19,8 @@ use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
* Base formatter
*
* @author Leaf Corcoran <leafot@gmail.com>
*
* @internal
*/
abstract class Formatter
{
@@ -77,7 +80,7 @@ abstract class Formatter
protected $currentColumn;
/**
* @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator
* @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null
*/
protected $sourceMapGenerator;
@@ -118,16 +121,33 @@ abstract class Formatter
return rtrim($name) . $this->assignSeparator . $value . ';';
}
/**
* Return custom property assignment
* differs in that you have to keep spaces in the value as is
*
* @api
*
* @param string $name
* @param mixed $value
*
* @return string
*/
public function customProperty($name, $value)
{
return rtrim($name) . trim($this->assignSeparator) . $value . ';';
}
/**
* Output lines inside a block
*
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
*
* @return void
*/
protected function blockLines(OutputBlock $block)
{
$inner = $this->indentStr();
$glue = $this->break . $inner;
$glue = $this->break . $inner;
$this->write($inner . implode($glue, $block->lines));
@@ -140,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
@@ -154,6 +178,8 @@ abstract class Formatter
* Output block children
*
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
*
* @return void
*/
protected function blockChildren(OutputBlock $block)
{
@@ -166,6 +192,8 @@ abstract class Formatter
* Output non-empty block
*
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
*
* @return void
*/
protected function block(OutputBlock $block)
{
@@ -269,6 +297,8 @@ abstract class Formatter
* Output content
*
* @param string $str
*
* @return void
*/
protected function write($str)
{
@@ -282,7 +312,8 @@ abstract class Formatter
* Maybe Strip semi-colon appended by property(); it's a separator, not a terminator
* will be striped for real before a closing, otherwise displayed unchanged starting the next write
*/
if (! $this->keepSemicolons &&
if (
! $this->keepSemicolons &&
$str &&
(strpos($str, ';') !== false) &&
(substr($str, -1) === ';')
@@ -293,22 +324,39 @@ 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 !== '') {
$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 !== '') {
$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;
+8 -1
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -17,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
{
@@ -25,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 = '';
+6 -2
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -12,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
{
@@ -67,6 +69,8 @@ class Compressed extends Formatter
*/
protected function blockSelectors(OutputBlock $block)
{
assert(! empty($block->selectors));
$inner = $this->indentStr();
$this->write(
+10 -2
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -12,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
{
@@ -26,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 = '';
@@ -65,6 +71,8 @@ class Crunched extends Formatter
*/
protected function blockSelectors(OutputBlock $block)
{
assert(! empty($block->selectors));
$inner = $this->indentStr();
$this->write(
+8 -2
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -12,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
{
@@ -26,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";
+5 -3
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -12,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
{
@@ -55,7 +57,7 @@ class Expanded extends Formatter
foreach ($block->lines as $index => $line) {
if (substr($line, 0, 2) === '/*') {
$block->lines[$index] = preg_replace('/[\r\n]+/', $glue, $line);
$block->lines[$index] = preg_replace('/\r\n?|\n|\f/', $this->break, $line);
}
}
+15 -7
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -12,13 +13,16 @@
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
{
@@ -32,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";
@@ -58,12 +64,11 @@ class Nested extends Formatter
protected function blockLines(OutputBlock $block)
{
$inner = $this->indentStr();
$glue = $this->break . $inner;
$glue = $this->break . $inner;
foreach ($block->lines as $index => $line) {
if (substr($line, 0, 2) === '/*') {
$block->lines[$index] = preg_replace('/[\r\n]+/', $glue, $line);
$block->lines[$index] = preg_replace('/\r\n?|\n|\f/', $this->break, $line);
}
}
@@ -90,7 +95,7 @@ class Nested extends Formatter
$previousHasSelector = false;
}
$isMediaOrDirective = in_array($block->type, [Type::T_DIRECTIVE, Type::T_MEDIA]);
$isMediaOrDirective = \in_array($block->type, [Type::T_DIRECTIVE, Type::T_MEDIA]);
$isSupport = ($block->type === Type::T_DIRECTIVE
&& $block->selectors && strpos(implode('', $block->selectors), '@supports') !== false);
@@ -98,7 +103,8 @@ class Nested extends Formatter
array_pop($depths);
$this->depth--;
if (! $this->depth && ($block->depth <= 1 || (! $this->indentLevel && $block->type === Type::T_COMMENT)) &&
if (
! $this->depth && ($block->depth <= 1 || (! $this->indentLevel && $block->type === Type::T_COMMENT)) &&
(($block->selectors && ! $isMediaOrDirective) || $previousHasSelector)
) {
$downLevel = $this->break;
@@ -119,10 +125,12 @@ class Nested extends Formatter
if ($block->depth > end($depths)) {
if (! $previousEmpty || $this->depth < 1) {
$this->depth++;
$depths[] = $block->depth;
} else {
// keep the current depth unchanged but take the block depth as a new reference for following blocks
array_pop($depths);
$depths[] = $block->depth;
}
}
+11 -8
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -15,6 +16,8 @@ namespace ScssPhp\ScssPhp\Formatter;
* Output block
*
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @internal
*/
class OutputBlock
{
@@ -29,37 +32,37 @@ class OutputBlock
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 integer|null
*/
public $sourceLine;
/**
* @var integer
* @var integer|null
*/
public $sourceColumn;
}
+48
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);
}
+27
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\Logger;
/**
* A logger that silently ignores all messages.
*/
class QuietLogger implements LoggerInterface
{
public function warn($message, $deprecation = false)
{
}
public function debug($message)
{
}
}
+60
View File
@@ -0,0 +1,60 @@
<?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)
*/
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");
}
}
+6 -3
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -15,6 +16,8 @@ namespace ScssPhp\ScssPhp;
* Base node
*
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @internal
*/
abstract class Node
{
@@ -29,12 +32,12 @@ abstract class Node
public $sourceIndex;
/**
* @var integer
* @var int|null
*/
public $sourceLine;
/**
* @var integer
* @var int|null
*/
public $sourceColumn;
}
+593 -123
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -11,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
@@ -25,20 +30,26 @@ 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
* @deprecated use {Number::PRECISION} instead to read the precision. Configuring it is not supported anymore.
*/
static public $precision = 10;
public static $precision = self::PRECISION;
/**
* @see http://www.w3.org/TR/2012/WD-css3-values-20120308/
*
* @var array
* @phpstan-var array<string, array<string, float|int>>
*/
static protected $unitTable = [
protected static $unitTable = [
'in' => [
'in' => 1,
'pc' => 6,
@@ -64,75 +75,75 @@ class Number extends Node implements \ArrayAccess
],
'dpi' => [
'dpi' => 1,
'dpcm' => 2.54,
'dppx' => 96,
'dpcm' => 1 / 2.54,
'dppx' => 1 / 96,
],
];
/**
* @var integer|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 integer|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;
foreach (static::$unitTable['in'] 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 = [];
return $this->numeratorUnits;
}
$this->normalizeUnits($dimension, $units, 'in');
return new Number($dimension, $units);
/**
* @return string[]
*/
public function getDenominatorUnits()
{
return $this->denominatorUnits;
}
/**
@@ -141,14 +152,15 @@ class Number extends Node implements \ArrayAccess
public function offsetExists($offset)
{
if ($offset === -3) {
return ! is_null($this->sourceColumn);
return ! \is_null($this->sourceColumn);
}
if ($offset === -2) {
return ! is_null($this->sourceLine);
return ! \is_null($this->sourceLine);
}
if ($offset === -1 ||
if (
$offset === -1 ||
$offset === 0 ||
$offset === 1 ||
$offset === 2
@@ -175,13 +187,13 @@ 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);
}
}
@@ -190,17 +202,7 @@ class Number extends Node implements \ArrayAccess
*/
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');
}
/**
@@ -208,17 +210,7 @@ class Number extends Node implements \ArrayAccess
*/
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');
}
/**
@@ -228,7 +220,19 @@ class Number extends Node implements \ArrayAccess
*/
public function unitless()
{
return ! array_sum($this->units);
return \count($this->numeratorUnits) === 0 && \count($this->denominatorUnits) === 0;
}
/**
* 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;
}
/**
@@ -238,22 +242,289 @@ 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 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;
}
}
/**
@@ -265,35 +536,31 @@ class Number extends Node implements \ArrayAccess
*/
public function output(Compiler $compiler = null)
{
$dimension = round($this->dimension, static::$precision);
$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, 'in');
$dimension = round($dimension, static::$precision);
$units = array_filter($units, function ($unitSize) {
return $unitSize;
});
if (is_nan($dimension)) {
return 'NaN';
}
$unitSize = array_sum($units);
if ($compiler && ($unitSize > 1 || $unitSize < 0 || count($units) > 1)) {
$compiler->throwError((string) $dimension . $this->unitStr() . " isn't a valid CSS value.");
if ($dimension === INF) {
return 'Infinity';
}
reset($units);
$unit = key($units);
$dimension = number_format($dimension, static::$precision, '.', '');
if ($dimension === -INF) {
return '-Infinity';
}
return (static::$precision ? rtrim(rtrim($dimension, '0'), '.') : $dimension) . $unit;
if ($compiler) {
$unit = $this->unitStr();
} elseif (isset($this->numeratorUnits[0])) {
$unit = $this->numeratorUnits[0];
} else {
$unit = '';
}
$dimension = number_format($dimension, self::PRECISION, '.', '');
return rtrim(rtrim($dimension, '0'), '.') . $unit;
}
/**
@@ -305,26 +572,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 = 'in')
private function coerceNumber(Number $other, $operation)
{
$dimension = $this->dimension;
$units = [];
$result = $this->coerceUnits($other, $operation);
foreach ($this->units as $unit => $exp) {
if (isset(static::$unitTable[$baseUnit][$unit])) {
$factor = pow(static::$unitTable[$baseUnit][$unit], $exp);
if (!$this->unitless()) {
return new Number($result, $this->numeratorUnits, $this->denominatorUnits);
}
$unit = $baseUnit;
$dimension /= $factor;
return new Number($result, $other->numeratorUnits, $other->denominatorUnits);
}
/**
* @param Number $other
* @param callable $operation
*
* @return mixed
*
* @phpstan-template T
* @phpstan-param callable(int|float, int|float): T $operation
* @phpstan-return T
*/
private function coerceUnits(Number $other, $operation)
{
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;
}
$units[$unit] = $exp + (isset($units[$unit]) ? $units[$unit] : 0);
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) : '');
}
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace ScssPhp\ScssPhp;
final class OutputStyle
{
const EXPANDED = 'expanded';
const COMPRESSED = 'compressed';
}
+1378 -389
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -11,17 +12,16 @@
namespace ScssPhp\ScssPhp;
use ScssPhp\ScssPhp\Block;
use ScssPhp\ScssPhp\Cache;
use ScssPhp\ScssPhp\Compiler;
use ScssPhp\ScssPhp\Exception\ParserException;
use ScssPhp\ScssPhp\Node;
use ScssPhp\ScssPhp\Type;
use ScssPhp\ScssPhp\Logger\LoggerInterface;
use ScssPhp\ScssPhp\Logger\QuietLogger;
/**
* Parser
*
* @author Leaf Corcoran <leafot@gmail.com>
*
* @internal
*/
class Parser
{
@@ -30,7 +30,7 @@ class Parser
const SOURCE_COLUMN = -3;
/**
* @var array
* @var array<string, int>
*/
protected static $precedence = [
'=' => 0,
@@ -38,7 +38,6 @@ class Parser
'and' => 2,
'==' => 3,
'!=' => 3,
'<=>' => 3,
'<=' => 4,
'>=' => 4,
'<' => 4,
@@ -50,38 +49,89 @@ class Parser
'%' => 6,
];
/**
* @var string
*/
protected static $commentPattern;
/**
* @var string
*/
protected static $operatorPattern;
/**
* @var string
*/
protected static $whitePattern;
/**
* @var Cache|null
*/
protected $cache;
private $sourceName;
private $sourceIndex;
/**
* @var array<int, int>
*/
private $sourcePositions;
/**
* @var array|null
*/
private $charset;
/**
* The current offset in the buffer
*
* @var int
*/
private $count;
/**
* @var Block|null
*/
private $env;
/**
* @var bool
*/
private $inParens;
/**
* @var bool
*/
private $eatWhiteDefault;
/**
* @var bool
*/
private $discardComments;
private $allowVars;
/**
* @var string
*/
private $buffer;
private $utf8;
/**
* @var string|null
*/
private $encoding;
private $patternModifiers;
private $commentsSeen;
private $cssOnly;
/**
* @var LoggerInterface
*/
private $logger;
/**
* Constructor
*
* @api
*
* @param string $sourceName
* @param integer $sourceIndex
* @param string $encoding
* @param \ScssPhp\ScssPhp\Cache $cache
* @param string|null $sourceName
* @param integer $sourceIndex
* @param string|null $encoding
* @param Cache|null $cache
* @param bool $cssOnly
* @param LoggerInterface|null $logger
*/
public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', $cache = null)
public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', Cache $cache = null, $cssOnly = false, LoggerInterface $logger = null)
{
$this->sourceName = $sourceName ?: '(stdin)';
$this->sourceIndex = $sourceIndex;
@@ -89,10 +139,13 @@ class Parser
$this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8';
$this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais';
$this->commentsSeen = [];
$this->discardComments = false;
$this->commentsSeen = [];
$this->allowVars = true;
$this->cssOnly = $cssOnly;
$this->logger = $logger ?: new QuietLogger();
if (empty(static::$operatorPattern)) {
static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)';
$commentSingle = '\/\/';
$commentMultiLeft = '\/\*';
@@ -104,9 +157,7 @@ class Parser
: '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS';
}
if ($cache) {
$this->cache = $cache;
}
$this->cache = $cache;
}
/**
@@ -128,9 +179,32 @@ class Parser
*
* @param string $msg
*
* @throws \ScssPhp\ScssPhp\Exception\ParserException
* @phpstan-return never-return
*
* @throws 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);
@@ -138,11 +212,21 @@ class Parser
? "line: $line, column: $column"
: "$this->sourceName on line $line, at column $column";
if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
throw new ParserException("$msg: failed at `$m[1]` $loc");
if ($this->peek('(.*?)(\n|$)', $m, $this->count)) {
$this->restoreEncoding();
$e = new ParserException("$msg: failed at `$m[1]` $loc");
$e->setSourcePosition([$this->sourceName, $line, $column]);
return $e;
}
throw new ParserException("$msg: $loc");
$this->restoreEncoding();
$e = new ParserException("$msg: $loc");
$e->setSourcePosition([$this->sourceName, $line, $column]);
return $e;
}
/**
@@ -152,19 +236,19 @@ class Parser
*
* @param string $buffer
*
* @return \ScssPhp\ScssPhp\Block
* @return Block
*/
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)) {
if (! \is_null($v)) {
return $v;
}
}
@@ -192,12 +276,12 @@ class Parser
;
}
if ($this->count !== strlen($this->buffer)) {
$this->throwParseError();
if ($this->count !== \strlen($this->buffer)) {
throw $this->parseError();
}
if (! empty($this->env->parent)) {
$this->throwParseError('unclosed block');
throw $this->parseError('unclosed block');
}
if ($this->charset) {
@@ -207,7 +291,7 @@ class Parser
$this->restoreEncoding();
if ($this->cache) {
$this->cache->setCache("parse", $cacheKey, $this->env, $parseOptions);
$this->cache->setCache('parse', $cacheKey, $this->env, $parseOptions);
}
return $this->env;
@@ -232,6 +316,7 @@ class Parser
$this->buffer = (string) $buffer;
$this->saveEncoding();
$this->extractLineNumbers($this->buffer);
$list = $this->valueList($out);
@@ -247,10 +332,11 @@ class Parser
*
* @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;
@@ -259,11 +345,21 @@ class Parser
$this->buffer = (string) $buffer;
$this->saveEncoding();
$this->extractLineNumbers($this->buffer);
// discard space/comments at the start
$this->discardComments = true;
$this->whitespace();
$this->discardComments = false;
$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;
}
@@ -286,6 +382,7 @@ class Parser
$this->buffer = (string) $buffer;
$this->saveEncoding();
$this->extractLineNumbers($this->buffer);
$isMediaQuery = $this->mediaQueryList($out);
@@ -339,14 +436,17 @@ class Parser
// 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)
) {
! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
$atRoot->selector = $selector;
$atRoot->with = $with;
@@ -356,7 +456,11 @@ class Parser
$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];
@@ -365,11 +469,14 @@ class Parser
$this->seek($s);
if ($this->literal('@mixin', 6) &&
if (
$this->literal('@mixin', 6) &&
$this->keyword($mixinName) &&
($this->argumentDef($args) || true) &&
$this->matchChar('{', false)
) {
! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
$mixin->name = $mixinName;
$mixin->args = $args;
@@ -379,17 +486,20 @@ class Parser
$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)
) {
! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$child = [
Type::T_INCLUDE,
$mixinName,
@@ -410,10 +520,17 @@ class Parser
$this->seek($s);
if ($this->literal('@scssphp-import-once', 20) &&
if (
$this->literal('@scssphp-import-once', 20) &&
$this->valueList($importPath) &&
$this->end()
) {
! $this->cssOnly || $this->assertPlainCssValid(false, $s);
list($line, $column) = $this->getSourcePosition($s);
$file = $this->sourceName;
$this->logger->warn("The \"@scssphp-import-once\" directive is deprecated and will be removed in ScssPhp 2.0, in \"$file\", line $line, column $column.", true);
$this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s);
return true;
@@ -421,10 +538,18 @@ class Parser
$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;
@@ -432,10 +557,17 @@ class Parser
$this->seek($s);
if ($this->literal('@import', 7) &&
if (
$this->literal('@import', 7) &&
$this->url($importPath) &&
$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;
@@ -443,10 +575,13 @@ class Parser
$this->seek($s);
if ($this->literal('@extend', 7) &&
if (
$this->literal('@extend', 7) &&
$this->selectors($selectors) &&
$this->end()
) {
! $this->cssOnly || $this->assertPlainCssValid(false, $s);
// check for '!flag'
$optional = $this->stripOptionalFlag($selectors);
$this->append([Type::T_EXTEND, $selectors, $optional], $s);
@@ -456,11 +591,14 @@ class Parser
$this->seek($s);
if ($this->literal('@function', 9) &&
if (
$this->literal('@function', 9) &&
$this->keyword($fnName) &&
$this->argumentDef($args) &&
$this->matchChar('{', false)
) {
! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
$func->name = $fnName;
$func->args = $args;
@@ -470,23 +608,13 @@ class Parser
$this->seek($s);
if ($this->literal('@break', 6) && $this->end()) {
$this->append([Type::T_BREAK], $s);
if (
$this->literal('@return', 7) &&
($this->valueList($retVal) || true) &&
$this->end()
) {
! $this->cssOnly || $this->assertPlainCssValid(false, $s);
return true;
}
$this->seek($s);
if ($this->literal('@continue', 9) && $this->end()) {
$this->append([Type::T_CONTINUE], $s);
return true;
}
$this->seek($s);
if ($this->literal('@return', 7) && ($this->valueList($retVal) || true) && $this->end()) {
$this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s);
return true;
@@ -494,12 +622,15 @@ class Parser
$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)
) {
! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$each = $this->pushSpecialBlock(Type::T_EACH, $s);
foreach ($varNames[2] as $varName) {
@@ -513,10 +644,22 @@ class Parser
$this->seek($s);
if ($this->literal('@while', 6) &&
if (
$this->literal('@while', 6) &&
$this->expression($cond) &&
$this->matchChar('{', false)
) {
! $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);
$while->cond = $cond;
@@ -525,7 +668,8 @@ class Parser
$this->seek($s);
if ($this->literal('@for', 4) &&
if (
$this->literal('@for', 4) &&
$this->variable($varName) &&
$this->literal('from', 4) &&
$this->expression($start) &&
@@ -534,6 +678,8 @@ class Parser
$this->expression($end) &&
$this->matchChar('{', false)
) {
! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$for = $this->pushSpecialBlock(Type::T_FOR, $s);
$for->var = $varName[1];
$for->start = $start;
@@ -545,14 +691,23 @@ class Parser
$this->seek($s);
if ($this->literal('@if', 3) && $this->valueList($cond) && $this->matchChar('{', false)) {
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]);
}
$if->cond = $cond;
$if->cases = [];
@@ -561,10 +716,12 @@ class Parser
$this->seek($s);
if ($this->literal('@debug', 6) &&
$this->valueList($value) &&
$this->end()
if (
$this->literal('@debug', 6) &&
$this->functionCallArgumentsList($value, false)
) {
! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$this->append([Type::T_DEBUG, $value], $s);
return true;
@@ -572,10 +729,12 @@ class Parser
$this->seek($s);
if ($this->literal('@warn', 5) &&
$this->valueList($value) &&
$this->end()
if (
$this->literal('@warn', 5) &&
$this->functionCallArgumentsList($value, false)
) {
! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$this->append([Type::T_WARN, $value], $s);
return true;
@@ -583,10 +742,12 @@ class Parser
$this->seek($s);
if ($this->literal('@error', 6) &&
$this->valueList($value) &&
$this->end()
if (
$this->literal('@error', 6) &&
$this->functionCallArgumentsList($value, false)
) {
! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$this->append([Type::T_ERROR, $value], $s);
return true;
@@ -594,14 +755,16 @@ class Parser
$this->seek($s);
#if ($this->literal('@content', 8))
if ($this->literal('@content', 8) &&
if (
$this->literal('@content', 8) &&
($this->end() ||
$this->matchChar('(') &&
$this->argValues($argContent) &&
$this->matchChar(')') &&
$this->end())) {
$this->argValues($argContent) &&
$this->matchChar(')') &&
$this->end())
) {
! $this->cssOnly || $this->assertPlainCssValid(false, $s);
$this->append([Type::T_MIXIN_CONTENT, isset($argContent) ? $argContent : null], $s);
return true;
@@ -617,7 +780,10 @@ class Parser
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;
}
@@ -634,7 +800,8 @@ class Parser
}
// only retain the first @charset directive encountered
if ($this->literal('@charset', 8) &&
if (
$this->literal('@charset', 8) &&
$this->valueList($charset) &&
$this->end()
) {
@@ -655,9 +822,10 @@ class Parser
$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';
@@ -669,11 +837,16 @@ class Parser
$this->seek($s);
// doesn't match built in directive, do generic one
if ($this->matchChar('@', false) &&
$this->keyword($dirName) &&
($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
$this->matchChar('{', false)
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 {
@@ -682,6 +855,7 @@ class Parser
}
if (isset($dirValue)) {
! $this->cssOnly || ($dirValue = $this->assertPlainCssValid($dirValue));
$directive->value = $dirValue;
}
@@ -691,12 +865,38 @@ class Parser
$this->seek($s);
// maybe it's a generic blockless directive
if ($this->matchChar('@', false) &&
$this->keyword($dirName) &&
$this->valueList($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;
}
@@ -706,9 +906,60 @@ class Parser
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->matchChar(':', false)) {
$start = $this->count;
// but can be complex and finish with ; or }
foreach ([';','}'] as $ending) {
if (
$this->openString($ending, $stringValue, '(', ')', false) &&
$this->end()
) {
$end = $this->count;
$value = $stringValue;
// check if we have only a partial value due to nested [] or { } to take in account
$nestingPairs = [['[', ']'], ['{', '}']];
foreach ($nestingPairs as $nestingPair) {
$p = strpos($this->buffer, $nestingPair[0], $start);
if ($p && $p < $end) {
$this->seek($start);
if (
$this->openString($ending, $stringValue, $nestingPair[0], $nestingPair[1], false) &&
$this->end() &&
$this->count > $end
) {
$end = $this->count;
$value = $stringValue;
}
}
}
$this->seek($end);
$this->append([Type::T_CUSTOM_PROPERTY, $name, $value], $s);
return true;
}
}
// TODO: output an error here if nothing found according to sass spec
}
$this->seek($s);
// 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()
@@ -722,11 +973,14 @@ class Parser
$this->seek($s);
// variable assigns
if ($this->variable($name) &&
if (
$this->variable($name) &&
$this->matchChar(':') &&
$this->valueList($value) &&
$this->end()
) {
! $this->cssOnly || $this->assertPlainCssValid(false, $s);
// check for '!flag'
$assignmentFlags = $this->stripAssignmentFlags($value);
$this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s);
@@ -736,13 +990,13 @@ class Parser
$this->seek($s);
// misc
if ($this->literal('-->', 3)) {
return true;
}
// opening css block
if ($this->selectors($selectors) && $this->matchChar('{', false)) {
if (
$this->selectors($selectors) &&
$this->matchChar('{', false)
) {
! $this->cssOnly || ! $inCssSelector || $this->assertPlainCssValid(false);
$this->pushBlock($selectors, $s);
if ($this->eatWhiteDefault) {
@@ -756,12 +1010,15 @@ class Parser
$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);
@@ -769,6 +1026,8 @@ class Parser
}
if ($this->matchChar('{', false)) {
! $this->cssOnly || $this->assertPlainCssValid(false);
$propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
$propBlock->prefix = $name;
$propBlock->hasValue = $foundSomething;
@@ -818,9 +1077,7 @@ class Parser
}
// extra stuff
if ($this->matchChar(';') ||
$this->literal('<!--', 4)
) {
if ($this->matchChar(';')) {
return true;
}
@@ -830,16 +1087,16 @@ class Parser
/**
* Push block onto parse tree
*
* @param array $selectors
* @param array|null $selectors
* @param integer $pos
*
* @return \ScssPhp\ScssPhp\Block
* @return Block
*/
protected function pushBlock($selectors, $pos = 0)
{
list($line, $column) = $this->getSourcePosition($pos);
$b = new Block;
$b = new Block();
$b->sourceName = $this->sourceName;
$b->sourceLine = $line;
$b->sourceColumn = $column;
@@ -879,7 +1136,7 @@ class Parser
* @param string $type
* @param integer $pos
*
* @return \ScssPhp\ScssPhp\Block
* @return Block
*/
protected function pushSpecialBlock($type, $pos)
{
@@ -892,7 +1149,7 @@ class Parser
/**
* Pop scope and return last block
*
* @return \ScssPhp\ScssPhp\Block
* @return Block
*
* @throws \Exception
*/
@@ -908,7 +1165,7 @@ class Parser
$block = $this->env;
if (empty($block->parent)) {
$this->throwParseError('unexpected }');
throw $this->parseError('unexpected }');
}
if ($block->type == Type::T_AT_ROOT) {
@@ -939,7 +1196,7 @@ class Parser
}
$r = '/' . $regex . '/' . $this->patternModifiers;
$result = preg_match($r, $this->buffer, $out, null, $from);
$result = preg_match($r, $this->buffer, $out, 0, $from);
return $result;
}
@@ -954,13 +1211,217 @@ class Parser
$this->count = $where;
}
/**
* Assert a parsed part is plain CSS Valid
*
* @param array|false $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',
'hwb',
'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_CUSTOM_PROPERTY:
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
*/
@@ -968,10 +1429,10 @@ class Parser
{
$token = null;
$end = strlen($this->buffer);
$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) {
@@ -990,7 +1451,7 @@ class Parser
$match,
$token
];
$this->count = $end + strlen($token);
$this->count = $end + \strlen($token);
return true;
}
@@ -1008,11 +1469,11 @@ class Parser
{
$r = '/' . $regex . '/' . $this->patternModifiers;
if (! preg_match($r, $this->buffer, $out, null, $this->count)) {
if (! preg_match($r, $this->buffer, $out, 0, $this->count)) {
return false;
}
$this->count += strlen($out[0]);
$this->count += \strlen($out[0]);
if (! isset($eatWhitespace)) {
$eatWhitespace = $this->eatWhiteDefault;
@@ -1089,12 +1550,12 @@ class Parser
{
$gotWhite = false;
while (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) {
while (preg_match(static::$whitePattern, $this->buffer, $m, 0, $this->count)) {
if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
// comment that are kept in the output CSS
$comment = [];
$startCommentCount = $this->count;
$endCommentCount = $this->count + strlen($m[1]);
$endCommentCount = $this->count + \strlen($m[1]);
// find interpolations in comment
$p = strpos($this->buffer, '#{', $this->count);
@@ -1108,7 +1569,7 @@ class Parser
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--;
}
@@ -1117,6 +1578,9 @@ class Parser
$comment[] = [Type::T_COMMENT, substr($this->buffer, $p, $this->count - $p), $out];
} else {
list($line, $column) = $this->getSourcePosition($this->count);
$file = $this->sourceName;
$this->logger->warn("Unterminated interpolations in multiline comments are deprecated and will be removed in ScssPhp 2.0, in \"$file\", line $line, column $column.", true);
$comment[] = substr($this->buffer, $this->count, 2);
$this->count += 2;
@@ -1134,14 +1598,25 @@ class Parser
} else {
$comment[] = $c;
$staticComment = substr($this->buffer, $startCommentCount, $endCommentCount - $startCommentCount);
$this->appendComment([Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]]);
$commentStatement = [Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]];
list($line, $column) = $this->getSourcePosition($startCommentCount);
$commentStatement[self::SOURCE_LINE] = $line;
$commentStatement[self::SOURCE_COLUMN] = $column;
$commentStatement[self::SOURCE_INDEX] = $this->sourceIndex;
$this->appendComment($commentStatement);
}
$this->commentsSeen[$startCommentCount] = true;
$this->count = $endCommentCount;
} else {
// comment that are ignored and not kept in the output css
$this->count += strlen($m[0]);
$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;
@@ -1158,23 +1633,6 @@ class Parser
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;
}
}
@@ -1182,13 +1640,15 @@ class Parser
/**
* Append statement to current block
*
* @param array $statement
* @param array|null $statement
* @param integer $pos
*/
protected function append($statement, $pos = null)
{
if (! is_null($statement)) {
if (! is_null($pos)) {
if (! \is_null($statement)) {
! $this->cssOnly || ($statement = $this->assertPlainCssValid($statement, $pos));
if (! \is_null($pos)) {
list($line, $column) = $this->getSourcePosition($pos);
$statement[static::SOURCE_LINE] = $line;
@@ -1214,7 +1674,7 @@ class Parser
*/
protected function last()
{
$i = count($this->env->children) - 1;
$i = \count($this->env->children) - 1;
if (isset($this->env->children[$i])) {
return $this->env->children[$i];
@@ -1245,7 +1705,9 @@ class Parser
$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];
@@ -1261,7 +1723,7 @@ class Parser
$media = [Type::T_LIST, '', []];
foreach ((array) $mediaType as $type) {
if (is_array($type)) {
if (\is_array($type)) {
$media[2][] = $type;
} else {
$media[2][] = [Type::T_KEYWORD, $type];
@@ -1275,7 +1737,7 @@ class Parser
if (empty($parts) || $this->literal('and', 3)) {
$this->genericList($expressions, 'mediaExpression', 'and', false);
if (is_array($expressions)) {
if (\is_array($expressions)) {
$parts = array_merge($parts, $expressions[2]);
}
}
@@ -1301,13 +1763,14 @@ class Parser
$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->valueList($value) &&
$this->matchChar(')')
) {
) {
$support = [Type::T_STRING, '', [[Type::T_KEYWORD, ($not ? 'not ' : '') . '(']]];
$support[2][] = $property;
$support[2][] = [Type::T_KEYWORD, ': '];
@@ -1320,7 +1783,8 @@ class Parser
$this->seek($s);
}
if ($this->matchChar('(') &&
if (
$this->matchChar('(') &&
$this->supportsQuery($subQuery) &&
$this->matchChar(')')
) {
@@ -1330,7 +1794,8 @@ class Parser
$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]];
@@ -1339,7 +1804,8 @@ class Parser
$this->seek($s);
}
if ($this->literal('selector(', 9) &&
if (
$this->literal('selector(', 9) &&
$this->selector($selector) &&
$this->matchChar(')')
) {
@@ -1351,7 +1817,7 @@ class Parser
$compound = [Type::T_STRING, '', []];
foreach ($sc as $scp) {
if (is_array($scp)) {
if (\is_array($scp)) {
$compound[2][] = $scp;
} else {
$compound[2][] = [Type::T_KEYWORD, $scp];
@@ -1360,6 +1826,7 @@ class Parser
$selectorList[2][] = $compound;
}
$support[2][] = $selectorList;
$support[2][] = [Type::T_KEYWORD, ')'];
$parts[] = $support;
@@ -1375,8 +1842,10 @@ class Parser
$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];
@@ -1385,8 +1854,10 @@ class Parser
$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];
@@ -1395,7 +1866,7 @@ class Parser
$this->seek($s);
}
if (count($parts)) {
if (\count($parts)) {
if ($this->eatWhiteDefault) {
$this->whitespace();
}
@@ -1421,9 +1892,11 @@ class Parser
$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];
@@ -1449,12 +1922,19 @@ class Parser
*/
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;
}
@@ -1477,7 +1957,7 @@ class Parser
$keyword = null;
}
if ($this->genericList($value, 'expression')) {
if ($this->genericList($value, 'expression', '', true)) {
$out = [$keyword, $value, false];
$s = $this->count;
@@ -1493,6 +1973,115 @@ class Parser
return false;
}
/**
* Check if a generic directive is known to be able to allow almost any syntax or not
* @param mixed $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
*
* @param array $out
* @param boolean|string $endChar
*
* @return boolean
*/
protected function directiveValue(&$out, $endChar = false)
{
$s = $this->count;
if ($this->variable($out)) {
if ($endChar && $this->matchChar($endChar, false)) {
return true;
}
if (! $endChar && $this->end()) {
return true;
}
}
$this->seek($s);
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;
}
}
$this->seek($s);
$allowVars = $this->allowVars;
$this->allowVars = false;
$res = $this->genericList($out, 'spaceList', ',');
$this->allowVars = $allowVars;
if ($res) {
if ($endChar && $this->matchChar($endChar, false)) {
return true;
}
if (! $endChar && $this->end()) {
return true;
}
}
$this->seek($s);
if ($endChar && $this->matchChar($endChar, false)) {
return true;
}
return false;
}
/**
* Parse comma separated value list
*
@@ -1510,6 +2099,45 @@ class Parser
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
*
@@ -1525,10 +2153,10 @@ class Parser
/**
* Parse generic list
*
* @param array $out
* @param callable $parseItem
* @param string $delim
* @param boolean $flatten
* @param array $out
* @param string $parseItem The name of the method used to parse items
* @param string $delim
* @param boolean $flatten
*
* @return boolean
*/
@@ -1543,10 +2171,64 @@ class Parser
$items[] = $value;
if ($delim) {
if (! $this->literal($delim, strlen($delim))) {
if (! $this->literal($delim, \strlen($delim))) {
break;
}
$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;
}
}
}
}
}
@@ -1559,7 +2241,8 @@ class Parser
if ($trailing_delim) {
$items[] = [Type::T_NULL];
}
if ($flatten && count($items) === 1) {
if ($flatten && \count($items) === 1) {
$out = $items[0];
} else {
$out = [Type::T_LIST, $delim, $items];
@@ -1571,9 +2254,9 @@ class Parser
/**
* Parse expression
*
* @param array $out
* @param bool $listOnly
* @param bool $lookForExp
* @param array $out
* @param boolean $listOnly
* @param boolean $lookForExp
*
* @return boolean
*/
@@ -1585,7 +2268,7 @@ class Parser
$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 {
@@ -1600,13 +2283,14 @@ class Parser
$this->seek($s);
}
if (in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) {
if ($this->enclosedExpression($lhs, $s, "]", [Type::T_LIST])) {
if (\in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) {
if ($this->enclosedExpression($lhs, $s, ']', [Type::T_LIST])) {
if ($lookForExp) {
$out = $this->expHelper($lhs, 0);
} else {
$out = $lhs;
}
$this->discardComments = $discard;
return true;
@@ -1615,7 +2299,7 @@ class Parser
$this->seek($s);
}
if (!$listOnly && $this->value($lhs)) {
if (! $listOnly && $this->value($lhs)) {
if ($lookForExp) {
$out = $this->expHelper($lhs, 0);
} else {
@@ -1628,6 +2312,7 @@ class Parser
}
$this->discardComments = $discard;
return false;
}
@@ -1641,41 +2326,50 @@ class Parser
*
* @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)) {
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])
&& in_array(Type::T_LIST, $allowedTypes)) {
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'])) {
$out = [Type::T_LIST, '', [$out]];
}
switch ($closingParen) {
case ")":
case ')':
$out['enclosing'] = 'parent'; // parenthesis list
break;
case "]":
case ']':
$out['enclosing'] = 'bracket'; // bracketed list
break;
}
return true;
}
$this->seek($s);
if (in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) {
if (\in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) {
return true;
}
@@ -1717,12 +2411,15 @@ class Parser
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]]);
if ($op === '-' && ! $whiteAfter && $rhs[0] === Type::T_KEYWORD) {
break;
}
// consume higher-precedence operators on the right-hand side
$rhs = $this->expHelper($rhs, static::$precedence[$op] + 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]);
@@ -1749,7 +2446,10 @@ class Parser
$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+/=',
@@ -1768,7 +2468,10 @@ class Parser
$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(')')) {
@@ -1783,7 +2486,10 @@ class Parser
// 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;
@@ -1804,28 +2510,56 @@ class Parser
if ($char === '+') {
$this->count++;
$follow_white = $this->whitespace();
if ($this->value($inner)) {
$out = [Type::T_UNARY, '+', $inner, $this->inParens];
return true;
}
$this->count--;
if ($follow_white) {
$out = [Type::T_KEYWORD, $char];
return true;
}
$this->seek($s);
return false;
}
// negation
if ($char === '-') {
if ($this->customProperty($out)) {
return true;
}
$this->count++;
$follow_white = $this->whitespace();
if ($this->variable($inner) || $this->unit($inner) || $this->parenValue($inner)) {
$out = [Type::T_UNARY, '-', $inner, $this->inParens];
return true;
}
$this->count--;
if (
$this->keyword($inner) &&
! $this->func($inner, $out)
) {
$out = [Type::T_UNARY, '-', $inner, $this->inParens];
return true;
}
if ($follow_white) {
$out = [Type::T_KEYWORD, $char];
return true;
}
$this->seek($s);
}
// paren
@@ -1837,6 +2571,16 @@ class Parser
if ($this->interpolation($out) || $this->color($out)) {
return true;
}
$this->count++;
if ($this->keyword($keyword)) {
$out = [Type::T_KEYWORD, '#' . $keyword];
return true;
}
$this->count--;
}
if ($this->matchChar('&', true)) {
@@ -1862,10 +2606,17 @@ class Parser
}
// 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)) {
@@ -1909,7 +2660,10 @@ class Parser
$this->inParens = true;
if ($this->expression($exp) && $this->matchChar(')')) {
if (
$this->expression($exp) &&
$this->matchChar(')')
) {
$out = $exp;
$this->inParens = $inParens;
@@ -1934,7 +2688,8 @@ class Parser
{
$s = $this->count;
if ($this->literal('progid:', 7, false) &&
if (
$this->literal('progid:', 7, false) &&
$this->openString('(', $fn) &&
$this->matchChar('(')
) {
@@ -1976,7 +2731,10 @@ class Parser
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;
@@ -1985,7 +2743,8 @@ class Parser
$this->seek($ss);
}
if (($this->openString(')', $str, '(') || true) &&
if (
($this->openString(')', $str, '(') || true) &&
$this->matchChar(')')
) {
$args = [];
@@ -2020,7 +2779,10 @@ class Parser
$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 {
@@ -2066,7 +2828,10 @@ class Parser
$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);
@@ -2078,10 +2843,11 @@ class Parser
$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;
$this->seek($sss);
} else {
$this->seek($ss);
@@ -2123,8 +2889,10 @@ class Parser
$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;
@@ -2156,13 +2924,15 @@ class Parser
{
$s = $this->count;
if ($this->match('(#([0-9a-f]+))', $m)) {
if (in_array(strlen($m[2]), [3,4,6,8])) {
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 true;
}
$this->seek($s);
return false;
}
@@ -2181,7 +2951,7 @@ class Parser
$s = $this->count;
if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m, false)) {
if (strlen($this->buffer) === $this->count || ! ctype_digit($this->buffer[$this->count])) {
if (\strlen($this->buffer) === $this->count || ! ctype_digit($this->buffer[$this->count])) {
$this->whitespace();
$unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
@@ -2202,7 +2972,7 @@ class Parser
*
* @return boolean
*/
protected function string(&$out)
protected function string(&$out, $keepDelimWithInterpolation = false)
{
$s = $this->count;
@@ -2225,52 +2995,48 @@ class Parser
}
if ($m[2] === '#{') {
$this->count -= strlen($m[2]);
$this->count -= \strlen($m[2]);
if ($this->interpolation($inter, false)) {
$content[] = $inter;
$hasInterpolation = true;
} else {
$this->count += strlen($m[2]);
$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) ||
$this->matchChar("\r", false) ||
$this->matchChar("\n", false) ||
$this->matchChar("\f", 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->count -= \strlen($delim);
break; // delim
}
}
$this->eatWhiteDefault = $oldWhite;
if ($this->literal($delim, strlen($delim))) {
if ($hasInterpolation) {
if ($this->literal($delim, \strlen($delim))) {
if ($hasInterpolation && ! $keepDelimWithInterpolation) {
$delim = '"';
foreach ($content as &$string) {
if ($string === "\\\\") {
$string = "\\";
} elseif ($string === "\\'") {
$string = "'";
} elseif ($string === '\\"') {
$string = '"';
}
}
}
$out = [Type::T_STRING, $delim, $content];
@@ -2283,6 +3049,55 @@ class Parser
return false;
}
/**
* @param string $out
* @param bool $inKeywords
* @return bool
*/
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;
}
}
// CSS allows Unicode escape sequences to be followed by a delimiter space
// (necessary in some cases for shorter sequences to disambiguate their end)
$this->matchChar(' ', false);
$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
*
@@ -2330,18 +3145,29 @@ class Parser
/**
* Parse an unbounded string stopped by $end
*
* @param string $end
* @param array $out
* @param string $nestingOpen
* @param string $end
* @param array $out
* @param string $nestOpen
* @param string $nestClose
* @param boolean $rtrim
* @param string $disallow
*
* @return boolean
*/
protected function openString($end, &$out, $nestingOpen = null)
protected function openString($end, &$out, $nestOpen = null, $nestClose = null, $rtrim = true, $disallow = null)
{
$oldWhite = $this->eatWhiteDefault;
$this->eatWhiteDefault = false;
$patt = '(.*?)([\'"]|#\{|' . $this->pregQuote($end) . '|' . static::$commentPattern . ')';
if ($nestOpen && ! $nestClose) {
$nestClose = $end;
}
$patt = ($disallow ? '[^' . $this->pregQuote($disallow) . ']' : '.');
$patt = '(' . $patt . '*?)([\'"]|#\{|'
. $this->pregQuote($end) . '|'
. (($nestClose && $nestClose !== $end) ? $this->pregQuote($nestClose) . '|' : '')
. static::$commentPattern . ')';
$nestingLevel = 0;
@@ -2351,20 +3177,24 @@ class Parser
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--) {
if ($tok === $end && ! $nestingLevel) {
break;
}
if (($tok === "'" || $tok === '"') && $this->string($str)) {
if ($tok === $nestClose) {
$nestingLevel--;
}
if (($tok === "'" || $tok === '"') && $this->string($str, true)) {
$content[] = $str;
continue;
}
@@ -2375,18 +3205,18 @@ class Parser
}
$content[] = $tok;
$this->count+= strlen($tok);
$this->count += \strlen($tok);
}
$this->eatWhiteDefault = $oldWhite;
if (! $content) {
if (! $content || $tok !== $end) {
return false;
}
// trim the end
if (is_string(end($content))) {
$content[count($content) - 1] = rtrim(end($content));
if ($rtrim && \is_string(end($content))) {
$content[\count($content) - 1] = rtrim(end($content));
}
$out = [Type::T_STRING, '', $content];
@@ -2405,17 +3235,26 @@ class Parser
protected function interpolation(&$out, $lookWhite = true)
{
$oldWhite = $this->eatWhiteDefault;
$allowVars = $this->allowVars;
$this->allowVars = true;
$this->eatWhiteDefault = true;
$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;
}
@@ -2424,6 +3263,7 @@ class Parser
}
$this->eatWhiteDefault = $oldWhite;
$this->allowVars = $allowVars;
if ($this->eatWhiteDefault) {
$this->whitespace();
@@ -2435,6 +3275,7 @@ class Parser
$this->seek($s);
$this->eatWhiteDefault = $oldWhite;
$this->allowVars = $allowVars;
return false;
}
@@ -2480,16 +3321,10 @@ class Parser
}
// match comment hack
if (preg_match(
static::$whitePattern,
$this->buffer,
$m,
null,
$this->count
)) {
if (preg_match(static::$whitePattern, $this->buffer, $m, 0, $this->count)) {
if (! empty($m[0])) {
$parts[] = $m[0];
$this->count += strlen($m[0]);
$this->count += \strlen($m[0]);
}
}
@@ -2500,11 +3335,70 @@ class Parser
return true;
}
/**
* Parse custom property name (as an array of parts or a string)
*
* @param array $out
*
* @return boolean
*/
protected function customProperty(&$out)
{
$s = $this->count;
if (! $this->literal('--', 2, false)) {
return false;
}
$parts = ['--'];
$oldWhite = $this->eatWhiteDefault;
$this->eatWhiteDefault = false;
for (;;) {
if ($this->interpolation($inter)) {
$parts[] = $inter;
continue;
}
if ($this->matchChar('&', false)) {
$parts[] = [Type::T_SELF];
continue;
}
if ($this->variable($var)) {
$parts[] = $var;
continue;
}
if ($this->keyword($text)) {
$parts[] = $text;
continue;
}
break;
}
$this->eatWhiteDefault = $oldWhite;
if (\count($parts) == 1) {
$this->seek($s);
return false;
}
$this->whitespace(); // get any extra whitespace
$out = [Type::T_STRING, '', $parts];
return true;
}
/**
* Parse comma separated selector list
*
* @param array $out
* @param boolean $subSelector
* @param array $out
* @param string|boolean $subSelector
*
* @return boolean
*/
@@ -2539,8 +3433,8 @@ class Parser
/**
* Parse whitespace separated selector list
*
* @param array $out
* @param boolean $subSelector
* @param array $out
* @param string|boolean $subSelector
*
* @return boolean
*/
@@ -2548,11 +3442,15 @@ class Parser
{
$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);
@@ -2564,18 +3462,15 @@ class Parser
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;
}
@@ -2585,6 +3480,55 @@ class Parser
return true;
}
/**
* parsing escaped chars in selectors:
* - escaped single chars are kept escaped in the selector but in a normalized form
* (if not in 0-9a-f range as this would be ambigous)
* - other escaped sequences (multibyte chars or 0-9a-f) are kept in their initial escaped form,
* normalized to lowercase
*
* TODO: this is a fallback solution. Ideally escaped chars in selectors should be encoded as the genuine chars,
* and escaping added when printing in the Compiler, where/if it's mandatory
* - but this require a better formal selector representation instead of the array we have now
*
* @param string $out
* @param bool $keepEscapedNumber
* @return bool
*/
protected function matchEscapeCharacterInSelector(&$out, $keepEscapedNumber = false)
{
$s_escape = $this->count;
if ($this->match('\\\\', $m)) {
$out = '\\' . $m[0];
return true;
}
if ($this->matchEscapeCharacter($escapedout, true)) {
if (strlen($escapedout) === 1) {
if (!preg_match(",\w,", $escapedout)) {
$out = '\\' . $escapedout;
return true;
} elseif (! $keepEscapedNumber || ! \is_numeric($escapedout)) {
$out = $escapedout;
return true;
}
}
$escape_sequence = rtrim(substr($this->buffer, $s_escape, $this->count - $s_escape));
if (strlen($escape_sequence) < 6) {
$escape_sequence .= ' ';
}
$out = '\\' . strtolower($escape_sequence);
return true;
}
if ($this->match('\\S', $m)) {
$out = '\\' . $m[0];
return true;
}
return false;
}
/**
* Parse the parts that make up a selector
*
@@ -2592,8 +3536,8 @@ class Parser
* div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
* }}
*
* @param array $out
* @param boolean $subSelector
* @param array $out
* @param string|boolean $subSelector
*
* @return boolean
*/
@@ -2631,6 +3575,7 @@ class Parser
case '&':
$parts[] = Compiler::$selfSelector;
$this->count++;
! $this->cssOnly || $this->assertPlainCssValid(false, $s);
continue 2;
case '.':
@@ -2644,9 +3589,14 @@ class Parser
continue 2;
}
if ($char === '\\' && $this->match('\\\\\S', $m)) {
$parts[] = $m[0];
continue;
// handling of escaping in selectors : get the escaped char
if ($char === '\\') {
$this->count++;
if ($this->matchEscapeCharacterInSelector($escaped, true)) {
$parts[] = $escaped;
continue;
}
$this->count--;
}
if ($char === '%') {
@@ -2655,6 +3605,7 @@ class Parser
if ($this->placeholder($placeholder)) {
$parts[] = '%';
$parts[] = $placeholder;
! $this->cssOnly || $this->assertPlainCssValid(false, $s);
continue;
}
@@ -2664,6 +3615,7 @@ class Parser
if ($char === '#') {
if ($this->interpolation($inter)) {
$parts[] = $inter;
! $this->cssOnly || $this->assertPlainCssValid(false, $s);
continue;
}
@@ -2691,15 +3643,21 @@ class Parser
$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) &&
($this->selectors($subs, reset($nameParts)) || true) &&
$this->matchChar(')')
if (
$this->matchChar('(', true) &&
($this->selectors($subs, reset($nameParts)) || true) &&
$this->matchChar(')')
) {
$parts[] = '(';
@@ -2709,12 +3667,12 @@ class Parser
$parts[] = $p;
}
if (count($sub) && reset($sub)) {
if (\count($sub) && reset($sub)) {
$parts[] = ' ';
}
}
if (count($subs) && reset($subs)) {
if (\count($subs) && reset($subs)) {
$parts[] = ', ';
}
}
@@ -2723,21 +3681,20 @@ class Parser
} else {
$this->seek($ss);
}
} else {
if ($this->matchChar('(') &&
($this->openString(')', $str, '(') || true) &&
$this->matchChar(')')
) {
$parts[] = '(';
} elseif (
$this->matchChar('(', true) &&
($this->openString(')', $str, '(') || true) &&
$this->matchChar(')')
) {
$parts[] = '(';
if (! empty($str)) {
$parts[] = $str;
}
$parts[] = ')';
} else {
$this->seek($ss);
if (! empty($str)) {
$parts[] = $str;
}
$parts[] = ')';
} else {
$this->seek($ss);
}
continue;
@@ -2747,7 +3704,7 @@ class Parser
$this->seek($s);
// 2n+1
if ($subSelector && is_string($subSelector) && strpos($subSelector, 'nth-') === 0) {
if ($subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0) {
if ($this->match("(\s*(\+\s*|\-\s*)?(\d+|n|\d+n))+", $counter)) {
$parts[] = $counter[0];
//$parts[] = str_replace(' ', '', $counter[0]);
@@ -2758,7 +3715,8 @@ class Parser
$this->seek($s);
// attribute selector
if ($char === '[' &&
if (
$char === '[' &&
$this->matchChar('[') &&
($this->openString(']', $str, '[') || true) &&
$this->matchChar(']')
@@ -2781,7 +3739,7 @@ class Parser
continue;
}
if ($this->restrictedKeyword($name)) {
if ($this->restrictedKeyword($name, false, true)) {
$parts[] = $name;
continue;
}
@@ -2811,8 +3769,15 @@ class Parser
{
$s = $this->count;
if ($this->matchChar('$', false) && $this->keyword($name)) {
$out = [Type::T_VARIABLE, $name];
if (
$this->matchChar('$', false) &&
$this->keyword($name)
) {
if ($this->allowVars) {
$out = [Type::T_VARIABLE, $name];
} else {
$out = [Type::T_KEYWORD, '$' . $name];
}
return true;
}
@@ -2827,20 +3792,62 @@ class Parser
*
* @param string $word
* @param boolean $eatWhitespace
* @param boolean $inSelector
*
* @return boolean
*/
protected function keyword(&$word, $eatWhitespace = null)
protected function keyword(&$word, $eatWhitespace = null, $inSelector = false)
{
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\-_"\']|[\\\\].)*)',
? '(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)*)'
: '(([\w_\-\*!"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)([\w\-_"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)*)',
$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
&& (
$inSelector ?
$this->matchEscapeCharacterInSelector($out)
:
$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;
}
@@ -2852,14 +3859,15 @@ class Parser
*
* @param string $word
* @param boolean $eatWhitespace
* @param boolean $inSelector
*
* @return boolean
*/
protected function restrictedKeyword(&$word, $eatWhitespace = null)
protected function restrictedKeyword(&$word, $eatWhitespace = null, $inSelector = false)
{
$s = $this->count;
if ($this->keyword($word, $eatWhitespace) && (ord($word[0]) > 57 || ord($word[0]) < 48)) {
if ($this->keyword($word, $eatWhitespace, $inSelector) && (\ord($word[0]) > 57 || \ord($word[0]) < 48)) {
return true;
}
@@ -2877,12 +3885,14 @@ class Parser
*/
protected function placeholder(&$placeholder)
{
if ($this->match(
$match = $this->match(
$this->utf8
? '([\pL\w\-_]+)'
: '([\w\-_]+)',
$m
)) {
);
if ($match) {
$placeholder = $m[1];
return true;
@@ -2904,10 +3914,28 @@ class Parser
*/
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;
@@ -2915,16 +3943,17 @@ class Parser
/**
* 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;
}
if ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
if ($this->count === \strlen($this->buffer) || $this->buffer[$this->count] === '}') {
// if there is end of file or a closing block next then we don't need a ;
return true;
}
@@ -2943,10 +3972,10 @@ class Parser
{
$flags = [];
for ($token = &$value; $token[0] === Type::T_LIST && ($s = count($token[2])); $token = &$lastNode) {
for ($token = &$value; $token[0] === Type::T_LIST && ($s = \count($token[2])); $token = &$lastNode) {
$lastNode = &$token[2][$s - 1];
while ($lastNode[0] === Type::T_KEYWORD && in_array($lastNode[1], ['!default', '!global'])) {
while ($lastNode[0] === Type::T_KEYWORD && \in_array($lastNode[1], ['!default', '!global'])) {
array_pop($token[2]);
$node = end($token[2]);
@@ -2973,7 +4002,7 @@ class Parser
$part = end($selector);
if ($part === ['!optional']) {
array_pop($selectors[count($selectors) - 1]);
array_pop($selectors[\count($selectors) - 1]);
$optional = true;
}
@@ -2990,57 +4019,13 @@ class Parser
*/
protected function flattenList($value)
{
if ($value[0] === Type::T_LIST && count($value[2]) === 1) {
if ($value[0] === Type::T_LIST && \count($value[2]) === 1) {
return $this->flattenList($value[2][0]);
}
return $value;
}
/**
* @deprecated
*
* {@internal
* advance counter to next occurrence of $what
* $until - don't include $what in advance
* $allowNewline, if string, will be used as valid char set
* }}
*/
protected function to($what, &$out, $until = false, $allowNewline = false)
{
if (is_string($allowNewline)) {
$validChars = $allowNewline;
} else {
$validChars = $allowNewline ? '.' : "[^\n]";
}
$m = null;
if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) {
return false;
}
if ($until) {
$this->count -= strlen($what); // give back $what
}
$out = $m[1];
return true;
}
/**
* @deprecated
*/
protected function show()
{
if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
return $m[1];
}
return '';
}
/**
* Quote regular expression
*
@@ -3068,10 +4053,10 @@ class Parser
$prev = $pos + 1;
}
$this->sourcePositions[] = strlen($buffer);
$this->sourcePositions[] = \strlen($buffer);
if (substr($buffer, -1) !== "\n") {
$this->sourcePositions[] = strlen($buffer) + 1;
$this->sourcePositions[] = \strlen($buffer) + 1;
}
}
@@ -3085,7 +4070,7 @@ class Parser
private function getSourcePosition($pos)
{
$low = 0;
$high = count($this->sourcePositions);
$high = \count($this->sourcePositions);
while ($low < $high) {
$mid = (int) (($high + $low) / 2);
@@ -3107,18 +4092,20 @@ class Parser
}
/**
* Save internal encoding
* Save internal encoding of mbstring
*
* When mbstring.func_overload is used to replace the standard PHP string functions,
* this method configures the internal encoding to a single-byte one so that the
* behavior matches the normal behavior of PHP string functions while using the parser.
* The existing internal encoding is saved and will be restored when calling {@see restoreEncoding}.
*
* If mbstring.func_overload is not used (or does not override string functions), this method is a no-op.
*
* @return void
*/
private function saveEncoding()
{
if (version_compare(PHP_VERSION, '7.2.0') >= 0) {
return;
}
// deprecated in PHP 7.2
$iniDirective = 'mbstring.func_overload';
if (extension_loaded('mbstring') && ini_get($iniDirective) & 2) {
if (\PHP_VERSION_ID < 80000 && \extension_loaded('mbstring') && (2 & (int) ini_get('mbstring.func_overload')) > 0) {
$this->encoding = mb_internal_encoding();
mb_internal_encoding('iso-8859-1');
@@ -3127,10 +4114,12 @@ class Parser
/**
* Restore internal encoding
*
* @return void
*/
private function restoreEncoding()
{
if (extension_loaded('mbstring') && $this->encoding) {
if (\extension_loaded('mbstring') && $this->encoding) {
mb_internal_encoding($this->encoding);
}
}
+6 -3
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -15,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',
@@ -89,7 +92,7 @@ class Base64
];
/**
* @var array
* @var array<string|int, int>
*/
private static $decodingMap = [
'A' => 0,
+10 -5
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -11,8 +12,6 @@
namespace ScssPhp\ScssPhp\SourceMap;
use ScssPhp\ScssPhp\SourceMap\Base64;
/**
* Base 64 VLQ
*
@@ -35,6 +34,8 @@ use ScssPhp\ScssPhp\SourceMap\Base64;
*
* @author John Lenz <johnlenz@google.com>
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @internal
*/
class Base64VLQ
{
@@ -61,7 +62,9 @@ class Base64VLQ
do {
$digit = $vlq & self::VLQ_BASE_MASK;
$vlq >>= self::VLQ_BASE_SHIFT;
//$vlq >>>= self::VLQ_BASE_SHIFT; // unsigned right shift
$vlq = (($vlq >> 1) & PHP_INT_MAX) >> (self::VLQ_BASE_SHIFT - 1);
if ($vlq > 0) {
$digit |= self::VLQ_CONTINUATION_BIT;
@@ -130,7 +133,9 @@ class Base64VLQ
private static function fromVLQSigned($value)
{
$negate = ($value & 1) === 1;
$value = ($value >> 1) & ~(1<<(8 * PHP_INT_SIZE - 1)); // unsigned right shift
//$value >>>= 1; // unsigned right shift
$value = ($value >> 1) & PHP_INT_MAX;
if (! $negate) {
return $value;
+47 -13
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -20,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
{
@@ -32,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
@@ -69,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 = [];
@@ -82,16 +87,24 @@ 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);
@@ -106,6 +119,8 @@ class SourceMapGenerator
* @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
*
* @return void
*/
public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $sourceFile)
{
@@ -128,11 +143,12 @@ class SourceMapGenerator
* @return string
*
* @throws \ScssPhp\ScssPhp\Exception\CompilerException If the file could not be saved
* @deprecated
*/
public function saveMap($content)
{
$file = $this->options['sourceMapWriteTo'];
$dir = dirname($file);
$dir = \dirname($file);
// directory does not exist
if (! is_dir($dir)) {
@@ -153,14 +169,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;
@@ -201,7 +219,7 @@ class SourceMapGenerator
}
// less.js compat fixes
if (count($sourceMap['sources']) && empty($sourceMap['sourceRoot'])) {
if (\count($sourceMap['sources']) && empty($sourceMap['sourceRoot'])) {
unset($sourceMap['sourceRoot']);
}
@@ -211,7 +229,7 @@ class SourceMapGenerator
/**
* Returns the sources contents
*
* @return array|null
* @return string[]|null
*/
protected function getSourcesContent()
{
@@ -231,14 +249,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)) {
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.
@@ -249,9 +274,16 @@ class SourceMapGenerator
}
ksort($groupedMap);
$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[] = ';';
}
@@ -260,8 +292,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']) {
@@ -313,8 +347,8 @@ class SourceMapGenerator
$basePath = $this->options['sourceMapBasepath'];
// "Trim" the 'sourceMapBasepath' from the output filename.
if (strlen($basePath) && strpos($filename, $basePath) === 0) {
$filename = substr($filename, strlen($basePath));
if (\strlen($basePath) && strpos($filename, $basePath) === 0) {
$filename = substr($filename, \strlen($basePath));
}
// Remove extra leading path separators.
+8 -1
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -21,12 +22,16 @@ class Type
const T_ASSIGN = 'assign';
const T_AT_ROOT = 'at-root';
const T_BLOCK = 'block';
/** @deprecated */
const T_BREAK = 'break';
const T_CHARSET = 'charset';
const T_COLOR = 'color';
const T_COMMENT = 'comment';
/** @deprecated */
const T_CONTINUE = 'continue';
/** @deprecated */
const T_CONTROL = 'control';
const T_CUSTOM_PROPERTY = 'custom';
const T_DEBUG = 'debug';
const T_DIRECTIVE = 'directive';
const T_EACH = 'each';
@@ -37,8 +42,10 @@ class Type
const T_EXTEND = 'extend';
const T_FOR = 'for';
const T_FUNCTION = 'function';
const T_FUNCTION_REFERENCE = 'function-reference';
const T_FUNCTION_CALL = 'fncall';
const T_HSL = 'hsl';
const T_HWB = 'hwb';
const T_IF = 'if';
const T_IMPORT = 'import';
const T_INCLUDE = 'include';
+120 -6
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -13,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
{
@@ -25,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.
*
@@ -39,6 +43,10 @@ class Util
$val = $value[1];
$grace = new Range(-0.00001, 0.00001);
if (! \is_numeric($val)) {
throw new RangeException("$name {$val} is not a number.");
}
if ($range->includes($val)) {
return $val;
}
@@ -67,4 +75,110 @@ class Util
return strtr(rawurlencode($string), $revert);
}
/**
* mb_chr() wrapper
*
* @param integer $code
*
* @return string
*/
public static function mbChr($code)
{
// 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');
}
if (0x80 > $code %= 0x200000) {
$s = \chr($code);
} elseif (0x800 > $code) {
$s = \chr(0xC0 | $code >> 6) . \chr(0x80 | $code & 0x3F);
} elseif (0x10000 > $code) {
$s = \chr(0xE0 | $code >> 12) . \chr(0x80 | $code >> 6 & 0x3F) . \chr(0x80 | $code & 0x3F);
} else {
$s = \chr(0xF0 | $code >> 18) . \chr(0x80 | $code >> 12 & 0x3F)
. \chr(0x80 | $code >> 6 & 0x3F) . \chr(0x80 | $code & 0x3F);
}
return $s;
}
/**
* mb_strlen() wrapper
*
* @param string $string
* @return int
*/
public static function mbStrlen($string)
{
// Use the native implementation if available.
if (\function_exists('mb_strlen')) {
return mb_strlen($string, 'UTF-8');
}
if (\function_exists('iconv_strlen')) {
return (int) @iconv_strlen($string, 'UTF-8');
}
throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.');
}
/**
* mb_substr() wrapper
* @param string $string
* @param int $start
* @param null|int $length
* @return string
*/
public static function mbSubstr($string, $start, $length = null)
{
// Use the native implementation if available.
if (\function_exists('mb_substr')) {
return mb_substr($string, $start, $length, 'UTF-8');
}
if (\function_exists('iconv_substr')) {
if ($start < 0) {
$start = static::mbStrlen($string) + $start;
if ($start < 0) {
$start = 0;
}
}
if (null === $length) {
$length = 2147483647;
} elseif ($length < 0) {
$length = static::mbStrlen($string) + $length - $start;
if ($length < 0) {
return '';
}
}
return (string)iconv_substr($string, $start, $length, 'UTF-8');
}
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.');
}
}
+77
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;
}
}
+95
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)));
}
}
+3 -2
View File
@@ -1,8 +1,9 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2019 Leaf Corcoran
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
@@ -18,5 +19,5 @@ namespace ScssPhp\ScssPhp;
*/
class Version
{
const VERSION = 'v1.0.6';
const VERSION = '1.6.0';
}
+84
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);
}
}