first commit

This commit is contained in:
2026-03-05 13:07:40 +01:00
commit 64ba0721ee
25709 changed files with 4691006 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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>

View File

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

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\Ast;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* A node in an abstract syntax tree.
*
* @internal
*/
interface AstNode
{
public function getSpan(): FileSpan;
public function __toString(): string;
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
/**
* An unknown plain CSS at-rule.
*
* @internal
*/
interface CssAtRule extends CssParentNode
{
/**
* The name of this rule.
*
* @return CssValue<string>
*/
public function getName(): CssValue;
/**
* The value of this rule.
*
* @return CssValue<string>|null
*/
public function getValue(): ?CssValue;
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
/**
* A plain CSS comment.
*
* This is always a multi-line comment.
*
* @internal
*/
interface CssComment extends CssNode
{
/**
* The contents of this comment, including `/*` and `* /`.
*/
public function getText(): string;
/**
* Whether this comment starts with `/*!` and so should be preserved even in
* compressed mode.
*/
public function isPreserved(): bool;
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Value\Value;
/**
* A plain CSS declaration (that is, a `name: value` pair).
*
* @internal
*/
interface CssDeclaration extends CssNode
{
/**
* The name of this declaration.
*
* @return CssValue<string>
*/
public function getName(): CssValue;
/**
* The value of this declaration.
*
* @return CssValue<Value>
*/
public function getValue(): CssValue;
/**
* The span for {@see getValue} that should be emitted to the source map.
*
* When the declaration's expression is just a variable, this is the span
* where that variable was declared whereas `$this->getValue()->getSpan()` is the span where
* the variable was used. Otherwise, this is identical to `$this->getValue()->getSpan()`.
*/
public function getValueSpanForMap(): FileSpan;
/**
* Returns whether this is a CSS Custom Property declaration.
*/
public function isCustomProperty(): bool;
/**
* Whether this was originally parsed as a custom property declaration, as
* opposed to using something like `#{--foo}: ...` to cause it to be parsed
* as a normal Sass declaration.
*
* If this is `true`, {@see isCustomProperty} will also be `true` and {@see getValue} will
* contain a {@see SassString}.
*/
public function isParsedAsCustomProperty(): bool;
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
/**
* A plain CSS `@import`.
*
* @internal
*/
interface CssImport extends CssNode
{
/**
* The URL being imported.
*
* This includes quotes.
*
* @return CssValue<string>
*/
public function getUrl(): CssValue;
/**
* The modifiers (such as media or supports queries) attached to this import.
*
* @return CssValue<string>|null
*/
public function getModifiers(): ?CssValue;
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
/**
* A block within a `@keyframes` rule.
*
* For example, `10% {opacity: 0.5}`.
*
* @internal
*/
interface CssKeyframeBlock extends CssParentNode
{
/**
* The selector for this block.
*
* @return CssValue<list<string>>
*/
public function getSelector(): CssValue;
}

View File

@@ -0,0 +1,258 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
use ScssPhp\ScssPhp\Exception\SassFormatException;
use ScssPhp\ScssPhp\Logger\LoggerInterface;
use ScssPhp\ScssPhp\Parser\MediaQueryParser;
/**
* A plain CSS media query, as used in `@media` and `@import`.
*
* @internal
*/
final class CssMediaQuery
{
public const MERGE_RESULT_EMPTY = 'empty';
public const MERGE_RESULT_UNREPRESENTABLE = 'unrepresentable';
/**
* The modifier, probably either "not" or "only".
*
* This may be `null` if no modifier is in use.
*
* @var string|null
* @readonly
*/
private $modifier;
/**
* The media type, for example "screen" or "print".
*
* This may be `null`. If so, {@see $conditions} will not be empty.
*
* @var string|null
* @readonly
*/
private $type;
/**
* Whether {@see $conditions} is a conjunction or a disjunction.
*
* In other words, if this is `true` this query matches when _all_
* {@see $conditions} are met, and if it's `false` this query matches when _any_
* condition in {@see $conditions} is met.
*
* If this is `false`, {@see $modifier} and {@see $type} will both be `null`.
*
* @var bool
* @readonly
*/
private $conjunction;
/**
* Media conditions, including parentheses.
*
* This is anything that can appear in the [`<media-in-parens>`] production.
*
* [`<media-in-parens>`]: https://drafts.csswg.org/mediaqueries-4/#typedef-media-in-parens
*
* @var list<string>
* @readonly
*/
private $conditions;
/**
* Parses a media query from $contents.
*
* If passed, $url is the name of the file from which $contents comes.
*
* @return list<CssMediaQuery>
*
* @throws SassFormatException if parsing fails
*/
public static function parseList(string $contents, ?LoggerInterface $logger = null, ?string $url = null): array
{
return (new MediaQueryParser($contents, $logger, $url))->parse();
}
/**
* @param list<string> $conditions
*/
private function __construct(array $conditions = [], bool $conjunction = true, ?string $type = null, ?string $modifier = null)
{
$this->modifier = $modifier;
$this->type = $type;
$this->conditions = $conditions;
$this->conjunction = $conjunction;
}
/**
* Creates a media query specifies a type and, optionally, conditions.
*
* This always sets {@see $conjunction} to `true`.
*
* @param list<string> $conditions
*/
public static function type(?string $type, ?string $modifier = null, array $conditions = []): CssMediaQuery
{
return new CssMediaQuery($conditions, true, $type, $modifier);
}
/**
* Creates a media query that matches $conditions according to
* $conjunction.
*
* The $conjunction argument may not be null if $conditions is longer than
* a single element.
*
* @param list<string> $conditions
*/
public static function condition(array $conditions, ?bool $conjunction = null): CssMediaQuery
{
if (\count($conditions) > 1 && $conjunction === null) {
throw new \InvalidArgumentException('If conditions is longer than one element, conjunction may not be null.');
}
return new CssMediaQuery($conditions, $conjunction ?? true);
}
public function getModifier(): ?string
{
return $this->modifier;
}
public function getType(): ?string
{
return $this->type;
}
public function isConjunction(): bool
{
return $this->conjunction;
}
/**
* @return list<string>
*/
public function getConditions(): array
{
return $this->conditions;
}
/**
* Whether this media query matches all media types.
*/
public function matchesAllTypes(): bool
{
return $this->type === null || strtolower($this->type) === 'all';
}
/**
* Merges this with $other to return a query that matches the intersection
* of both inputs.
*
* @return CssMediaQuery|string
* @phpstan-return CssMediaQuery|CssMediaQuery::*
*/
public function merge(CssMediaQuery $other)
{
if (!$this->conjunction || !$other->conjunction) {
return self::MERGE_RESULT_UNREPRESENTABLE;
}
$ourModifier = $this->modifier !== null ? strtolower($this->modifier) : null;
$ourType = $this->type !== null ? strtolower($this->type) : null;
$theirModifier = $other->modifier !== null ? strtolower($other->modifier) : null;
$theirType = $other->type !== null ? strtolower($other->type) : null;
if ($ourType === null && $theirType === null) {
return self::condition(array_merge($this->conditions, $other->conditions), true);
}
if (($ourModifier === 'not') !== ($theirModifier === 'not')) {
if ($ourType === $theirType) {
$negativeConditions = $ourModifier === 'not' ? $this->conditions : $other->conditions;
$positiveConditions = $ourModifier === 'not' ? $other->conditions : $this->conditions;
// If the negative conditions are a subset of the positive conditions, the
// query is empty. For example, `not screen and (color)` has no
// intersection with `screen and (color) and (grid)`.
//
// However, `not screen and (color)` *does* intersect with `screen and
// (grid)`, because it means `not (screen and (color))` and so it allows
// a screen with no color but with a grid.
if (empty(array_diff($negativeConditions, $positiveConditions))) {
return self::MERGE_RESULT_EMPTY;
}
return self::MERGE_RESULT_UNREPRESENTABLE;
}
if ($this->matchesAllTypes() || $other->matchesAllTypes()) {
return self::MERGE_RESULT_UNREPRESENTABLE;
}
if ($ourModifier === 'not') {
$modifier = $theirModifier;
$type = $theirType;
$conditions = $other->conditions;
} else {
$modifier = $ourModifier;
$type = $ourType;
$conditions = $this->conditions;
}
} elseif ($ourModifier === 'not') {
// CSS has no way of representing "neither screen nor print".
if ($ourType !== $theirType) {
return self::MERGE_RESULT_UNREPRESENTABLE;
}
$moreConditions = \count($this->conditions) > \count($other->conditions) ? $this->conditions : $other->conditions;
$fewerConditions = \count($this->conditions) > \count($other->conditions) ? $other->conditions : $this->conditions;
// If one set of features is a superset of the other, use those features
// because they're strictly narrower.
if (empty(array_diff($fewerConditions, $moreConditions))) {
$modifier = $ourModifier; // "not"
$type = $ourType;
$conditions = $moreConditions;
} else {
// Otherwise, there's no way to represent the intersection.
return self::MERGE_RESULT_UNREPRESENTABLE;
}
} elseif ($this->matchesAllTypes()) {
$modifier = $theirModifier;
// Omit the type if either input query did, since that indicates that they
// aren't targeting a browser that requires "all and".
$type = $other->matchesAllTypes() && $ourType === null ? null : $theirType;
$conditions = array_merge($this->conditions, $other->conditions);
} elseif ($other->matchesAllTypes()) {
$modifier = $ourModifier;
$type = $ourType;
$conditions = array_merge($this->conditions, $other->conditions);
} elseif ($ourType !== $theirType) {
return self::MERGE_RESULT_EMPTY;
} else {
$modifier = $ourModifier ?? $theirModifier;
$type = $ourType;
$conditions = array_merge($this->conditions, $other->conditions);
}
return CssMediaQuery::type(
$type === $ourType ? $this->type : $other->type,
$modifier === $ourModifier ? $this->modifier : $other->modifier,
$conditions
);
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
/**
* A plain CSS `@media` rule.
*
* @internal
*/
interface CssMediaRule extends CssParentNode
{
/**
* The queries for this rule.
*
* This is never empty.
*
* @return list<CssMediaQuery>
*/
public function getQueries(): array;
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
use ScssPhp\ScssPhp\Ast\AstNode;
use ScssPhp\ScssPhp\Visitor\CssVisitor;
/**
* A statement in a plain CSS syntax tree.
*
* @internal
*/
interface CssNode extends AstNode
{
/**
* Whether this was generated from the last node in a nested Sass tree that
* got flattened during evaluation.
*/
public function isGroupEnd(): bool;
/**
* Calls the appropriate visit method on $visitor.
*
* @template T
*
* @param CssVisitor<T> $visitor
*
* @return T
*/
public function accept($visitor);
/**
* Whether this is invisible and won't be emitted to the compiled stylesheet.
*
* Note that this doesn't consider nodes that contain loud comments to be
* invisible even though they're omitted in compressed mode.
*/
public function isInvisible(): bool;
/**
* Whether this node would be invisible even if style rule selectors within it
* didn't have bogus combinators.
*
* Note that this doesn't consider nodes that contain loud comments to be
* invisible even though they're omitted in compressed mode.
*/
public function isInvisibleOtherThanBogusCombinators(): bool;
/**
* Whether this node will be invisible when loud comments are stripped.
*/
public function isInvisibleHidingComments(): bool;
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
/**
* A {@see CssNode} that can have child statements.
*
* @internal
*/
interface CssParentNode extends CssNode
{
/**
* The child statements of this node.
*
* @return list<CssNode>
*/
public function getChildren(): array;
/**
* Whether the rule has no children and should be emitted without curly
* braces.
*
* This implies `children.isEmpty`, but the reverse is not true—for a rule
* like `@foo {}`, {@see getChildren} is empty but {@see isChildless} is `false`.
*/
public function isChildless(): bool;
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
use ScssPhp\ScssPhp\Ast\Selector\SelectorList;
/**
* A plain CSS style rule.
* *
* * This applies style declarations to elements that match a given selector.
* * Note that this isn't *strictly* plain CSS, since {@see getSelector} may still
* * contain placeholder selectors.
*
* @internal
*/
interface CssStyleRule extends CssParentNode
{
/**
* The selector for this rule.
*
* @return CssValue<SelectorList>
*/
public function getSelector(): CssValue;
/**
* The selector for this rule, before any extensions were applied.
*/
public function getOriginalSelector(): SelectorList;
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
/**
* A plain CSS stylesheet.
*
* This is the root plain CSS node. It contains top-level statements.
*
* @internal
*/
interface CssStylesheet extends CssParentNode
{
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
/**
* A plain CSS `@supports` rule.
*
* @internal
*/
interface CssSupportsRule extends CssParentNode
{
/**
* The supports condition.
*
* @return CssValue<string>
*/
public function getCondition(): CssValue;
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
use ScssPhp\ScssPhp\Ast\AstNode;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* A value in a plain CSS tree.
*
* This is used to associate a span with a value that doesn't otherwise track
* its span.
*
* @template T
*
* @internal
*/
class CssValue implements AstNode
{
/**
* @phpstan-var T
*/
protected $value;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param T $value
*/
public function __construct($value, FileSpan $span)
{
$this->value = $value;
$this->span = $span;
}
/**
* @return T
*/
public function getValue()
{
return $this->value;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function __toString(): string
{
if (\is_array($this->value)) {
return implode($this->value);
}
return (string) $this->value;
}
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
use ScssPhp\ScssPhp\Visitor\EveryCssVisitor;
/**
* The visitor used to implement {@see CssNode::isInvisible}
*
* @internal
*/
final class IsInvisibleVisitor extends EveryCssVisitor
{
/**
* Whether to consider selectors with bogus combinators invisible.
*
* @var bool
* @readonly
*/
private $includeBogus;
/**
* Whether to consider comments invisible.
*
* @var bool
* @readonly
*/
private $includeComments;
public function __construct(bool $includeBogus, bool $includeComments)
{
$this->includeBogus = $includeBogus;
$this->includeComments = $includeComments;
}
public function visitCssAtRule($node): bool
{
// An unknown at-rule is never invisible. Because we don't know the semantics
// of unknown rules, we can't guarantee that (for example) `@foo {}` isn't
// meaningful.
return false;
}
public function visitCssComment($node): bool
{
return $this->includeComments && !$node->isPreserved();
}
public function visitCssStyleRule($node): bool
{
return ($this->includeBogus ? $node->getSelector()->getValue()->isInvisible() : $node->getSelector()->getValue()->isInvisibleOtherThanBogusCombinators()) || parent::visitCssStyleRule($node);
}
}

View File

@@ -0,0 +1,103 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* A modifiable version of {@see CssAtRule} for use in the evaluation step.
*
* @internal
*/
final class ModifiableCssAtRule extends ModifiableCssParentNode implements CssAtRule
{
/**
* @var CssValue<string>
*/
private $name;
/**
* @var CssValue<string>|null
*/
private $value;
/**
* @var bool
* @readonly
*/
private $childless;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param CssValue<string> $name
* @param CssValue<string>|null $value
* @param bool $childless
* @param FileSpan $span
*/
public function __construct(CssValue $name, FileSpan $span, bool $childless = false, ?CssValue $value = null)
{
parent::__construct();
$this->name = $name;
$this->value = $value;
$this->childless = $childless;
$this->span = $span;
}
public function getName(): CssValue
{
return $this->name;
}
public function getValue(): ?CssValue
{
return $this->value;
}
public function isChildless(): bool
{
return $this->childless;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept($visitor)
{
return $visitor->visitCssAtRule($this);
}
/**
* @phpstan-return ModifiableCssAtRule
*/
public function copyWithoutChildren(): ModifiableCssParentNode
{
return new ModifiableCssAtRule($this->name, $this->span, $this->childless, $this->value);
}
public function addChild(ModifiableCssNode $child): void
{
if ($this->childless) {
throw new \LogicException('Cannot add a child in a childless at-rule.');
}
parent::addChild($child);
}
}

View File

@@ -0,0 +1,61 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* A modifiable version of {@see CssComment} for use in the evaluation step.
*
* @internal
*/
final class ModifiableCssComment extends ModifiableCssNode implements CssComment
{
/**
* @var string
* @readonly
*/
private $text;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(string $text, FileSpan $span)
{
$this->text = $text;
$this->span = $span;
}
public function getText(): string
{
return $this->text;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function isPreserved(): bool
{
return $this->text[2] === '!';
}
public function accept($visitor)
{
return $visitor->visitCssComment($this);
}
}

View File

@@ -0,0 +1,115 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Value\SassString;
use ScssPhp\ScssPhp\Value\Value;
/**
* A modifiable version of {@see CssDeclaration} for use in the evaluation step.
*
* @internal
*/
final class ModifiableCssDeclaration extends ModifiableCssNode implements CssDeclaration
{
/**
* @var CssValue<string>
* @readonly
*/
private $name;
/**
* @var CssValue<Value>
* @readonly
*/
private $value;
/**
* @var bool
* @readonly
*/
private $parsedAsCustomProperty;
/**
* @var FileSpan
* @readonly
*/
private $valueSpanForMap;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param CssValue<string> $name
* @param CssValue<Value> $value
* @param bool $parsedAsCustomProperty
* @param FileSpan $valueSpanForMap
* @param FileSpan $span
*/
public function __construct(CssValue $name, CssValue $value, FileSpan $span, bool $parsedAsCustomProperty, ?FileSpan $valueSpanForMap = null) {
$this->name = $name;
$this->value = $value;
$this->parsedAsCustomProperty = $parsedAsCustomProperty;
$this->valueSpanForMap = $valueSpanForMap ?? $value->getSpan();
$this->span = $span;
if ($parsedAsCustomProperty) {
if (!$this->isCustomProperty()) {
throw new \InvalidArgumentException('parsedAsCustomProperty must be false if name doesn\'t begin with "--".');
}
if (!$value->getValue() instanceof SassString) {
throw new \InvalidArgumentException(sprintf('If parsedAsCustomProperty is true, value must contain a SassString (was %s).', get_class($value->getValue())));
}
}
}
public function getName(): CssValue
{
return $this->name;
}
public function getValue(): CssValue
{
return $this->value;
}
public function isParsedAsCustomProperty(): bool
{
return $this->parsedAsCustomProperty;
}
public function getValueSpanForMap(): FileSpan
{
return $this->valueSpanForMap;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function isCustomProperty(): bool
{
return 0 === strpos($this->name->getValue(), '--');
}
public function accept($visitor)
{
return $visitor->visitCssDeclaration($this);
}
}

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\Ast\Css;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* A modifiable version of {@see CssImport} for use in the evaluation step.
*
* @internal
*/
final class ModifiableCssImport extends ModifiableCssNode implements CssImport
{
/**
* The URL being imported.
*
* This includes quotes.
*
* @var CssValue<string>
* @readonly
*/
private $url;
/**
* @var CssValue<string>|null
* @readonly
*/
private $modifiers;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param CssValue<string> $url
* @param FileSpan $span
* @param CssValue<string>|null $modifiers
*/
public function __construct(CssValue $url, FileSpan $span, ?CssValue $modifiers = null)
{
$this->url = $url;
$this->modifiers = $modifiers;
$this->span = $span;
}
public function getUrl(): CssValue
{
return $this->url;
}
public function getModifiers(): ?CssValue
{
return $this->modifiers;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept($visitor)
{
return $visitor->visitCssImport($this);
}
}

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\Ast\Css;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* A modifiable version of {@see CssKeyframeBlock} for use in the evaluation step.
*
* @internal
*/
final class ModifiableCssKeyframeBlock extends ModifiableCssParentNode implements CssKeyframeBlock
{
/**
* @var CssValue<list<string>>
* @readonly
*/
private $selector;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param CssValue<list<string>> $selector
* @param FileSpan $span
*/
public function __construct(CssValue $selector, FileSpan $span)
{
parent::__construct();
$this->selector = $selector;
$this->span = $span;
}
public function getSelector(): CssValue
{
return $this->selector;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept($visitor)
{
return $visitor->visitCssKeyframeBlock($this);
}
/**
* @phpstan-return ModifiableCssKeyframeBlock
*/
public function copyWithoutChildren(): ModifiableCssParentNode
{
return new ModifiableCssKeyframeBlock($this->selector, $this->span);
}
}

View File

@@ -0,0 +1,65 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* A modifiable version of {@see CssMediaRule} for use in the evaluation step.
*
* @internal
*/
final class ModifiableCssMediaRule extends ModifiableCssParentNode implements CssMediaRule
{
/**
* @var list<CssMediaQuery>
*/
private $queries;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param CssMediaQuery[] $queries
* @param FileSpan $span
*/
public function __construct(array $queries, FileSpan $span)
{
parent::__construct();
$this->queries = $queries;
$this->span = $span;
}
public function getQueries(): array
{
return $this->queries;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept($visitor)
{
return $visitor->visitCssMediaRule($this);
}
public function copyWithoutChildren(): ModifiableCssParentNode
{
return new ModifiableCssMediaRule($this->queries, $this->span);
}
}

View File

@@ -0,0 +1,151 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
use ScssPhp\ScssPhp\Serializer\Serializer;
use ScssPhp\ScssPhp\Visitor\ModifiableCssVisitor;
/**
* A modifiable version of {@see CssNode}.
*
* Almost all CSS nodes are the modifiable classes under the covers. However,
* modification should only be done within the evaluation step, so the
* unmodifiable types are used elsewhere to enforce that constraint.
*
* @internal
*/
abstract class ModifiableCssNode implements CssNode
{
/**
* @var ModifiableCssParentNode|null
*/
private $parent;
/**
* The index of `$this` in parent's children.
*
* This makes {@see remove} more efficient.
*
* @var int|null
*/
private $indexInParent;
/**
* @var bool
*/
private $groupEnd = false;
public function getParent(): ?ModifiableCssParentNode
{
return $this->parent;
}
protected function setParent(ModifiableCssParentNode $parent, int $indexInParent): void
{
$this->parent = $parent;
$this->indexInParent = $indexInParent;
}
public function isGroupEnd(): bool
{
return $this->groupEnd;
}
public function setGroupEnd(bool $groupEnd): void
{
$this->groupEnd = $groupEnd;
}
/**
* Whether this node has a visible sibling after it.
*/
public function hasFollowingSibling(): bool
{
$parent = $this->parent;
if ($parent === null) {
return false;
}
assert($this->indexInParent !== null);
$siblings = $parent->getChildren();
for ($i = $this->indexInParent + 1; $i < \count($siblings); $i++) {
$sibling = $siblings[$i];
if (!$sibling->isInvisible()) {
return true;
}
}
return false;
}
public function isInvisible(): bool
{
return $this->accept(new IsInvisibleVisitor(true, false));
}
public function isInvisibleOtherThanBogusCombinators(): bool
{
return $this->accept(new IsInvisibleVisitor(false, false));
}
public function isInvisibleHidingComments(): bool
{
return $this->accept(new IsInvisibleVisitor(true, true));
}
/**
* Calls the appropriate visit method on $visitor.
*
* @template T
*
* @param ModifiableCssVisitor<T> $visitor
*
* @return T
*/
abstract public function accept($visitor);
/**
* Removes $this from {@see parent}'s child list.
*
* @throws \LogicException if {@see parent} is `null`.
*/
public function remove(): void
{
$parent = $this->parent;
if ($parent === null) {
throw new \LogicException("Can't remove a node without a parent.");
}
assert($this->indexInParent !== null);
$parent->removeChildAt($this->indexInParent);
$children = $parent->getChildren();
for ($i = $this->indexInParent; $i < \count($children); $i++) {
$child = $children[$i];
assert($child->indexInParent !== null);
$child->indexInParent = $child->indexInParent - 1;
}
$this->parent = null;
$this->indexInParent = null;
}
public function __toString(): string
{
return Serializer::serialize($this, true)->getCss();
}
}

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\Ast\Css;
/**
* A modifiable version of {@see CssParentNode} for use in the evaluation step.
*
* @internal
*/
abstract class ModifiableCssParentNode extends ModifiableCssNode implements CssParentNode
{
/**
* @var list<ModifiableCssNode>
*/
private $children;
/**
* @param list<ModifiableCssNode> $children
*/
public function __construct(array $children = [])
{
$this->children = $children;
}
/**
* @return list<ModifiableCssNode>
*/
public function getChildren(): array
{
return $this->children;
}
public function isChildless(): bool
{
return false;
}
/**
* Returns a copy of $this with an empty {@see children} list.
*
* This is *not* a deep copy. If other parts of this node are modifiable,
* they are shared between the new and old nodes.
*/
abstract public function copyWithoutChildren(): ModifiableCssParentNode;
public function addChild(ModifiableCssNode $child): void
{
$child->setParent($this, \count($this->children));
$this->children[] = $child;
}
/**
* @internal
*/
public function removeChildAt(int $index): void
{
array_splice($this->children, $index, 1);
}
}

View File

@@ -0,0 +1,86 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
use ScssPhp\ScssPhp\Ast\Selector\SelectorList;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* A modifiable version of {@see CssStyleRule} for use in the evaluation step.
*
* @internal
*/
final class ModifiableCssStyleRule extends ModifiableCssParentNode implements CssStyleRule
{
/**
* @var ModifiableCssValue<SelectorList>
* @readonly
*/
private $selector;
/**
* @var SelectorList
* @readonly
*/
private $originalSelector;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param ModifiableCssValue<SelectorList> $selector
* @param FileSpan $span
* @param SelectorList|null $originalSelector
*/
public function __construct(ModifiableCssValue $selector, FileSpan $span, ?SelectorList $originalSelector = null)
{
parent::__construct();
$this->selector = $selector;
$this->originalSelector = $originalSelector ?? $selector->getValue();
$this->span = $span;
}
/**
* @phpstan-return ModifiableCssValue<SelectorList>
*/
public function getSelector(): CssValue
{
return $this->selector;
}
public function getOriginalSelector(): SelectorList
{
return $this->originalSelector;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept($visitor)
{
return $visitor->visitCssStyleRule($this);
}
/**
* @phpstan-return ModifiableCssStyleRule
*/
public function copyWithoutChildren(): ModifiableCssParentNode
{
return new ModifiableCssStyleRule($this->selector, $this->span, $this->originalSelector);
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* A modifiable version of {@see CssStylesheet} for use in the evaluation step.
*
* @internal
*/
final class ModifiableCssStylesheet extends ModifiableCssParentNode implements CssStylesheet
{
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param FileSpan $span
* @param list<ModifiableCssNode> $children
*/
public function __construct(FileSpan $span, array $children = [])
{
parent::__construct($children);
$this->span = $span;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept($visitor)
{
return $visitor->visitCssStylesheet($this);
}
/**
* @phpstan-return ModifiableCssStylesheet
*/
public function copyWithoutChildren(): ModifiableCssParentNode
{
return new ModifiableCssStylesheet($this->span);
}
}

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\Ast\Css;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* A modifiable version of {@see CssSupportsRule} for use in the evaluation step.
*
* @internal
*/
final class ModifiableCssSupportsRule extends ModifiableCssParentNode implements CssSupportsRule
{
/**
* @var CssValue<string>
* @readonly
*/
private $condition;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param CssValue<string> $condition
* @param FileSpan $span
*/
public function __construct(CssValue $condition, FileSpan $span)
{
parent::__construct();
$this->condition = $condition;
$this->span = $span;
}
public function getCondition(): CssValue
{
return $this->condition;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept($visitor)
{
return $visitor->visitCssSupportsRule($this);
}
/**
* @phpstan-return ModifiableCssSupportsRule
*/
public function copyWithoutChildren(): ModifiableCssParentNode
{
return new ModifiableCssSupportsRule($this->condition, $this->span);
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Css;
/**
* A modifiable version of {@see CssValue} for use in the evaluation step.
*
* @template T
* @template-extends CssValue<T>
*
* @internal
*/
final class ModifiableCssValue extends CssValue
{
/**
* @param T $value
*/
public function setValue($value): void
{
$this->value = $value;
}
}

View File

@@ -0,0 +1,47 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* An {@see AstNode} that just exposes a single span generated by a callback.
*
* @internal
*/
final class FakeAstNode implements AstNode
{
/**
* @var callable(): FileSpan
* @readonly
*/
private $callback;
/**
* @param callable(): FileSpan $callback
*/
public function __construct(callable $callback)
{
$this->callback = $callback;
}
public function getSpan(): FileSpan
{
return ($this->callback)();
}
public function __toString(): string
{
return '';
}
}

View File

@@ -0,0 +1,99 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Util;
use ScssPhp\ScssPhp\Util\SpanUtil;
/**
* An argument declared as part of an {@see ArgumentDeclaration}.
*
* @internal
*/
final class Argument implements SassNode, SassDeclaration
{
/**
* @var string
* @readonly
*/
private $name;
/**
* @var Expression|null
* @readonly
*/
private $defaultValue;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(string $name, FileSpan $span, ?Expression $defaultValue = null)
{
$this->name = $name;
$this->defaultValue = $defaultValue;
$this->span = $span;
}
public function getName(): string
{
return $this->name;
}
/**
* The variable name as written in the document, without underscores
* converted to hyphens and including the leading `$`.
*
* This isn't particularly efficient, and should only be used for error
* messages.
*/
public function getOriginalName(): string
{
if ($this->defaultValue === null) {
return $this->span->getText();
}
return Util::declarationName($this->span);
}
public function getNameSpan(): FileSpan
{
if ($this->defaultValue === null) {
return $this->span;
}
return SpanUtil::initialIdentifier($this->span, 1);
}
public function getDefaultValue(): ?Expression
{
return $this->defaultValue;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function __toString(): string
{
if ($this->defaultValue === null) {
return $this->name;
}
return $this->name . ': ' . $this->defaultValue;
}
}

View File

@@ -0,0 +1,228 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass;
use ScssPhp\ScssPhp\Exception\SassFormatException;
use ScssPhp\ScssPhp\Exception\SassScriptException;
use ScssPhp\ScssPhp\Logger\LoggerInterface;
use ScssPhp\ScssPhp\Parser\ScssParser;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* An argument declaration, as for a function or mixin definition.
*
* @internal
*/
final class ArgumentDeclaration implements SassNode
{
/**
* @var list<Argument>
* @readonly
*/
private $arguments;
/**
* @var string|null
* @readonly
*/
private $restArgument;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param list<Argument> $arguments
* @param FileSpan $span
* @param string|null $restArgument
*/
public function __construct(array $arguments, FileSpan $span, ?string $restArgument = null)
{
$this->arguments = $arguments;
$this->restArgument = $restArgument;
$this->span = $span;
}
public static function createEmpty(FileSpan $span): ArgumentDeclaration
{
return new self([], $span);
}
/**
* Parses an argument declaration from $contents, which should be of the
* form `@rule name(args) {`.
*
* If passed, $url is the name of the file from which $contents comes.
*
* @throws SassFormatException if parsing fails.
*/
public static function parse(string $contents, ?LoggerInterface $logger = null, ?string $url = null): ArgumentDeclaration
{
return (new ScssParser($contents, $logger, $url))->parseArgumentDeclaration();
}
public function isEmpty(): bool
{
return \count($this->arguments) === 0 && $this->restArgument === null;
}
/**
* @return list<Argument>
*/
public function getArguments(): array
{
return $this->arguments;
}
public function getRestArgument(): ?string
{
return $this->restArgument;
}
public function getSpan(): FileSpan
{
return $this->span;
}
/**
* @param int $positional
* @param array<string, mixed> $names Only keys are relevant
*
* @throws SassScriptException if $positional and $names aren't valid for this argument declaration.
*/
public function verify(int $positional, array $names): void
{
$nameUsed = 0;
foreach ($this->arguments as $i => $argument) {
if ($i < $positional) {
if (isset($names[$argument->getName()])) {
$originalName = $this->originalArgumentName($argument->getName());
throw new SassScriptException(sprintf('Argument $%s was passed both by position and by name.', $originalName));
}
} elseif (isset($names[$argument->getName()])) {
$nameUsed++;
} elseif ($argument->getDefaultValue() === null) {
$originalName = $this->originalArgumentName($argument->getName());
throw new SassScriptException(sprintf('Missing argument $%s', $originalName));
}
}
if ($this->restArgument !== null) {
return;
}
if ($positional > \count($this->arguments)) {
$message = sprintf(
'Only %d %sargument%s allowed, but %d %s passed.',
\count($this->arguments),
empty($names) ? '' : 'positional ',
\count($this->arguments) === 1 ? '' : 's',
$positional,
$positional === 1 ? 'was' : 'were'
);
throw new SassScriptException($message);
}
if ($nameUsed < \count($names)) {
$unknownNames = array_values(array_diff(array_keys($names), array_map(function ($argument) {
return $argument->getName();
}, $this->arguments)));
$lastName = array_pop($unknownNames);
$message = sprintf(
'No argument%s named $%s%s.',
$unknownNames ? 's' : '',
$unknownNames ? implode(', $', $unknownNames) . ' or $' : '',
$lastName
);
throw new SassScriptException($message);
}
}
private function originalArgumentName(string $name): string
{
if ($name === $this->restArgument) {
$text = $this->span->getText();
$lastDollar = strrpos($text, '$');
assert($lastDollar !== false);
$fromDollar = substr($text, $lastDollar);
$dot = strrpos($fromDollar, '.');
assert($dot !== false);
return substr($fromDollar, 0, $dot);
}
foreach ($this->arguments as $argument) {
if ($argument->getName() === $name) {
return $argument->getOriginalName();
}
}
throw new \InvalidArgumentException("This declaration has no argument named \"\$$name\".");
}
/**
* Returns whether $positional and $names are valid for this argument
* declaration.
*
* @param int $positional
* @param array<string, mixed> $names Only keys are relevant
*
* @return bool
*/
public function matches(int $positional, array $names): bool
{
$nameUsed = 0;
foreach ($this->arguments as $i => $argument) {
if ($i < $positional) {
if (isset($names[$argument->getName()])) {
return false;
}
} elseif (isset($names[$argument->getName()])) {
$nameUsed++;
} elseif ($argument->getDefaultValue() === null) {
return false;
}
}
if ($this->restArgument !== null) {
return true;
}
if ($positional > \count($this->arguments)) {
return false;
}
if ($nameUsed < \count($names)) {
return false;
}
return true;
}
public function __toString(): string
{
$parts = [];
foreach ($this->arguments as $arg) {
$parts[] = "\$$arg";
}
if ($this->restArgument !== null) {
$parts[] = "\$$this->restArgument...";
}
return implode(', ', $parts);
}
}

View File

@@ -0,0 +1,127 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* A set of arguments passed in to a function or mixin.
*
* @internal
*/
final class ArgumentInvocation implements SassNode
{
/**
* @var list<Expression>
* @readonly
*/
private $positional;
/**
* @var array<string, Expression>
* @readonly
*/
private $named;
/**
* @var Expression|null
* @readonly
*/
private $rest;
/**
* @var Expression|null
*/
private $keywordRest;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param list<Expression> $positional
* @param array<string, Expression> $named
* @param FileSpan $span
* @param Expression|null $rest
* @param Expression|null $keywordRest
*/
public function __construct(array $positional, array $named, FileSpan $span, ?Expression $rest = null, ?Expression $keywordRest = null)
{
assert($keywordRest === null || $rest !== null);
$this->positional = $positional;
$this->named = $named;
$this->rest = $rest;
$this->keywordRest = $keywordRest;
$this->span = $span;
}
public static function createEmpty(FileSpan $span): ArgumentInvocation
{
return new self([], [], $span);
}
public function isEmpty(): bool
{
return \count($this->positional) === 0 && \count($this->named) === 0 && $this->rest === null;
}
/**
* @return list<Expression>
*/
public function getPositional(): array
{
return $this->positional;
}
/**
* @return array<string, Expression>
*/
public function getNamed(): array
{
return $this->named;
}
public function getRest(): ?Expression
{
return $this->rest;
}
public function getKeywordRest(): ?Expression
{
return $this->keywordRest;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function __toString(): string
{
$parts = $this->positional;
foreach ($this->named as $name => $arg) {
$parts[] = "\$$name: $arg";
}
if ($this->rest !== null) {
$parts[] = "$this->rest...";
}
if ($this->keywordRest !== null) {
$parts[] = "$this->keywordRest...";
}
return '(' . implode(', ', $parts) . ')';
}
}

View File

@@ -0,0 +1,167 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass;
use ScssPhp\ScssPhp\Ast\Css\CssAtRule;
use ScssPhp\ScssPhp\Ast\Css\CssMediaRule;
use ScssPhp\ScssPhp\Ast\Css\CssParentNode;
use ScssPhp\ScssPhp\Ast\Css\CssStyleRule;
use ScssPhp\ScssPhp\Ast\Css\CssSupportsRule;
use ScssPhp\ScssPhp\Exception\SassFormatException;
use ScssPhp\ScssPhp\Logger\LoggerInterface;
use ScssPhp\ScssPhp\Parser\AtRootQueryParser;
/**
* A query for the `@at-root` rule.
*
* @internal
*/
final class AtRootQuery
{
/**
* Whether the query includes or excludes rules with the specified names.
*
* @var bool
* @readonly
*/
private $include;
/**
* The names of the rules included or excluded by this query.
*
* There are two special names. "all" indicates that all rules are included
* or excluded, and "rule" indicates style rules are included or excluded.
*
* @var string[]
* @readonly
*/
private $names;
/**
* Whether this includes or excludes *all* rules.
*
* @var bool
* @readonly
*/
private $all;
/**
* Whether this includes or excludes style rules.
*
* @var bool
* @readonly
*/
private $rule;
/**
* Parses an at-root query from $contents.
*
* If passed, $url is the name of the file from which $contents comes.
*
* @throws SassFormatException if parsing fails
*/
public static function parse(string $contents, ?LoggerInterface $logger = null, ?string $url = null): AtRootQuery
{
return (new AtRootQueryParser($contents, $logger, $url))->parse();
}
/**
* @param string[] $names
* @param bool $include
*/
public static function create(array $names, bool $include): AtRootQuery
{
return new AtRootQuery($names, $include, \in_array('all', $names, true), \in_array('rule', $names, true));
}
/**
* The default at-root query
*/
public static function getDefault(): AtRootQuery
{
return new AtRootQuery([], false, false, true);
}
/**
* @param string[] $names
* @param bool $include
* @param bool $all
* @param bool $rule
*/
private function __construct(array $names, bool $include, bool $all, bool $rule)
{
$this->include = $include;
$this->names = $names;
$this->all = $all;
$this->rule = $rule;
}
public function getInclude(): bool
{
return $this->include;
}
/**
* @return string[]
*/
public function getNames(): array
{
return $this->names;
}
/**
* Whether this excludes style rules.
*
* Note that this takes {@see include} into account.
*/
public function excludesStyleRules(): bool
{
return ($this->all || $this->rule) !== $this->include;
}
/**
* Returns whether $this excludes $node
*/
public function excludes(CssParentNode $node): bool
{
if ($this->all) {
return !$this->include;
}
if ($node instanceof CssStyleRule) {
return $this->excludesStyleRules();
}
if ($node instanceof CssMediaRule) {
return $this->excludesName('media');
}
if ($node instanceof CssSupportsRule) {
return $this->excludesName('supports');
}
if ($node instanceof CssAtRule) {
return $this->excludesName(strtolower($node->getName()->getValue()));
}
return false;
}
/**
* Returns whether $this excludes an at-rule with the given $name.
*/
public function excludesName(string $name): bool
{
return ($this->all || \in_array($name, $this->names, true)) !== $this->include;
}
}

View File

@@ -0,0 +1,18 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass;
interface CallableInvocation extends SassNode
{
public function getArguments(): ArgumentInvocation;
}

View File

@@ -0,0 +1,86 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Util\SpanUtil;
/**
* A variable configured by a `with` clause in a `@use` or `@forward` rule.
*
* @internal
*/
final class ConfiguredVariable implements SassNode, SassDeclaration
{
/**
* @var string
* @readonly
*/
private $name;
/**
* @var Expression
* @readonly
*/
private $expression;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @var bool
* @readonly
*/
private $guarded;
public function __construct(string $name, Expression $expression, FileSpan $span, bool $guarded = false)
{
$this->name = $name;
$this->expression = $expression;
$this->span = $span;
$this->guarded = $guarded;
}
public function getName(): string
{
return $this->name;
}
public function getExpression(): Expression
{
return $this->expression;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function isGuarded(): bool
{
return $this->guarded;
}
public function getNameSpan(): FileSpan
{
return SpanUtil::initialIdentifier($this->span, 1);
}
public function __toString(): string
{
return '$' . $this->name . ': ' . $this->expression . ($this->guarded ? ' !default' : '');
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* A SassScript expression in a Sass syntax tree.
*
* @internal
*/
interface Expression extends SassNode
{
/**
* @template T
* @param ExpressionVisitor<T> $visitor
* @return T
*/
public function accept(ExpressionVisitor $visitor);
}

View File

@@ -0,0 +1,148 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* A binary operator, as in `1 + 2` or `$this and $other`.
*
* @internal
*/
final class BinaryOperationExpression implements Expression
{
/**
* @var BinaryOperator::*
* @readonly
*/
private $operator;
/**
* @var Expression
* @readonly
*/
private $left;
/**
* @var Expression
* @readonly
*/
private $right;
/**
* Whether this is a dividedBy operation that may be interpreted as slash-separated numbers.
*
* @var bool
*/
private $allowsSlash = false;
/**
* @param BinaryOperator::* $operator
*/
public function __construct(string $operator, Expression $left, Expression $right)
{
$this->operator = $operator;
$this->left = $left;
$this->right = $right;
}
/**
* Creates a dividedBy operation that may be interpreted as slash-separated numbers.
*/
public static function slash(Expression $left, Expression $right): self
{
$operation = new self(BinaryOperator::DIVIDED_BY, $left, $right);
$operation->allowsSlash = true;
return $operation;
}
/**
* @return BinaryOperator::*
*/
public function getOperator(): string
{
return $this->operator;
}
public function getLeft(): Expression
{
return $this->left;
}
public function getRight(): Expression
{
return $this->right;
}
public function allowsSlash(): bool
{
return $this->allowsSlash;
}
public function getSpan(): FileSpan
{
$left = $this->left;
while ($left instanceof BinaryOperationExpression) {
$left = $left->left;
}
$right = $this->right;
while ($right instanceof BinaryOperationExpression) {
$right = $right->right;
}
$leftSpan = $left->getSpan();
$rightSpan = $right->getSpan();
return $leftSpan->expand($rightSpan);
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitBinaryOperationExpression($this);
}
public function __toString(): string
{
$buffer = '';
$leftNeedsParens = $this->left instanceof BinaryOperationExpression && BinaryOperator::getPrecedence($this->left->getOperator()) < BinaryOperator::getPrecedence($this->operator);
if ($leftNeedsParens) {
$buffer .= '(';
}
$buffer .= $this->left;
if ($leftNeedsParens) {
$buffer .= ')';
}
$buffer .= ' ';
$buffer .= $this->operator;
$buffer .= ' ';
$rightNeedsParens = $this->right instanceof BinaryOperationExpression && BinaryOperator::getPrecedence($this->right->getOperator()) <= BinaryOperator::getPrecedence($this->operator);
if ($rightNeedsParens) {
$buffer .= '(';
}
$buffer .= $this->right;
if ($rightNeedsParens) {
$buffer .= ')';
}
return $buffer;
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
/**
* @internal
*/
final class BinaryOperator
{
const SINGLE_EQUALS = '=';
const OR = 'or';
const AND = 'and';
const EQUALS = '==';
const NOT_EQUALS = '!=';
const GREATER_THAN = '>';
const GREATER_THAN_OR_EQUALS = '>=';
const LESS_THAN = '<';
const LESS_THAN_OR_EQUALS = '<=';
const PLUS = '+';
const MINUS = '-';
const TIMES = '*';
const DIVIDED_BY = '/';
const MODULO = '%';
/**
* @param BinaryOperator::* $operator
*/
public static function getPrecedence(string $operator): int
{
switch ($operator) {
case self::SINGLE_EQUALS:
return 0;
case self::OR:
return 1;
case self::AND:
return 2;
case self::EQUALS:
case self::NOT_EQUALS:
return 3;
case self::GREATER_THAN:
case self::GREATER_THAN_OR_EQUALS:
case self::LESS_THAN:
case self::LESS_THAN_OR_EQUALS:
return 4;
case self::PLUS:
case self::MINUS:
return 5;
case self::TIMES:
case self::DIVIDED_BY:
case self::MODULO:
return 6;
}
throw new \InvalidArgumentException(sprintf('Unknown operator "%s".', $operator));
}
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* A boolean literal, `true` or `false`.
*
* @internal
*/
final class BooleanExpression implements Expression
{
/**
* @var bool
* @readonly
*/
private $value;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(bool $value, FileSpan $span)
{
$this->value = $value;
$this->span = $span;
}
public function getValue(): bool
{
return $this->value;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitBooleanExpression($this);
}
public function __toString(): string
{
return $this->value ? 'true' : 'false';
}
}

View File

@@ -0,0 +1,235 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* A calculation literal.
*
* @internal
*/
final class CalculationExpression implements Expression
{
/**
* This calculation's name.
*
* @var string
* @readonly
*/
private $name;
/**
* The arguments for the calculation.
*
* @var list<Expression>
* @readonly
*/
private $arguments;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* Returns a `calc()` calculation expression.
*
* @param Expression $argument
* @param FileSpan $span
*
* @return CalculationExpression
*/
public static function calc(Expression $argument, FileSpan $span): CalculationExpression
{
return new CalculationExpression('calc', [$argument], $span);
}
/**
* Returns a `min()` calculation expression.
*
* @param list<Expression> $arguments
* @param FileSpan $span
*
* @return CalculationExpression
*/
public static function min(array $arguments, FileSpan $span): CalculationExpression
{
if (!$arguments) {
throw new \InvalidArgumentException('min() requires at least one argument.');
}
return new CalculationExpression('min', $arguments, $span);
}
/**
* Returns a `max()` calculation expression.
*
* @param list<Expression> $arguments
* @param FileSpan $span
*
* @return CalculationExpression
*/
public static function max(array $arguments, FileSpan $span): CalculationExpression
{
if (!$arguments) {
throw new \InvalidArgumentException('max() requires at least one argument.');
}
return new CalculationExpression('max', $arguments, $span);
}
/**
* Returns a `clamp()` calculation expression.
*
* @param Expression $min
* @param Expression $value
* @param Expression $max
* @param FileSpan $span
*
* @return CalculationExpression
*/
public static function clamp(Expression $min, Expression $value, Expression $max, FileSpan $span): CalculationExpression
{
return new CalculationExpression('clamp', [$min, $value, $max], $span);
}
/**
* Returns a calculation expression with the given name and arguments.
*
* Unlike the other constructors, this doesn't verify that the arguments are
* valid for the name.
*
* @param string $name
* @param list<Expression> $arguments
* @param FileSpan $span
*/
public function __construct(string $name, array $arguments, FileSpan $span)
{
self::verifyArguments($arguments);
$this->name = $name;
$this->arguments = $arguments;
$this->span = $span;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @return list<Expression>
*/
public function getArguments(): array
{
return $this->arguments;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitCalculationExpression($this);
}
/**
* @param list<Expression> $arguments
*
* @throws \InvalidArgumentException if $arguments aren't valid calculation arguments.
*/
private static function verifyArguments(array $arguments): void
{
foreach ($arguments as $argument) {
self::verify($argument);
}
}
/**
* @throws \InvalidArgumentException if $expression isn't a valid calculation argument.
*/
private static function verify(Expression $expression): void
{
if ($expression instanceof NumberExpression) {
return;
}
if ($expression instanceof CalculationExpression) {
return;
}
if ($expression instanceof VariableExpression) {
return;
}
if ($expression instanceof FunctionExpression) {
return;
}
if ($expression instanceof IfExpression) {
return;
}
if ($expression instanceof StringExpression) {
if ($expression->hasQuotes()) {
throw new \InvalidArgumentException('Invalid calculation argument.');
}
return;
}
if ($expression instanceof ParenthesizedExpression) {
self::verify($expression->getExpression());
return;
}
if ($expression instanceof BinaryOperationExpression) {
self::verify($expression->getLeft());
self::verify($expression->getRight());
if ($expression->getOperator() === BinaryOperator::PLUS) {
return;
}
if ($expression->getOperator() === BinaryOperator::MINUS) {
return;
}
if ($expression->getOperator() === BinaryOperator::TIMES) {
return;
}
if ($expression->getOperator() === BinaryOperator::DIVIDED_BY) {
return;
}
throw new \InvalidArgumentException('Invalid calculation argument.');
}
throw new \InvalidArgumentException('Invalid calculation argument.');
}
public function __toString(): string
{
return $this->name . '(' . implode(', ', $this->arguments) . ')';
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Value\SassColor;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* A color literal.
*
* @internal
*/
final class ColorExpression implements Expression
{
/**
* @var SassColor
* @readonly
*/
private $value;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(SassColor $value, FileSpan $span)
{
$this->value = $value;
$this->span = $span;
}
public function getValue(): SassColor
{
return $this->value;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitColorExpression($this);
}
public function __toString(): string
{
return (string) $this->value;
}
}

View File

@@ -0,0 +1,141 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\ArgumentInvocation;
use ScssPhp\ScssPhp\Ast\Sass\CallableInvocation;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\SassReference;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Util\SpanUtil;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* A function invocation.
*
* This may be a plain CSS function or a Sass function, but may not include
* interpolation.
*
* @internal
*/
final class FunctionExpression implements Expression, CallableInvocation, SassReference
{
/**
* The name of the function being invoked, with underscores left as-is.
*
* @var string
* @readonly
*/
private $originalName;
/**
* The arguments to pass to the function.
*
* @var ArgumentInvocation
* @readonly
*/
private $arguments;
/**
* The namespace of the function being invoked, or `null` if it's invoked
* without a namespace.
*
* @var string|null
* @readonly
*/
private $namespace;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(string $originalName, ArgumentInvocation $arguments, FileSpan $span, ?string $namespace = null)
{
$this->span = $span;
$this->originalName = $originalName;
$this->arguments = $arguments;
$this->namespace = $namespace;
}
/**
* @return string
*/
public function getOriginalName(): string
{
return $this->originalName;
}
/**
* The name of the function being invoked, with underscores converted to
* hyphens.
*
* If this function is a plain CSS function, use {@see getOriginalName} instead.
*/
public function getName(): string
{
return str_replace('_', '-', $this->originalName);
}
public function getArguments(): ArgumentInvocation
{
return $this->arguments;
}
public function getNamespace(): ?string
{
return $this->namespace;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function getNameSpan(): FileSpan
{
if ($this->namespace === null) {
return SpanUtil::initialIdentifier($this->span);
}
return SpanUtil::initialIdentifier(SpanUtil::withoutNamespace($this->span));
}
public function getNamespaceSpan(): ?FileSpan
{
if ($this->namespace === null) {
return null;
}
return SpanUtil::initialIdentifier($this->span);
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitFunctionExpression($this);
}
public function __toString(): string
{
$buffer = '';
if ($this->namespace !== null) {
$buffer .= $this->namespace . '.';
}
$buffer .= $this->originalName . $this->arguments;
return $buffer;
}
}

View File

@@ -0,0 +1,89 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\ArgumentDeclaration;
use ScssPhp\ScssPhp\Ast\Sass\ArgumentInvocation;
use ScssPhp\ScssPhp\Ast\Sass\CallableInvocation;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* A ternary expression.
*
* This is defined as a separate syntactic construct rather than a normal
* function because only one of the `$if-true` and `$if-false` arguments are
* evaluated.
*
* @internal
*/
final class IfExpression implements Expression, CallableInvocation
{
/**
* The arguments passed to `if()`.
*
* @var ArgumentInvocation
* @readonly
*/
private $arguments;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @var ArgumentDeclaration|null
*/
private static $declaration;
public function __construct(ArgumentInvocation $arguments, FileSpan $span)
{
$this->span = $span;
$this->arguments = $arguments;
}
/**
* The declaration of `if()`, as though it were a normal function.
*/
public static function getDeclaration(): ArgumentDeclaration
{
if (self::$declaration === null) {
self::$declaration = ArgumentDeclaration::parse('@function if($condition, $if-true, $if-false) {');
}
return self::$declaration;
}
public function getArguments(): ArgumentInvocation
{
return $this->arguments;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitIfExpression($this);
}
public function __toString(): string
{
return 'if' . $this->arguments;
}
}

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\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\ArgumentInvocation;
use ScssPhp\ScssPhp\Ast\Sass\CallableInvocation;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* An interpolated function invocation.
*
* This is always a plain CSS function.
*
* @internal
*/
final class InterpolatedFunctionExpression implements Expression, CallableInvocation
{
/**
* The name of the function being invoked.
*
* @var Interpolation
* @readonly
*/
private $name;
/**
* The arguments to pass to the function.
*
* @var ArgumentInvocation
* @readonly
*/
private $arguments;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(Interpolation $name, ArgumentInvocation $arguments, FileSpan $span)
{
$this->span = $span;
$this->name = $name;
$this->arguments = $arguments;
}
public function getName(): Interpolation
{
return $this->name;
}
public function getArguments(): ArgumentInvocation
{
return $this->arguments;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitInterpolatedFunctionExpression($this);
}
public function __toString(): string
{
return $this->name . $this->arguments;
}
}

View File

@@ -0,0 +1,142 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Value\ListSeparator;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* A list literal.
*
* @internal
*/
final class ListExpression implements Expression
{
/**
* @var Expression[]
* @readonly
*/
private $contents;
/**
* @var ListSeparator::*
* @readonly
*/
private $separator;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @var bool
* @readonly
*/
private $brackets;
/**
* ListExpression constructor.
*
* @param Expression[] $contents
* @param ListSeparator::* $separator
*/
public function __construct(array $contents, string $separator, FileSpan $span, bool $brackets = false)
{
$this->contents = $contents;
$this->separator = $separator;
$this->span = $span;
$this->brackets = $brackets;
}
/**
* @return Expression[]
*/
public function getContents(): array
{
return $this->contents;
}
/**
* @return ListSeparator::*
*/
public function getSeparator(): string
{
return $this->separator;
}
public function hasBrackets(): bool
{
return $this->brackets;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitListExpression($this);
}
public function __toString(): string
{
$buffer = '';
if ($this->hasBrackets()) {
$buffer .= '[';
}
$buffer .= implode($this->separator === ListSeparator::COMMA ? ', ' : ' ', array_map(function ($element) {
return $this->elementNeedsParens($element) ? "($element)" : (string) $element;
}, $this->contents));
if ($this->hasBrackets()) {
$buffer .= ']';
}
return $buffer;
}
/**
* Returns whether $expression, contained in $this, needs parentheses when
* printed as Sass source.
*/
private function elementNeedsParens(Expression $expression): bool
{
if ($expression instanceof ListExpression) {
if (\count($expression->contents) < 2) {
return false;
}
if ($expression->brackets) {
return false;
}
return $this->separator === ListSeparator::COMMA ? $expression->separator === ListSeparator::COMMA : $expression->separator !== ListSeparator::UNDECIDED;
}
if ($this->separator !== ListSeparator::SPACE) {
return false;
}
if ($expression instanceof UnaryOperationExpression) {
return $expression->getOperator() === UnaryOperator::PLUS || $expression->getOperator() === UnaryOperator::MINUS;
}
return false;
}
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* A map literal.
*
* @internal
*/
final class MapExpression implements Expression
{
/**
* @var list<array{Expression, Expression}>
* @readonly
*/
private $pairs;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param list<array{Expression, Expression}> $pairs
*/
public function __construct(array $pairs, FileSpan $span)
{
$this->pairs = $pairs;
$this->span = $span;
}
/**
* @return list<array{Expression, Expression}>
*/
public function getPairs(): array
{
return $this->pairs;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitMapExpression($this);
}
public function __toString(): string
{
return '(' . implode(', ', array_map(function ($pair) {
return $pair[0] . ': ' . $pair[1];
}, $this->pairs)) . ')';
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* A null literal.
*
* @internal
*/
final class NullExpression implements Expression
{
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(FileSpan $span)
{
$this->span = $span;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitNullExpression($this);
}
public function __toString(): string
{
return 'null';
}
}

View File

@@ -0,0 +1,76 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Value\SassNumber;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* A number literal.
*
* @internal
*/
final class NumberExpression implements Expression
{
/**
* @var float
* @readonly
*/
private $value;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @var string|null
* @readonly
*/
private $unit;
public function __construct(float $value, FileSpan $span, ?string $unit = null)
{
$this->value = $value;
$this->span = $span;
$this->unit = $unit;
}
public function getValue(): float
{
return $this->value;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function getUnit(): ?string
{
return $this->unit;
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitNumberExpression($this);
}
public function __toString(): string
{
return (string) SassNumber::create($this->value, $this->unit);
}
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* An expression wrapped in parentheses.
*
* @internal
*/
final class ParenthesizedExpression implements Expression
{
/**
* @var Expression
* @readonly
*/
private $expression;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(Expression $expression, FileSpan $span)
{
$this->expression = $expression;
$this->span = $span;
}
public function getExpression(): Expression
{
return $this->expression;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitParenthesizedExpression($this);
}
public function __toString(): string
{
return '(' . $this->expression . ')';
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* A parent selector reference, `&`.
*
* @internal
*/
final class SelectorExpression implements Expression
{
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(FileSpan $span)
{
$this->span = $span;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitSelectorExpression($this);
}
public function __toString(): string
{
return '&';
}
}

View File

@@ -0,0 +1,182 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
use ScssPhp\ScssPhp\Parser\InterpolationBuffer;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Util\Character;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* A string literal.
*
* @internal
*/
final class StringExpression implements Expression
{
/**
* @var Interpolation
* @readonly
*/
private $text;
/**
* @var bool
* @readonly
*/
private $quotes;
public function __construct(Interpolation $text, bool $quotes = false)
{
$this->text = $text;
$this->quotes = $quotes;
}
/**
* Returns a string expression with no interpolation.
*/
public static function plain(string $text, FileSpan $span, bool $quotes = false): self
{
return new self(new Interpolation([$text], $span), $quotes);
}
/**
* Returns Sass source for a quoted string that, when evaluated, will have
* $text as its contents.
*/
public static function quoteText(string $text): string
{
$quote = self::bestQuote([$text]);
$buffer = $quote;
$buffer .= self::quoteInnerText($text, $quote, true);
$buffer .= $quote;
return $buffer;
}
public function getText(): Interpolation
{
return $this->text;
}
public function hasQuotes(): bool
{
return $this->quotes;
}
public function getSpan(): FileSpan
{
return $this->text->getSpan();
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitStringExpression($this);
}
public function asInterpolation(bool $static = false, string $quote = null): Interpolation
{
if (!$this->quotes) {
return $this->text;
}
$quote = $quote ?? self::bestQuote($this->text->getContents());
$buffer = new InterpolationBuffer();
$buffer->write($quote);
foreach ($this->text->getContents() as $value) {
if ($value instanceof Expression) {
$buffer->add($value);
} else {
$buffer->write(self::quoteInnerText($value, $quote, $static));
}
}
$buffer->write($quote);
return $buffer->buildInterpolation($this->text->getSpan());
}
private static function quoteInnerText(string $value, string $quote, bool $static = false): string
{
$buffer = '';
$length = \strlen($value);
for ($i = 0; $i < $length; $i++) {
$char = $value[$i];
if (Character::isNewline($char)) {
$buffer .= '\\a';
if ($i !== $length - 1) {
$next = $value[$i + 1];
if (Character::isWhitespace($next) || Character::isHex($next)) {
$buffer .= ' ';
}
}
} else {
if ($char === $quote || $char === '\\' || ($static && $char === '#' && $i < $length - 1 && $value[$i + 1] === '{')) {
$buffer .= '\\';
}
if (\ord($char) < 0x80) {
$buffer .= $char;
} else {
if (!preg_match('/./usA', $value, $m, 0, $i)) {
throw new \UnexpectedValueException('Invalid UTF-8 char');
}
$buffer .= $m[0];
$i += \strlen($m[0]) - 1; // skip over the extra bytes that have been processed.
}
}
}
return $buffer;
}
/**
* @param array<string|Expression> $parts
*
* @return string
*/
private static function bestQuote(array $parts): string
{
$containsDoubleQuote = false;
foreach ($parts as $part) {
if (!\is_string($part)) {
continue;
}
if (false !== strpos($part, "'")) {
return '"';
}
if (false !== strpos($part, '"')) {
$containsDoubleQuote = true;
}
}
return $containsDoubleQuote ? "'": '"';
}
public function __toString(): string
{
return (string) $this->asInterpolation();
}
}

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\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* An expression-level `@supports` condition.
*
* This appears only in the modifiers that come after a plain-CSS `@import`. It
* doesn't include the function name wrapping the condition.
*
* @internal
*/
final class SupportsExpression implements Expression
{
/**
* @var SupportsCondition
* @readonly
*/
private $condition;
public function __construct(SupportsCondition $condition)
{
$this->condition = $condition;
}
public function getCondition(): SupportsCondition
{
return $this->condition;
}
public function getSpan(): FileSpan
{
return $this->condition->getSpan();
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitSupportsExpression($this);
}
public function __toString(): string
{
return (string) $this->condition;
}
}

View File

@@ -0,0 +1,87 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* A unary operator, as in `+$var` or `not fn()`.
*
* @internal
*/
final class UnaryOperationExpression implements Expression
{
/**
* @var UnaryOperator::*
* @readonly
*/
private $operator;
/**
* @var Expression
* @readonly
*/
private $operand;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param UnaryOperator::* $operator
*/
public function __construct(string $operator, Expression $operand, FileSpan $span)
{
$this->operator = $operator;
$this->operand = $operand;
$this->span = $span;
}
/**
* @return UnaryOperator::*
*/
public function getOperator()
{
return $this->operator;
}
public function getOperand(): Expression
{
return $this->operand;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitUnaryOperationExpression($this);
}
public function __toString(): string
{
$buffer = $this->operator;
if ($this->operator === UnaryOperator::NOT) {
$buffer .= ' ';
}
$buffer .= $this->operand;
return $buffer;
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
/**
* @internal
*/
final class UnaryOperator
{
const PLUS = '+';
const MINUS = '-';
const DIVIDE = '/';
const NOT = 'not';
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Value\Value;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* An expression that directly embeds a value.
*
* This is never constructed by the parser. It's only used when ASTs are
* constructed dynamically, as for the `call()` function.
*
* @internal
*/
final class ValueExpression implements Expression
{
/**
* @var Value
* @readonly
*/
private $value;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(Value $value, FileSpan $span)
{
$this->value = $value;
$this->span = $span;
}
public function getValue(): Value
{
return $this->value;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitValueExpression($this);
}
public function __toString(): string
{
return (string) $this->value;
}
}

View File

@@ -0,0 +1,104 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\SassReference;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Util\SpanUtil;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* A Sass variable.
*
* @internal
*/
final class VariableExpression implements Expression, SassReference
{
/**
* The name of this variable, with underscores converted to hyphens.
*
* @var string
* @readonly
*/
private $name;
/**
* The namespace of the variable being referenced, or `null` if it's
* referenced without a namespace.
*
* @var string|null
* @readonly
*/
private $namespace;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(string $name, FileSpan $span, ?string $namespace = null)
{
$this->span = $span;
$this->name = $name;
$this->namespace = $namespace;
}
public function getName(): string
{
return $this->name;
}
public function getNamespace(): ?string
{
return $this->namespace;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function getNameSpan(): FileSpan
{
if ($this->namespace === null) {
return $this->span;
}
return SpanUtil::withoutNamespace($this->span);
}
public function getNamespaceSpan(): ?FileSpan
{
if ($this->namespace === null) {
return null;
}
return SpanUtil::initialIdentifier($this->span);
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitVariableExpression($this);
}
public function __toString(): string
{
if ($this->namespace === null) {
return '$' . $this->name;
}
return $this->namespace . '$' . $this->name;
}
}

View File

@@ -0,0 +1,22 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass;
/**
* An interface for different types of import.
*
* @internal
*/
interface Import extends SassNode
{
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Import;
use ScssPhp\ScssPhp\Ast\Sass\Expression\StringExpression;
use ScssPhp\ScssPhp\Ast\Sass\Import;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* An import that will load a Sass file at runtime.
*
* @internal
*/
final class DynamicImport implements Import
{
/**
* The URI of the file to import.
*
* If this is relative, it's relative to the containing file.
*
* @var string
* @readonly
*/
private $urlString;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(string $urlString, FileSpan $span)
{
$this->urlString = $urlString;
$this->span = $span;
}
public function getUrlString(): string
{
return $this->urlString;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function __toString(): string
{
return StringExpression::quoteText($this->urlString);
}
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Import;
use ScssPhp\ScssPhp\Ast\Sass\Import;
use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* An import that produces a plain CSS `@import` rule.
*
* @internal
*/
final class StaticImport implements Import
{
/**
* The URL for this import.
*
* This already contains quotes.
*
* @var Interpolation
* @readonly
*/
private $url;
/**
* The modifiers (such as media or supports queries) attached to this import,
* or `null` if none are attached.
*
* @var Interpolation|null
* @readonly
*/
private $modifiers;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(Interpolation $url, FileSpan $span, ?Interpolation $modifiers = null)
{
$this->url = $url;
$this->span = $span;
$this->modifiers = $modifiers;
}
public function getUrl(): Interpolation
{
return $this->url;
}
public function getModifiers(): ?Interpolation
{
return $this->modifiers;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function __toString(): string
{
$buffer = (string) $this->url;
if ($this->modifiers !== null) {
$buffer .= ' ' . $this->modifiers;
}
return $buffer;
}
}

View File

@@ -0,0 +1,138 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass;
use ScssPhp\ScssPhp\Parser\InterpolationBuffer;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* Plain text interpolated with Sass expressions.
*
* @internal
*/
final class Interpolation implements SassNode
{
/**
* @var list<string|Expression>
* @readonly
*/
private $contents;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* Creates a new {@see Interpolation} by concatenating a sequence of strings,
* {@see Expression}s, or nested {@see Interpolation}s.
*
* @param array<string|Expression|Interpolation> $contents
*/
public static function concat(array $contents, FileSpan $span): Interpolation
{
$buffer = new InterpolationBuffer();
foreach ($contents as $element) {
if (\is_string($element)) {
$buffer->write($element);
} elseif ($element instanceof Expression) {
$buffer->add($element);
} elseif ($element instanceof Interpolation) {
$buffer->addInterpolation($element);
} else {
throw new \InvalidArgumentException(sprintf('The elements in $contents may only contains strings, Expressions, or Interpolations, "%s" given.', \is_object($element) ? get_class($element) : gettype($element)));
}
}
return $buffer->buildInterpolation($span);
}
/**
* @param list<string|Expression> $contents
*/
public function __construct(array $contents, FileSpan $span)
{
for ($i = 0; $i < \count($contents); $i++) {
if (!\is_string($contents[$i]) && !$contents[$i] instanceof Expression) {
throw new \TypeError('The contents of an Interpolation may only contain strings or Expression instances.');
}
if ($i != 0 && \is_string($contents[$i]) && \is_string($contents[$i - 1])) {
throw new \InvalidArgumentException('The contents of an Interpolation may not contain adjacent strings.');
}
}
$this->contents = $contents;
$this->span = $span;
}
/**
* @return list<string|Expression>
*/
public function getContents(): array
{
return $this->contents;
}
public function getSpan(): FileSpan
{
return $this->span;
}
/**
* If this contains no interpolated expressions, returns its text contents.
*
* Otherwise, returns `null`.
*
* @psalm-mutation-free
*/
public function getAsPlain(): ?string
{
if (\count($this->contents) === 0) {
return '';
}
if (\count($this->contents) > 1) {
return null;
}
if (\is_string($this->contents[0])) {
return $this->contents[0];
}
return null;
}
/**
* Returns the plain text before the interpolation, or the empty string.
*/
public function getInitialPlain(): string
{
$first = $this->contents[0] ?? null;
if (\is_string($first)) {
return $first;
}
return '';
}
public function __toString(): string
{
return implode('', array_map(function ($value) {
return \is_string($value) ? $value : '#{' . $value .'}';
}, $this->contents));
}
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* A common interface for any node that declares a Sass member.
*
* @internal
*/
interface SassDeclaration extends SassNode
{
/**
* The name of the declaration, with underscores converted to hyphens.
*
* This does not include the `$` for variables.
*/
public function getName(): string;
/**
* The span containing this declaration's name.
*
* This includes the `$` for variables.
*/
public function getNameSpan(): FileSpan;
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass;
use ScssPhp\ScssPhp\Ast\AstNode;
/**
* A node in the abstract syntax tree for an unevaluated Sass file.
*
* @internal
*/
interface SassNode extends AstNode
{
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* A common interface for any node that references a Sass member.
*
* @internal
*/
interface SassReference extends SassNode
{
/**
* The namespace of the member being referenced, or `null` if it's referenced
* without a namespace.
*/
public function getNamespace(): ?string;
/**
* The name of the member being referenced, with underscores converted to
* hyphens.
*
* This does not include the `$` for variables.
*/
public function getName(): string;
/**
* The span containing this reference's name.
*
* For variables, this should include the `$`.
*/
public function getNameSpan(): FileSpan;
/**
* The span containing this reference's namespace, null if {@see getNamespace} is
* null.
*/
public function getNamespaceSpan(): ?FileSpan;
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A statement in a Sass syntax tree.
*
* @internal
*/
interface Statement extends SassNode
{
/**
* @template T
* @param StatementVisitor<T> $visitor
* @return T
*/
public function accept(StatementVisitor $visitor);
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A `@at-root` rule.
*
* This moves it contents "up" the tree through parent nodes.
*
* @extends ParentStatement<Statement[]>
*
* @internal
*/
final class AtRootRule extends ParentStatement
{
/**
* @var Interpolation|null
* @readonly
*/
private $query;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param Statement[] $children
*/
public function __construct(array $children, FileSpan $span, ?Interpolation $query = null)
{
$this->query = $query;
$this->span = $span;
parent::__construct($children);
}
/**
* The query specifying which statements this should move its contents through.
*/
public function getQuery(): ?Interpolation
{
return $this->query;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitAtRootRule($this);
}
public function __toString(): string
{
$buffer = '@at-root ';
if ($this->query !== null) {
$buffer .= $this->query . ' ';
}
return $buffer . '{' . implode(' ', $this->getChildren()) . '}';
}
}

View File

@@ -0,0 +1,93 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* An unknown at-rule.
*
* @extends ParentStatement<Statement[]|null>
*
* @internal
*/
final class AtRule extends ParentStatement
{
/**
* @var Interpolation
* @readonly
*/
private $name;
/**
* @var Interpolation|null
* @readonly
*/
private $value;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param Statement[]|null $children
*/
public function __construct(Interpolation $name, FileSpan $span, ?Interpolation $value = null, ?array $children = null)
{
$this->name = $name;
$this->value = $value;
$this->span = $span;
parent::__construct($children);
}
public function getName(): Interpolation
{
return $this->name;
}
public function getValue(): ?Interpolation
{
return $this->value;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitAtRule($this);
}
public function __toString(): string
{
$buffer = '@' . $this->name;
if ($this->value !== null) {
$buffer .= ' ' . $this->value;
}
$children = $this->getChildren();
if ($children === null) {
return $buffer . ';';
}
return $buffer . '{' . implode(' ', $children) . '}';
}
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\ArgumentDeclaration;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
/**
* An abstract class for callables (functions or mixins) that are declared in
* user code.
*
* @extends ParentStatement<Statement[]>
*
* @internal
*/
abstract class CallableDeclaration extends ParentStatement
{
/**
* @var string
* @readonly
*/
private $name;
/**
* @var ArgumentDeclaration
* @readonly
*/
private $arguments;
/**
* @var SilentComment|null
* @readonly
*/
private $comment;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param Statement[] $children
*/
public function __construct(string $name, ArgumentDeclaration $arguments, FileSpan $span, array $children, ?SilentComment $comment = null)
{
$this->name = $name;
$this->arguments = $arguments;
$this->comment = $comment;
$this->span = $span;
parent::__construct($children);
}
/**
* The name of this callable, with underscores converted to hyphens.
*/
final public function getName(): string
{
return $this->name;
}
final public function getArguments(): ArgumentDeclaration
{
return $this->arguments;
}
/**
* @return SilentComment|null
*/
final public function getComment(): ?SilentComment
{
return $this->comment;
}
final public function getSpan(): FileSpan
{
return $this->span;
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\ArgumentDeclaration;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* An anonymous block of code that's invoked for a {@see ContentRule}.
*
* @internal
*/
final class ContentBlock extends CallableDeclaration
{
/**
* @param Statement[] $children
*/
public function __construct(ArgumentDeclaration $arguments, array $children, FileSpan $span)
{
parent::__construct('@content', $arguments, $span, $children);
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitContentBlock($this);
}
public function __toString(): string
{
$buffer = $this->getArguments()->isEmpty() ? '' : ' using (' . $this->getArguments() . ')';
return $buffer . '{' . implode(' ', $this->getChildren()) . '}';
}
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\ArgumentInvocation;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A `@content` rule.
*
* This is used in a mixin to include statement-level content passed by the
* caller.
*
* @internal
*/
final class ContentRule implements Statement
{
/**
* The arguments pass to this `@content` rule.
*
* This will be an empty invocation if `@content` has no arguments.
*
* @var ArgumentInvocation
* @readonly
*/
private $arguments;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(ArgumentInvocation $arguments, FileSpan $span)
{
$this->arguments = $arguments;
$this->span = $span;
}
public function getArguments(): ArgumentInvocation
{
return $this->arguments;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitContentRule($this);
}
public function __toString(): string
{
return $this->arguments->isEmpty() ? '@content;' : "@content($this->arguments);";
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A `@debug` rule.
*
* This prints a Sass value for debugging purposes.
*
* @internal
*/
final class DebugRule implements Statement
{
/**
* @var Expression
* @readonly
*/
private $expression;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(Expression $expression, FileSpan $span)
{
$this->expression = $expression;
$this->span = $span;
}
public function getExpression(): Expression
{
return $this->expression;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitDebugRule($this);
}
public function __toString(): string
{
return '@debug ' . $this->expression . ';';
}
}

View File

@@ -0,0 +1,143 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression\StringExpression;
use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A declaration (that is, a `name: value` pair).
*
* @extends ParentStatement<Statement[]|null>
*
* @internal
*/
final class Declaration extends ParentStatement
{
/**
* @var Interpolation
* @readonly
*/
private $name;
/**
* The value of this declaration.
*
* If {@see getChildren} is `null`, this is never `null`. Otherwise, it may or may
* not be `null`.
*
* @var Expression|null
* @readonly
*/
private $value;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param Statement[]|null $children
*/
private function __construct(Interpolation $name, ?Expression $value, FileSpan $span, ?array $children = null)
{
$this->name = $name;
$this->value = $value;
$this->span = $span;
parent::__construct($children);
}
public static function create(Interpolation $name, Expression $value, FileSpan $span): self
{
$declaration = new self($name, $value, $span);
if ($declaration->isCustomProperty() && !$value instanceof StringExpression) {
throw new \InvalidArgumentException(sprintf('Declarations whose names begin with "--" must have StringExpression values (got %s)', get_class($value)));
}
return $declaration;
}
/**
* @param Statement[] $children
*/
public static function nested(Interpolation $name, array $children, FileSpan $span, ?Expression $value = null): self
{
$declaration = new self($name, $value, $span, $children);
if ($declaration->isCustomProperty() && !$value instanceof StringExpression) {
throw new \InvalidArgumentException('Declarations whose names begin with "--" may not be nested.');
}
return $declaration;
}
public function getName(): Interpolation
{
return $this->name;
}
public function getValue(): ?Expression
{
return $this->value;
}
/**
* Returns whether this is a CSS Custom Property declaration.
*
* Note that this can return `false` for declarations that will ultimately be
* serialized as custom properties if they aren't *parsed as* custom
* properties, such as `#{--foo}: ...`.
*
* If this is `true`, then `value` will be a {@see StringExpression}.
*/
public function isCustomProperty(): bool
{
return 0 === strpos($this->name->getInitialPlain(), '--');
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitDeclaration($this);
}
public function __toString(): string
{
$buffer = $this->name . ':';
if ($this->value !== null) {
if (!$this->isCustomProperty()) {
$buffer .= ' ';
}
$buffer .= $this->value;
}
$children = $this->getChildren();
if ($children === null) {
return $buffer . ';';
}
return $buffer . '{' . implode(' ', $children) . '}';
}
}

View File

@@ -0,0 +1,88 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* An `@each` rule.
*
* This iterates over values in a list or map.
*
* @extends ParentStatement<Statement[]>
*
* @internal
*/
final class EachRule extends ParentStatement
{
/**
* @var string[]
* @readonly
*/
private $variables;
/**
* @var Expression
* @readonly
*/
private $list;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param string[] $variables
* @param Statement[] $children
*/
public function __construct(array $variables, Expression $list, array $children, FileSpan $span)
{
$this->variables = $variables;
$this->list = $list;
$this->span = $span;
parent::__construct($children);
}
/**
* @return string[]
*/
public function getVariables(): array
{
return $this->variables;
}
public function getList(): Expression
{
return $this->list;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitEachRule($this);
}
public function __toString(): string
{
return '@each ' . implode(', ', array_map(function ($variable) { return '$' . $variable; }, $this->variables)) . ' in ' . $this->list . ' {' . implode(' ', $this->getChildren()) . '}';
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
/**
* An `@else` clause in an `@if` rule.
*
* @internal
*/
final class ElseClause extends IfRuleClause
{
public function __toString(): string
{
return '@else {' . implode(' ', $this->getChildren()) . '}';
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A `@error` rule.
*
* This emits an error and stops execution.
*
* @internal
*/
final class ErrorRule implements Statement
{
/**
* @var Expression
* @readonly
*/
private $expression;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(Expression $expression, FileSpan $span)
{
$this->expression = $expression;
$this->span = $span;
}
public function getExpression(): Expression
{
return $this->expression;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitErrorRule($this);
}
public function __toString(): string
{
return '@error ' . $this->expression . ';';
}
}

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\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* An `@extend` rule.
*
* This gives one selector all the styling of another.
*
* @internal
*/
final class ExtendRule implements Statement
{
/**
* @var Interpolation
* @readonly
*/
private $selector;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @var bool
* @readonly
*/
private $optional;
public function __construct(Interpolation $selector, FileSpan $span, bool $optional = false)
{
$this->selector = $selector;
$this->span = $span;
$this->optional = $optional;
}
public function getSelector(): Interpolation
{
return $this->selector;
}
/**
* Whether this is an optional extension.
*
* If an extension isn't optional, it will emit an error if it doesn't match
* any selectors.
*/
public function isOptional(): bool
{
return $this->optional;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitExtendRule($this);
}
public function __toString(): string
{
return '@extend ' . $this->selector . ($this->optional ? ' !optional' : '') . ';';
}
}

View File

@@ -0,0 +1,111 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A `@for` rule.
*
* This iterates a set number of times.
*
* @extends ParentStatement<Statement[]>
*
* @internal
*/
final class ForRule extends ParentStatement
{
/**
* @var string
* @readonly
*/
private $variable;
/**
* @var Expression
* @readonly
*/
private $from;
/**
* @var Expression
* @readonly
*/
private $to;
/**
* @var bool
* @readonly
*/
private $exclusive;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param Statement[] $children
*/
public function __construct(string $variable, Expression $from, Expression $to, array $children, FileSpan $span, bool $exclusive = false)
{
$this->variable = $variable;
$this->from = $from;
$this->to = $to;
$this->exclusive = $exclusive;
$this->span = $span;
parent::__construct($children);
}
public function getVariable(): string
{
return $this->variable;
}
public function getFrom(): Expression
{
return $this->from;
}
public function getTo(): Expression
{
return $this->to;
}
/**
* Whether {@see getTo} is exclusive.
*/
public function isExclusive(): bool
{
return $this->exclusive;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitForRule($this);
}
public function __toString(): string
{
return '@for $' . $this->variable . ' from ' . $this->from . ($this->exclusive ? ' to ' : ' through ') . $this->to . '{' . implode(' ', $this->getChildren()) . '}';
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\SassDeclaration;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Util\SpanUtil;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A function declaration.
*
* This declares a function that's invoked using normal CSS function syntax.
*
* @internal
*/
final class FunctionRule extends CallableDeclaration implements SassDeclaration
{
public function getNameSpan(): FileSpan
{
return SpanUtil::initialIdentifier(SpanUtil::withoutInitialAtRule($this->getSpan()));
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitFunctionRule($this);
}
public function __toString(): string
{
return '@function ' . $this->getName() . '(' . $this->getArguments() . ') {' . implode(' ', $this->getChildren()) . '}';
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Visitor\StatementSearchVisitor;
/**
* A visitor for determining whether a {@see MixinRule} recursively contains a
* {@see ContentRule}.
*
* @internal
*
* @extends StatementSearchVisitor<bool>
*/
final class HasContentVisitor extends StatementSearchVisitor
{
public function visitContentRule(ContentRule $node): bool
{
return true;
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
/**
* An `@if` or `@else if` clause in an `@if` rule.
*
* @internal
*/
final class IfClause extends IfRuleClause
{
/**
* @var Expression
* @readonly
*/
private $expression;
/**
* @param Statement[] $children
*/
public function __construct(Expression $expression, array $children)
{
$this->expression = $expression;
parent::__construct($children);
}
public function getExpression(): Expression
{
return $this->expression;
}
}

View File

@@ -0,0 +1,106 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* An `@if` rule.
*
* This conditionally executes a block of code.
*
* @internal
*/
final class IfRule implements Statement
{
/**
* @var IfClause[]
* @readonly
*/
private $clauses;
/**
* @var ElseClause|null
* @readonly
*/
private $lastClause;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param IfClause[] $clauses
*/
public function __construct(array $clauses, FileSpan $span, ?ElseClause $lastClause = null)
{
$this->clauses = $clauses;
$this->span = $span;
$this->lastClause = $lastClause;
}
/**
* The `@if` and `@else if` clauses.
*
* The first clause whose expression evaluates to `true` will have its
* statements executed. If no expression evaluates to `true`, `lastClause`
* will be executed if it's not `null`.
*
* @return IfClause[]
*/
public function getClauses(): array
{
return $this->clauses;
}
/**
* The final, unconditional `@else` clause.
*
* This is `null` if there is no unconditional `@else`.
*
* @return ElseClause|null
*/
public function getLastClause(): ?ElseClause
{
return $this->lastClause;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitIfRule($this);
}
public function __toString(): string
{
$parts = [];
foreach ($this->clauses as $index => $clause) {
$parts[] = ($index === 0 ? '@if ' : '@else if ') . $clause->getExpression() . '{' . implode(' ', $clause->getChildren()) . '}';
}
if ($this->lastClause !== null) {
$parts[] = $this->lastClause;
}
return implode(' ', $parts);
}
}

View File

@@ -0,0 +1,73 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Import\DynamicImport;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
/**
* The superclass of `@if` and `@else` clauses.
*
* @internal
*/
abstract class IfRuleClause
{
/**
* @var Statement[]
* @readonly
*/
private $children;
/**
* @var bool
* @readonly
*/
private $declarations = false;
/**
* @param Statement[] $children
*/
public function __construct(array $children)
{
$this->children = $children;
foreach ($children as $child) {
if ($child instanceof VariableDeclaration || $child instanceof FunctionRule || $child instanceof MixinRule) {
$this->declarations = true;
break;
}
if ($child instanceof ImportRule) {
foreach ($child->getImports() as $import) {
if ($import instanceof DynamicImport) {
$this->declarations = true;
break 2;
}
}
}
}
}
/**
* @return Statement[]
*/
final public function getChildren(): array
{
return $this->children;
}
final public function hasDeclarations(): bool
{
return $this->declarations;
}
}

View File

@@ -0,0 +1,70 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Import;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* An `@import` rule.
*
* @internal
*/
final class ImportRule implements Statement
{
/**
* @var Import[]
* @readonly
*/
private $imports;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param Import[] $imports
*/
public function __construct(array $imports, FileSpan $span)
{
$this->imports = $imports;
$this->span = $span;
}
/**
* @return Import[]
*/
public function getImports(): array
{
return $this->imports;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitImportRule($this);
}
public function __toString(): string
{
return '@import ' . implode(', ', $this->imports) . ';';
}
}

View File

@@ -0,0 +1,149 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\ArgumentInvocation;
use ScssPhp\ScssPhp\Ast\Sass\CallableInvocation;
use ScssPhp\ScssPhp\Ast\Sass\SassReference;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Util\SpanUtil;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A mixin invocation.
*
* @internal
*/
final class IncludeRule implements Statement, CallableInvocation, SassReference
{
/**
* @var string|null
* @readonly
*/
private $namespace;
/**
* @var string
* @readonly
*/
private $name;
/**
* @var ArgumentInvocation
* @readonly
*/
private $arguments;
/**
* @var ContentBlock|null
* @readonly
*/
private $content;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(string $name, ArgumentInvocation $arguments, FileSpan $span, ?string $namespace = null,?ContentBlock $content = null)
{
$this->name = $name;
$this->arguments = $arguments;
$this->span = $span;
$this->namespace = $namespace;
$this->content = $content;
}
public function getNamespace(): ?string
{
return $this->namespace;
}
public function getName(): string
{
return $this->name;
}
public function getArguments(): ArgumentInvocation
{
return $this->arguments;
}
public function getContent(): ?ContentBlock
{
return $this->content;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function getSpanWithoutContent(): FileSpan
{
if ($this->content === null) {
return $this->span;
}
return SpanUtil::trim($this->span->getFile()->span($this->span->getStart()->getOffset(), $this->arguments->getSpan()->getEnd()->getOffset()));
}
public function getNameSpan(): FileSpan
{
$startSpan = $this->span->getText()[0] === '+' ? SpanUtil::trimLeft($this->span->subspan(1)) : SpanUtil::withoutInitialAtRule($this->span);
if ($this->namespace !== null) {
$startSpan = SpanUtil::withoutNamespace($startSpan);
}
return SpanUtil::initialIdentifier($startSpan);
}
public function getNamespaceSpan(): ?FileSpan
{
if ($this->namespace === null) {
return null;
}
$startSpan = $this->span->getText()[0] === '+'
? SpanUtil::trimLeft($this->span->subspan(1))
: SpanUtil::withoutInitialAtRule($this->span);
return SpanUtil::initialIdentifier($startSpan);
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitIncludeRule($this);
}
public function __toString(): string
{
$buffer = '@include ';
if ($this->namespace !== null) {
$buffer .= $this->namespace . '.';
}
$buffer .= $this->name;
if (!$this->arguments->isEmpty()) {
$buffer .= "($this->arguments)";
}
$buffer .= $this->content === null ? ';' : ' ' . $this->content;
return $buffer;
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A loud CSS-style comment.
*
* @internal
*/
final class LoudComment implements Statement
{
/**
* @var Interpolation
* @readonly
*/
private $text;
public function __construct(Interpolation $text)
{
$this->text = $text;
}
public function getText(): Interpolation
{
return $this->text;
}
public function getSpan(): FileSpan
{
return $this->text->getSpan();
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitLoudComment($this);
}
public function __toString(): string
{
return (string) $this->text;
}
}

View File

@@ -0,0 +1,75 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A `@media` rule.
*
* @extends ParentStatement<Statement[]>
*
* @internal
*/
final class MediaRule extends ParentStatement
{
/**
* @var Interpolation
* @readonly
*/
private $query;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param Statement[] $children
*/
public function __construct(Interpolation $query, array $children, FileSpan $span)
{
$this->query = $query;
$this->span = $span;
parent::__construct($children);
}
/**
* The query that determines on which platforms the styles will be in effect.
*
* This is only parsed after the interpolation has been resolved.
*/
public function getQuery(): Interpolation
{
return $this->query;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitMediaRule($this);
}
public function __toString(): string
{
return '@media ' . $this->query . ' {' . implode(' ', $this->getChildren()) . '}';
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\ArgumentDeclaration;
use ScssPhp\ScssPhp\Ast\Sass\SassDeclaration;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Util\SpanUtil;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A mixin declaration.
*
* This declares a mixin that's invoked using `@include`.
*
* @internal
*/
final class MixinRule extends CallableDeclaration implements SassDeclaration
{
/**
* Whether the mixin contains a `@content` rule.
*
* @var bool|null
*/
private $content;
/**
* @param Statement[] $children
*/
public function __construct(string $name, ArgumentDeclaration $arguments, FileSpan $span, array $children, ?SilentComment $comment = null)
{
parent::__construct($name, $arguments, $span, $children, $comment);
}
public function hasContent(): bool
{
if (!isset($this->content)) {
$this->content = (new HasContentVisitor())->visitMixinRule($this) === true;
}
return $this->content;
}
public function getNameSpan(): FileSpan
{
$startSpan = $this->getSpan()->getText()[0] === '='
? SpanUtil::trimLeft($this->getSpan()->subspan(1))
: SpanUtil::withoutInitialAtRule($this->getSpan());
return SpanUtil::initialIdentifier($startSpan);
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitMixinRule($this);
}
public function __toString(): string
{
$buffer = '@mixin ' . $this->getName();
if (!$this->getArguments()->isEmpty()) {
$buffer .= "({$this->getArguments()})";
}
$buffer .= ' {' . implode(' ', $this->getChildren()) . '}';
return $buffer;
}
}

View File

@@ -0,0 +1,86 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Import\DynamicImport;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
/**
* A {@see Statement} that can have child statements.
*
* This has a generic parameter so that its subclasses can choose whether or
* not their children lists are nullable.
*
* @template T
* @psalm-template T of (Statement[]|null)
*
* @internal
*/
abstract class ParentStatement implements Statement
{
/**
* @var T
* @readonly
*/
private $children;
/**
* @var bool
* @readonly
*/
private $declarations;
/**
* @param T $children
*/
public function __construct(?array $children)
{
$this->children = $children;
if ($children === null) {
$this->declarations = false;
return;
}
foreach ($children as $child) {
if ($child instanceof VariableDeclaration || $child instanceof FunctionRule || $child instanceof MixinRule) {
$this->declarations = true;
return;
}
if ($child instanceof ImportRule) {
foreach ($child->getImports() as $import) {
if ($import instanceof DynamicImport) {
$this->declarations = true;
return;
}
}
}
}
$this->declarations = false;
}
/**
* @return T
*/
final public function getChildren()
{
return $this->children;
}
final public function hasDeclarations(): bool
{
return $this->declarations;
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A `@return` rule.
*
* This exits from the current function body with a return value.
*
* @internal
*/
final class ReturnRule implements Statement
{
/**
* @var Expression
* @readonly
*/
private $expression;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(Expression $expression, FileSpan $span)
{
$this->expression = $expression;
$this->span = $span;
}
public function getExpression(): Expression
{
return $this->expression;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitReturnRule($this);
}
public function __toString(): string
{
return '@return ' . $this->expression . ';';
}
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A silent Sass-style comment.
*
* @internal
*/
final class SilentComment implements Statement
{
/**
* @var string
* @readonly
*/
private $text;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(string $text, FileSpan $span)
{
$this->text = $text;
$this->span = $span;
}
public function getText(): string
{
return $this->text;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitSilentComment($this);
}
public function __toString(): string
{
return $this->text;
}
}

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\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A style rule.
*
* This applies style declarations to elements that match a given selector.
*
* @extends ParentStatement<Statement[]>
*
* @internal
*/
final class StyleRule extends ParentStatement
{
/**
* @var Interpolation
* @readonly
*/
private $selector;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param Statement[] $children
*/
public function __construct(Interpolation $selector, array $children, FileSpan $span)
{
$this->selector = $selector;
$this->span = $span;
parent::__construct($children);
}
/**
* The selector to which the declaration will be applied.
*
* This is only parsed after the interpolation has been resolved.
*/
public function getSelector(): Interpolation
{
return $this->selector;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitStyleRule($this);
}
public function __toString(): string
{
return $this->selector . ' {' . implode(' ', $this->getChildren()) . '}';
}
}

View File

@@ -0,0 +1,126 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Exception\SassFormatException;
use ScssPhp\ScssPhp\Logger\LoggerInterface;
use ScssPhp\ScssPhp\Parser\CssParser;
use ScssPhp\ScssPhp\Parser\ScssParser;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\SourceSpan\SourceFile;
use ScssPhp\ScssPhp\Syntax;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A Sass stylesheet.
*
* This is the root Sass node. It contains top-level statements.
*
* @extends ParentStatement<Statement[]>
*
* @internal
*/
final class Stylesheet extends ParentStatement
{
/**
* @var bool
* @readonly
*/
private $plainCss;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param Statement[] $children
*/
public function __construct(array $children, FileSpan $span, bool $plainCss = false)
{
$this->span = $span;
$this->plainCss = $plainCss;
parent::__construct($children);
}
public function isPlainCss(): bool
{
return $this->plainCss;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitStylesheet($this);
}
/**
* @param Syntax::* $syntax
*
* @throws SassFormatException when parsing fails
*/
public static function parse(string $contents, string $syntax, ?LoggerInterface $logger = null, ?string $sourceUrl = null): self
{
switch ($syntax) {
case Syntax::SASS:
return self::parseSass($contents, $logger, $sourceUrl);
case Syntax::SCSS:
return self::parseScss($contents, $logger, $sourceUrl);
case Syntax::CSS:
return self::parseCss($contents, $logger, $sourceUrl);
default:
throw new \InvalidArgumentException("Unknown syntax $syntax.");
}
}
/**
* @throws SassFormatException when parsing fails
*/
public static function parseSass(string $contents, ?LoggerInterface $logger = null, ?string $sourceUrl = null): self
{
$file = new SourceFile($contents, $sourceUrl);
$span = $file->span(0, 0);
throw new SassFormatException('The Sass indented syntax is not implemented.', $span);
}
/**
* @throws SassFormatException when parsing fails
*/
public static function parseScss(string $contents, ?LoggerInterface $logger = null, ?string $sourceUrl = null): self
{
return (new ScssParser($contents, $logger, $sourceUrl))->parse();
}
/**
* @throws SassFormatException when parsing fails
*/
public static function parseCss(string $contents, ?LoggerInterface $logger = null, ?string $sourceUrl = null): self
{
return (new CssParser($contents, $logger, $sourceUrl))->parse();
}
public function __toString(): string
{
return implode(' ', $this->getChildren());
}
}

View File

@@ -0,0 +1,70 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A `@supports` rule.
*
* @extends ParentStatement<Statement[]>
*
* @internal
*/
final class SupportsRule extends ParentStatement
{
/**
* @var SupportsCondition
* @readonly
*/
private $condition;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param Statement[] $children
*/
public function __construct(SupportsCondition $condition, array $children, FileSpan $span)
{
$this->condition = $condition;
$this->span = $span;
parent::__construct($children);
}
public function getCondition(): SupportsCondition
{
return $this->condition;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitSupportsRule($this);
}
public function __toString(): string
{
return '@supports ' . $this->condition . ' {' . implode(' ', $this->getChildren()) . '}';
}
}

View File

@@ -0,0 +1,174 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\SassDeclaration;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Util;
use ScssPhp\ScssPhp\Util\SpanUtil;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A variable declaration.
*
* This defines or sets a variable.
*
* @internal
*/
final class VariableDeclaration implements Statement, SassDeclaration
{
/**
* @var string|null
* @readonly
*/
private $namespace;
/**
* @var string
* @readonly
*/
private $name;
/**
* @var SilentComment|null
* @readonly
*/
private $comment;
/**
* @var Expression
* @readonly
*/
private $expression;
/**
* @var bool
* @readonly
*/
private $guarded;
/**
* @var bool
* @readonly
*/
private $global;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(string $name, Expression $expression, FileSpan $span, ?string $namespace = null, bool $guarded = false, bool $global = false, ?SilentComment $comment = null)
{
$this->name = $name;
$this->expression = $expression;
$this->span = $span;
$this->namespace = $namespace;
$this->guarded = $guarded;
$this->global = $global;
$this->comment = $comment;
if ($namespace !== null && $global) {
throw new \InvalidArgumentException("Other modules' members can't be defined with !global.");
}
}
public function getNamespace(): ?string
{
return $this->namespace;
}
/**
* The name of the variable, with underscores converted to hyphens.
*/
public function getName(): string
{
return $this->name;
}
/**
* The variable name as written in the document, without underscores
* converted to hyphens and including the leading `$`.
*
* This isn't particularly efficient, and should only be used for error
* messages.
*/
public function getOriginalName(): string
{
return Util::declarationName($this->span);
}
public function getComment(): ?SilentComment
{
return $this->comment;
}
public function getExpression(): Expression
{
return $this->expression;
}
public function isGuarded(): bool
{
return $this->guarded;
}
public function isGlobal(): bool
{
return $this->global;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function getNameSpan(): FileSpan
{
$span = $this->span;
if ($this->namespace !== null) {
$span = SpanUtil::withoutNamespace($span);
}
return SpanUtil::initialIdentifier($span, 1);
}
public function getNamespaceSpan(): ?FileSpan
{
if ($this->namespace === null) {
return null;
}
return SpanUtil::initialIdentifier($this->span);
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitVariableDeclaration($this);
}
public function __toString(): string
{
$buffer = '';
if ($this->namespace !== null) {
$buffer .= $this->namespace . '.';
}
$buffer .= "\$$this->name: $this->expression;";
return $buffer;
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A `@warn` rule.
*
* This prints a Sass value—usually a string—to warn the user of something.
*
* @internal
*/
final class WarnRule implements Statement
{
/**
* @var Expression
* @readonly
*/
private $expression;
/**
* @var FileSpan
* @readonly
*/
private $span;
public function __construct(Expression $expression, FileSpan $span)
{
$this->expression = $expression;
$this->span = $span;
}
public function getExpression(): Expression
{
return $this->expression;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitWarnRule($this);
}
public function __toString(): string
{
return '@warn ' . $this->expression . ';';
}
}

View File

@@ -0,0 +1,73 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\SourceSpan\FileSpan;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
/**
* A `@while` rule.
*
* This repeatedly executes a block of code as long as a statement evaluates to
* `true`.
*
* @extends ParentStatement<Statement[]>
*
* @internal
*/
final class WhileRule extends ParentStatement
{
/**
* @var Expression
* @readonly
*/
private $condition;
/**
* @var FileSpan
* @readonly
*/
private $span;
/**
* @param Statement[] $children
*/
public function __construct(Expression $condition, array $children, FileSpan $span)
{
$this->condition = $condition;
$this->span = $span;
parent::__construct($children);
}
public function getCondition(): Expression
{
return $this->condition;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(StatementVisitor $visitor)
{
return $visitor->visitWhileRule($this);
}
public function __toString(): string
{
return '@while ' . $this->condition . ' {' . implode(' ', $this->getChildren()) . '}';
}
}

Some files were not shown because too many files have changed in this diff Show More