feat(demo): add story 1 — Sorano: Rock and Time

This commit is contained in:
2026-06-20 21:19:57 +02:00
parent 42ed59a6b3
commit 8f87155c1d
5508 changed files with 1595740 additions and 124 deletions
+20
View File
@@ -0,0 +1,20 @@
Copyright (c) 2015 Leaf Corcoran, https://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.
+71
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
## 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.
+244
View File
@@ -0,0 +1,244 @@
#!/usr/bin/env php
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
error_reporting(E_ALL);
if (version_compare(PHP_VERSION, '5.6') < 0) {
die('Requires PHP 5.6 or above');
}
include __DIR__ . '/../scss.inc.php';
use ScssPhp\ScssPhp\Compiler;
use ScssPhp\ScssPhp\Exception\SassException;
use ScssPhp\ScssPhp\OutputStyle;
use ScssPhp\ScssPhp\Parser;
use ScssPhp\ScssPhp\Version;
$style = null;
$loadPaths = [];
$dumpTree = false;
$inputFile = null;
$changeDir = false;
$encoding = false;
$sourceMap = false;
$embedSources = false;
$embedSourceMap = false;
/**
* Parse argument
*
* @param int $i
* @param string[] $options
*
* @return string|null
*/
function parseArgument(&$i, $options) {
global $argc;
global $argv;
if (! preg_match('/^(?:' . implode('|', (array) $options) . ')=?(.*)/', $argv[$i], $matches)) {
return;
}
if (strlen($matches[1])) {
return $matches[1];
}
if ($i + 1 < $argc) {
$i++;
return $argv[$i];
}
}
$arguments = [];
for ($i = 1; $i < $argc; $i++) {
if ($argv[$i] === '-?' || $argv[$i] === '-h' || $argv[$i] === '--help') {
$exe = $argv[0];
$HELP = <<<EOT
Usage: $exe [options] [input-file] [output-file]
Options include:
--help Show this message [-h, -?]
--continue-on-error [deprecated] Ignored
--debug-info [deprecated] Ignored [-g]
--dump-tree [deprecated] Dump formatted parse tree [-T]
--iso8859-1 Use iso8859-1 encoding instead of default utf-8
--line-numbers [deprecated] Ignored [--line-comments]
--load-path=PATH Set import path [-I]
--precision=N [deprecated] Ignored. (default 10) [-p]
--sourcemap Create source map file
--embed-sources Embed source file contents in source maps
--embed-source-map Embed the source map contents in CSS (default if writing to stdout)
--style=FORMAT Set the output style (compressed or expanded) [-s, -t]
--version Print the version [-v]
EOT;
exit($HELP);
}
if ($argv[$i] === '-v' || $argv[$i] === '--version') {
exit(Version::VERSION . "\n");
}
// Keep parsing --continue-on-error to avoid BC breaks for scripts using it
if ($argv[$i] === '--continue-on-error') {
// TODO report it as a warning ?
continue;
}
// Keep parsing it to avoid BC breaks for scripts using it
if ($argv[$i] === '-g' || $argv[$i] === '--debug-info') {
// TODO report it as a warning ?
continue;
}
if ($argv[$i] === '--iso8859-1') {
$encoding = 'iso8859-1';
continue;
}
// Keep parsing it to avoid BC breaks for scripts using it
if ($argv[$i] === '--line-numbers' || $argv[$i] === '--line-comments') {
// TODO report it as a warning ?
continue;
}
if ($argv[$i] === '--sourcemap') {
$sourceMap = true;
continue;
}
if ($argv[$i] === '--embed-sources') {
$embedSources = true;
continue;
}
if ($argv[$i] === '--embed-source-map') {
$embedSourceMap = true;
continue;
}
if ($argv[$i] === '-T' || $argv[$i] === '--dump-tree') {
$dumpTree = true;
continue;
}
$value = parseArgument($i, array('-t', '-s', '--style'));
if (isset($value)) {
$style = $value;
continue;
}
$value = parseArgument($i, array('-I', '--load-path'));
if (isset($value)) {
$loadPaths[] = $value;
continue;
}
// Keep parsing --precision to avoid BC breaks for scripts using it
$value = parseArgument($i, array('-p', '--precision'));
if (isset($value)) {
// TODO report it as a warning ?
continue;
}
$arguments[] = $argv[$i];
}
if (isset($arguments[0]) && file_exists($arguments[0])) {
$inputFile = $arguments[0];
$data = file_get_contents($inputFile);
} else {
$data = '';
while (! feof(STDIN)) {
$data .= fread(STDIN, 8192);
}
}
if ($dumpTree) {
$parser = new Parser($inputFile);
print_r(json_decode(json_encode($parser->parse($data)), true));
fwrite(STDERR, 'Warning: the --dump-tree option is deprecated. Use proper debugging tools instead.');
exit();
}
$scss = new Compiler();
if ($loadPaths) {
$scss->setImportPaths($loadPaths);
}
if ($style) {
if ($style === OutputStyle::COMPRESSED || $style === OutputStyle::EXPANDED) {
$scss->setOutputStyle($style);
} else {
fwrite(STDERR, "WARNING: the $style style is deprecated.\n");
$scss->setFormatter('ScssPhp\\ScssPhp\\Formatter\\' . ucfirst($style));
}
}
$outputFile = isset($arguments[1]) ? $arguments[1] : null;
$sourceMapFile = null;
if ($sourceMap) {
$sourceMapOptions = array(
'outputSourceFiles' => $embedSources,
);
if ($embedSourceMap || $outputFile === null) {
$scss->setSourceMap(Compiler::SOURCE_MAP_INLINE);
} else {
$sourceMapFile = $outputFile . '.map';
$sourceMapOptions['sourceMapWriteTo'] = $sourceMapFile;
$sourceMapOptions['sourceMapURL'] = basename($sourceMapFile);
$sourceMapOptions['sourceMapBasepath'] = getcwd();
$sourceMapOptions['sourceMapFilename'] = basename($outputFile);
$scss->setSourceMap(Compiler::SOURCE_MAP_FILE);
}
$scss->setSourceMapOptions($sourceMapOptions);
}
if ($encoding) {
$scss->setEncoding($encoding);
}
try {
$result = $scss->compileString($data, $inputFile);
} catch (SassException $e) {
fwrite(STDERR, 'Error: '.$e->getMessage()."\n");
exit(1);
}
if ($outputFile) {
file_put_contents($outputFile, $result->getCss());
if ($sourceMapFile !== null && $result->getSourceMap() !== null) {
file_put_contents($sourceMapFile, $result->getSourceMap());
}
} else {
echo $result->getCss();
}
+131
View File
@@ -0,0 +1,131 @@
{
"name": "scssphp/scssphp",
"type": "library",
"description": "scssphp is a compiler for SCSS written in PHP.",
"keywords": ["css", "stylesheet", "scss", "sass", "less"],
"homepage": "https://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": ">=8.1",
"ext-ctype": "*",
"ext-json": "*",
"ext-mbstring": "*",
"league/uri": "^7.6",
"league/uri-interfaces": "^7.6",
"scssphp/source-span": "^1.1",
"symfony/filesystem": "^5.4 || ^6.0 || ^7.0 || ^8.0"
},
"require-dev": {
"jgthms/bulma": "~0.9.4",
"jiripudil/phpstan-sealed-classes": "^1.3",
"phpstan/phpstan": "^2.1.31",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpunit/phpunit": "^9.5.6",
"sass/sass-spec": "*",
"squizlabs/php_codesniffer": "^3.13",
"symfony/phpunit-bridge": "^7.3 || ^8.0",
"symfony/polyfill-php84": "^1.33",
"symfony/var-dumper": "^6.4 || ^7.3 || ^8.0",
"thoughtbot/bourbon": "^7.0",
"twbs/bootstrap": "^5.3",
"twbs/bootstrap4": "4.6.1",
"zurb/foundation": "~6.7.0"
},
"repositories": [
{
"type": "package",
"package": {
"name": "sass/sass-spec",
"version": "2024.06.24",
"source": {
"type": "git",
"url": "https://github.com/sass/sass-spec.git",
"reference": "7ac806618da724333c60ad7b9c16b969470b9302"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sass/sass-spec/zipball/7ac806618da724333c60ad7b9c16b969470b9302",
"reference": "7ac806618da724333c60ad7b9c16b969470b9302",
"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": "jgthms/bulma",
"version": "v0.9.4",
"source": {
"type": "git",
"url": "https://github.com/jgthms/bulma.git",
"reference": "3e00a8e6d0d0e566d507328f0185ef84854effba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jgthms/bulma/zipball/3e00a8e6d0d0e566d507328f0185ef84854effba",
"reference": "3e00a8e6d0d0e566d507328f0185ef84854effba",
"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
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
if (version_compare(PHP_VERSION, '5.6') < 0) {
throw new \Exception('scssphp requires PHP 5.6 or above');
}
if (! class_exists('ScssPhp\ScssPhp\Version')) {
spl_autoload_register(function ($class) {
if (0 !== strpos($class, 'ScssPhp\ScssPhp\\')) {
// Not a ScssPhp class
return;
}
$subClass = substr($class, strlen('ScssPhp\ScssPhp\\'));
$path = __DIR__ . '/src/' . str_replace('\\', '/', $subClass) . '.php';
if (file_exists($path)) {
require $path;
}
});
}
@@ -0,0 +1,25 @@
<?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 SourceSpan\FileSpan;
/**
* A node in an abstract syntax tree.
*
* @internal
*/
interface AstNode extends \Stringable
{
public function getSpan(): FileSpan;
}
@@ -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;
}
@@ -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;
}
@@ -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\Css;
use ScssPhp\ScssPhp\StackTrace\Trace;
use ScssPhp\ScssPhp\Value\Value;
use SourceSpan\FileSpan;
/**
* 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;
/**
* A list of style rules that appeared before this declaration in the Sass
* input but after it in the CSS output.
*
* These are used to emit mixed declaration deprecation warnings during
* serialization, so we can check based on specificity whether the warnings
* are really necessary without worrying about `@extend` potentially changing
* things up.
*
* @return list<CssStyleRule>
*/
public function getInterleavedRules(): array;
/**
* The stack trace indicating where this node was created.
*
* This is used to emit interleaved declaration warnings, and only needs to be set if
* {@see getInterleavedRules} isn't empty.
*/
public function getTrace(): ?Trace;
/**
* 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;
}
@@ -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;
}
@@ -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;
}
@@ -0,0 +1,250 @@
<?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 League\Uri\Contracts\UriInterface;
use ScssPhp\ScssPhp\Exception\SassFormatException;
use ScssPhp\ScssPhp\Logger\LoggerInterface;
use ScssPhp\ScssPhp\Parser\InterpolationMap;
use ScssPhp\ScssPhp\Parser\MediaQueryParser;
use ScssPhp\ScssPhp\Util\Equatable;
/**
* A plain CSS media query, as used in `@media` and `@import`.
*
* @internal
*/
final class CssMediaQuery implements MediaQueryMergeResult, Equatable
{
/**
* The modifier, probably either "not" or "only".
*
* This may be `null` if no modifier is in use.
*/
private readonly ?string $modifier;
/**
* The media type, for example "screen" or "print".
*
* This may be `null`. If so, {@see $conditions} will not be empty.
*/
private readonly ?string $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`.
*/
private readonly bool $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>
*/
private readonly array $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, ?UriInterface $url = null, ?InterpolationMap $interpolationMap = null): array
{
return (new MediaQueryParser($contents, $logger, $url, $interpolationMap))->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.
*/
public function merge(CssMediaQuery $other): MediaQueryMergeResult
{
if (!$this->conjunction || !$other->conjunction) {
return MediaQuerySingletonMergeResult::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 MediaQuerySingletonMergeResult::empty;
}
return MediaQuerySingletonMergeResult::unrepresentable;
}
if ($this->matchesAllTypes() || $other->matchesAllTypes()) {
return MediaQuerySingletonMergeResult::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 MediaQuerySingletonMergeResult::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 MediaQuerySingletonMergeResult::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 MediaQuerySingletonMergeResult::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
);
}
public function equals(object $other): bool
{
return $other instanceof CssMediaQuery && $other->modifier === $this->modifier && $other->type === $this->type && $other->conditions === $this->conditions;
}
}
@@ -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;
}
@@ -0,0 +1,68 @@
<?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
{
/**
* The node that contains this, or `null` for the root {@see CssStylesheet} node.
*/
public function getParent(): ?CssParentNode;
/**
* 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(CssVisitor $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;
}
@@ -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;
}
@@ -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\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.
*/
public function getSelector(): SelectorList;
/**
* The selector for this rule, before any extensions were applied.
*/
public function getOriginalSelector(): SelectorList;
/**
* Whether this style rule was originally defined in a plain CSS stylesheet.
*
* @internal
*/
public function isFromPlainCss(): bool;
}
@@ -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
{
}
@@ -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;
}
@@ -0,0 +1,79 @@
<?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\Ast\Selector\Combinator;
use ScssPhp\ScssPhp\Util\Equatable;
use ScssPhp\ScssPhp\Util\EquatableUtil;
use 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. It has value equality semantics.
*
* @template-covariant T of string|\Stringable|array<string|\Stringable>|Combinator|null
*
* @internal
*/
final class CssValue implements AstNode, Equatable
{
/**
* @var T
*/
private readonly mixed $value;
private readonly FileSpan $span;
/**
* @param T $value
*/
public function __construct(mixed $value, FileSpan $span)
{
$this->value = $value;
$this->span = $span;
}
/**
* @return T
*/
public function getValue(): mixed
{
return $this->value;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function equals(object $other): bool
{
return $other instanceof CssValue && EquatableUtil::equals($this->value, $other->value);
}
public function __toString(): string
{
if ($this->value instanceof Combinator) {
return $this->value->getText();
}
if (\is_array($this->value)) {
return implode($this->value);
}
return (string) $this->value;
}
}
@@ -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\Visitor\EveryCssVisitor;
/**
* The visitor used to implement {@see CssNode::isInvisible}
*
* @internal
*/
final class IsInvisibleVisitor extends EveryCssVisitor
{
/**
* Whether to consider selectors with bogus combinators invisible.
*/
private readonly bool $includeBogus;
/**
* Whether to consider comments invisible.
*/
private readonly bool $includeComments;
public function __construct(bool $includeBogus, bool $includeComments)
{
$this->includeBogus = $includeBogus;
$this->includeComments = $includeComments;
}
public function visitCssAtRule(CssAtRule $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(CssComment $node): bool
{
return $this->includeComments && !$node->isPreserved();
}
public function visitCssStyleRule(CssStyleRule $node): bool
{
return ($this->includeBogus ? $node->getSelector()->isInvisible() : $node->getSelector()->isInvisibleOtherThanBogusCombinators()) || parent::visitCssStyleRule($node);
}
}
@@ -0,0 +1,23 @@
<?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 JiriPudil\SealedClasses\Sealed;
/**
* @internal
*/
#[Sealed(permits: [CssMediaQuery::class, MediaQuerySingletonMergeResult::class])]
interface MediaQueryMergeResult
{
}
@@ -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\Css;
/**
* @internal
*/
enum MediaQuerySingletonMergeResult implements MediaQueryMergeResult
{
case empty;
case unrepresentable;
}
@@ -0,0 +1,97 @@
<?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\Util\EquatableUtil;
use ScssPhp\ScssPhp\Visitor\ModifiableCssVisitor;
use 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 readonly CssValue $name;
/**
* @var CssValue<string>|null
*/
private readonly ?CssValue $value;
private readonly bool $childless;
private readonly FileSpan $span;
/**
* @param CssValue<string> $name
* @param CssValue<string>|null $value
*/
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(ModifiableCssVisitor $visitor)
{
return $visitor->visitCssAtRule($this);
}
public function equalsIgnoringChildren(ModifiableCssNode $other): bool
{
return $other instanceof ModifiableCssAtRule && EquatableUtil::equals($this->name, $other->name) && EquatableUtil::equals($this->value, $other->value) && $this->childless === $other->childless;
}
public function copyWithoutChildren(): ModifiableCssAtRule
{
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);
}
}
@@ -0,0 +1,54 @@
<?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\ModifiableCssVisitor;
use SourceSpan\FileSpan;
/**
* A modifiable version of {@see CssComment} for use in the evaluation step.
*
* @internal
*/
final class ModifiableCssComment extends ModifiableCssNode implements CssComment
{
private readonly string $text;
private readonly FileSpan $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(ModifiableCssVisitor $visitor)
{
return $visitor->visitCssComment($this);
}
}
@@ -0,0 +1,121 @@
<?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\StackTrace\Trace;
use ScssPhp\ScssPhp\Value\SassString;
use ScssPhp\ScssPhp\Value\Value;
use ScssPhp\ScssPhp\Visitor\ModifiableCssVisitor;
use SourceSpan\FileSpan;
/**
* A modifiable version of {@see CssDeclaration} for use in the evaluation step.
*
* @internal
*/
final class ModifiableCssDeclaration extends ModifiableCssNode implements CssDeclaration
{
/**
* @var CssValue<string>
*/
private readonly CssValue $name;
/**
* @var CssValue<Value>
*/
private readonly CssValue $value;
/**
* @var list<CssStyleRule>
*/
private readonly array $interleavedRules;
private readonly ?Trace $trace;
private readonly bool $parsedAsCustomProperty;
private readonly FileSpan $valueSpanForMap;
private readonly FileSpan $span;
/**
* @param CssValue<string> $name
* @param CssValue<Value> $value
* @param list<CssStyleRule> $interleavedRules
*/
public function __construct(CssValue $name, CssValue $value, FileSpan $span, bool $parsedAsCustomProperty, array $interleavedRules = [], ?Trace $trace = null, ?FileSpan $valueSpanForMap = null)
{
$this->name = $name;
$this->value = $value;
$this->parsedAsCustomProperty = $parsedAsCustomProperty;
$this->interleavedRules = $interleavedRules;
$this->trace = $trace;
$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_debug_type($value->getValue())));
}
}
}
public function getName(): CssValue
{
return $this->name;
}
public function getValue(): CssValue
{
return $this->value;
}
public function getInterleavedRules(): array
{
return $this->interleavedRules;
}
public function getTrace(): ?Trace
{
return $this->trace;
}
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 str_starts_with($this->name->getValue(), '--');
}
public function accept(ModifiableCssVisitor $visitor)
{
return $visitor->visitCssDeclaration($this);
}
}
@@ -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\Visitor\ModifiableCssVisitor;
use 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>
*/
private readonly CssValue $url;
/**
* @var CssValue<string>|null
*/
private readonly ?CssValue $modifiers;
private readonly FileSpan $span;
/**
* @param CssValue<string> $url
* @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(ModifiableCssVisitor $visitor)
{
return $visitor->visitCssImport($this);
}
}
@@ -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\Css;
use ScssPhp\ScssPhp\Util\EquatableUtil;
use ScssPhp\ScssPhp\Visitor\ModifiableCssVisitor;
use 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>>
*/
private readonly CssValue $selector;
private readonly FileSpan $span;
/**
* @param CssValue<list<string>> $selector
*/
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(ModifiableCssVisitor $visitor)
{
return $visitor->visitCssKeyframeBlock($this);
}
public function equalsIgnoringChildren(ModifiableCssNode $other): bool
{
return $other instanceof ModifiableCssKeyframeBlock && EquatableUtil::listEquals($this->selector->getValue(), $other->selector->getValue());
}
public function copyWithoutChildren(): ModifiableCssKeyframeBlock
{
return new ModifiableCssKeyframeBlock($this->selector, $this->span);
}
}
@@ -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\Css;
use ScssPhp\ScssPhp\Util\EquatableUtil;
use ScssPhp\ScssPhp\Visitor\ModifiableCssVisitor;
use 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 readonly array $queries;
private readonly FileSpan $span;
/**
* @param list<CssMediaQuery> $queries
*/
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(ModifiableCssVisitor $visitor)
{
return $visitor->visitCssMediaRule($this);
}
public function equalsIgnoringChildren(ModifiableCssNode $other): bool
{
return $other instanceof ModifiableCssMediaRule && EquatableUtil::listEquals($this->queries, $other->queries);
}
public function copyWithoutChildren(): ModifiableCssMediaRule
{
return new ModifiableCssMediaRule($this->queries, $this->span);
}
}
@@ -0,0 +1,152 @@
<?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
{
private ?ModifiableCssParentNode $parent = null;
/**
* The index of `$this` in parent's children.
*
* This makes {@see remove} more efficient.
*/
private ?int $indexInParent = null;
private bool $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(ModifiableCssVisitor $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;
}
/**
* @internal
*/
protected function resetParentReferences(): void
{
$this->parent = null;
$this->indexInParent = null;
}
public function __toString(): string
{
return Serializer::serialize($this, true)->css;
}
}
@@ -0,0 +1,85 @@
<?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 array $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 whether $this is equal to $other, ignoring their child nodes.
*/
abstract public function equalsIgnoringChildren(ModifiableCssNode $other): bool;
/**
* 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);
}
/**
* Destructively removes all elements from {@see children}.
*/
public function clearChildren(): void
{
foreach ($this->children as $child) {
$child->resetParentReferences();
}
$this->children = [];
}
}
@@ -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\Css;
use ScssPhp\ScssPhp\Ast\Selector\SelectorList;
use ScssPhp\ScssPhp\Util\Box;
use ScssPhp\ScssPhp\Util\EquatableUtil;
use ScssPhp\ScssPhp\Visitor\ModifiableCssVisitor;
use SourceSpan\FileSpan;
/**
* A modifiable version of {@see CssStyleRule} for use in the evaluation step.
*
* @internal
*/
final class ModifiableCssStyleRule extends ModifiableCssParentNode implements CssStyleRule
{
/**
* A reference to the modifiable selector list provided by the extension
* store, which may update it over time as new extensions are applied.
*
* @var Box<SelectorList>
*/
private readonly Box $selector;
private readonly SelectorList $originalSelector;
private readonly FileSpan $span;
private readonly bool $fromPlainCss;
/**
* @param Box<SelectorList> $selector
*/
public function __construct(Box $selector, FileSpan $span, ?SelectorList $originalSelector = null, bool $fromPlainCss = false)
{
parent::__construct();
$this->selector = $selector;
$this->originalSelector = $originalSelector ?? $selector->getValue();
$this->span = $span;
$this->fromPlainCss = $fromPlainCss;
}
public function getSelector(): SelectorList
{
return $this->selector->getValue();
}
public function getOriginalSelector(): SelectorList
{
return $this->originalSelector;
}
public function isFromPlainCss(): bool
{
return $this->fromPlainCss;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function accept(ModifiableCssVisitor $visitor)
{
return $visitor->visitCssStyleRule($this);
}
public function equalsIgnoringChildren(ModifiableCssNode $other): bool
{
return $other instanceof ModifiableCssStyleRule && EquatableUtil::equals($this->selector, $other->selector);
}
public function copyWithoutChildren(): ModifiableCssStyleRule
{
return new ModifiableCssStyleRule($this->selector, $this->span, $this->originalSelector);
}
}
@@ -0,0 +1,55 @@
<?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\ModifiableCssVisitor;
use SourceSpan\FileSpan;
/**
* A modifiable version of {@see CssStylesheet} for use in the evaluation step.
*
* @internal
*/
final class ModifiableCssStylesheet extends ModifiableCssParentNode implements CssStylesheet
{
private readonly 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(ModifiableCssVisitor $visitor)
{
return $visitor->visitCssStylesheet($this);
}
public function equalsIgnoringChildren(ModifiableCssNode $other): bool
{
return $other instanceof ModifiableCssStylesheet;
}
public function copyWithoutChildren(): ModifiableCssStylesheet
{
return new ModifiableCssStylesheet($this->span);
}
}
@@ -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\Css;
use ScssPhp\ScssPhp\Util\EquatableUtil;
use ScssPhp\ScssPhp\Visitor\ModifiableCssVisitor;
use 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>
*/
private readonly CssValue $condition;
private readonly FileSpan $span;
/**
* @param CssValue<string> $condition
*/
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(ModifiableCssVisitor $visitor)
{
return $visitor->visitCssSupportsRule($this);
}
public function equalsIgnoringChildren(ModifiableCssNode $other): bool
{
return $other instanceof ModifiableCssSupportsRule && EquatableUtil::equals($this->condition, $other->condition);
}
public function copyWithoutChildren(): ModifiableCssSupportsRule
{
return new ModifiableCssSupportsRule($this->condition, $this->span);
}
}
@@ -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;
use SourceSpan\FileSpan;
/**
* An {@see AstNode} that just exposes a single span generated by a callback.
*
* @internal
*/
final class FakeAstNode implements AstNode
{
/**
* @var \Closure(): FileSpan
*/
private readonly \Closure $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 '';
}
}
@@ -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;
use ScssPhp\ScssPhp\Util;
use ScssPhp\ScssPhp\Util\SpanUtil;
use SourceSpan\FileSpan;
/**
* An argument declared as part of an {@see ArgumentDeclaration}.
*
* @internal
*/
final class Argument implements SassNode, SassDeclaration
{
private readonly string $name;
private readonly ?Expression $defaultValue;
private readonly FileSpan $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;
}
}
@@ -0,0 +1,243 @@
<?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 League\Uri\Contracts\UriInterface;
use ScssPhp\ScssPhp\Exception\MultiSpanSassScriptException;
use ScssPhp\ScssPhp\Exception\SassFormatException;
use ScssPhp\ScssPhp\Exception\SassScriptException;
use ScssPhp\ScssPhp\Logger\LoggerInterface;
use ScssPhp\ScssPhp\Parser\ScssParser;
use ScssPhp\ScssPhp\Util\Character;
use ScssPhp\ScssPhp\Util\SpanUtil;
use ScssPhp\ScssPhp\Util\StringUtil;
use SourceSpan\FileSpan;
/**
* An argument declaration, as for a function or mixin definition.
*
* @internal
*/
final class ArgumentDeclaration implements SassNode
{
/**
* @var list<Argument>
*/
private readonly array $arguments;
private readonly ?string $restArgument;
private readonly FileSpan $span;
/**
* @param list<Argument> $arguments
*/
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, ?UriInterface $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;
}
/**
* Returns {@see $span} expanded to include an identifier immediately before the
* declaration, if possible.
*/
public function getSpanWithName(): FileSpan
{
$text = $this->span->getFile()->getText(0);
// Move backwards through any whitespace between the name and the arguments.
$i = $this->span->getStart()->getOffset() - 1;
while ($i > 0 && Character::isWhitespace($text[$i])) {
$i--;
}
// Then move backwards through the name itself.
if (!Character::isName($text[$i])) {
return $this->span;
}
$i--;
while ($i >= 0 && Character::isName($text[$i])) {
$i--;
}
// Trim because it's possible that this span is empty (for example, a mixin
// may be declared without an argument list).
return SpanUtil::trim($this->span->getFile()->span($i + 1, $this->span->getEnd()->getOffset()));
}
/**
* @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 MultiSpanSassScriptException(sprintf('Missing argument %s.', $originalName), 'invocation', ['declaration' => $this->getSpanWithName()]);
}
}
if ($this->restArgument !== null) {
return;
}
if ($positional > \count($this->arguments)) {
$message = sprintf(
'Only %d %s%s allowed, but %d %s passed.',
\count($this->arguments),
empty($names) ? '' : 'positional ',
StringUtil::pluralize('argument', \count($this->arguments)),
$positional,
StringUtil::pluralize('was', $positional, 'were')
);
throw new MultiSpanSassScriptException($message, 'invocation', ['declaration' => $this->getSpanWithName()]);
}
if ($nameUsed < \count($names)) {
$unknownNames = array_values(array_diff(array_keys($names), array_map(fn($argument) => $argument->getName(), $this->arguments)));
\assert(\count($unknownNames) > 0);
$message = sprintf(
'No %s named %s.',
StringUtil::pluralize('argument', \count($unknownNames)),
StringUtil::toSentence(array_map(fn ($name) => '$' . $name, $unknownNames), 'or')
);
throw new MultiSpanSassScriptException($message, 'invocation', ['declaration' => $this->getSpanWithName()]);
}
}
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 array<string, mixed> $names Only keys are relevant
*/
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);
}
}
@@ -0,0 +1,125 @@
<?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\Sass\Expression\ListExpression;
use ScssPhp\ScssPhp\Value\ListSeparator;
use SourceSpan\FileSpan;
/**
* A set of arguments passed in to a function or mixin.
*
* @internal
*/
final class ArgumentInvocation implements SassNode
{
/**
* @var list<Expression>
*/
private readonly array $positional;
/**
* @var array<string, Expression>
*/
private readonly array $named;
private readonly ?Expression $rest;
private readonly ?Expression $keywordRest;
private readonly FileSpan $span;
/**
* @param list<Expression> $positional
* @param array<string, Expression> $named
*/
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 = [];
foreach ($this->positional as $argument) {
$parts[] = $this->parenthesizeArgument($argument);
}
foreach ($this->named as $name => $arg) {
$parts[] = "\$$name: {$this->parenthesizeArgument($arg)}";
}
if ($this->rest !== null) {
$parts[] = "{$this->parenthesizeArgument($this->rest)}...";
}
if ($this->keywordRest !== null) {
$parts[] = "{$this->parenthesizeArgument($this->keywordRest)}...";
}
return '(' . implode(', ', $parts) . ')';
}
private function parenthesizeArgument(Expression $argument): string
{
if ($argument instanceof ListExpression && $argument->getSeparator() === ListSeparator::COMMA && !$argument->hasBrackets() && \count($argument->getContents()) > 1) {
return "($argument)";
}
return (string) $argument;
}
}
@@ -0,0 +1,155 @@
<?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 League\Uri\Contracts\UriInterface;
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;
use ScssPhp\ScssPhp\Parser\InterpolationMap;
/**
* A query for the `@at-root` rule.
*
* @internal
*/
final class AtRootQuery
{
/**
* Whether the query includes or excludes rules with the specified names.
*/
private readonly bool $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[]
*/
private readonly array $names;
/**
* Whether this includes or excludes *all* rules.
*/
private readonly bool $all;
/**
* Whether this includes or excludes style rules.
*/
private readonly bool $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, ?UriInterface $url = null, ?InterpolationMap $interpolationMap = null): AtRootQuery
{
return (new AtRootQueryParser($contents, $logger, $url, $interpolationMap))->parse();
}
/**
* @param string[] $names
*/
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
*/
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;
}
}
@@ -0,0 +1,21 @@
<?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;
/**
* @internal
*/
interface CallableInvocation extends SassNode
{
public function getArguments(): ArgumentInvocation;
}
@@ -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;
use ScssPhp\ScssPhp\Util\SpanUtil;
use SourceSpan\FileSpan;
/**
* A variable configured by a `with` clause in a `@use` or `@forward` rule.
*
* @internal
*/
final class ConfiguredVariable implements SassNode, SassDeclaration
{
private readonly string $name;
private readonly Expression $expression;
private readonly FileSpan $span;
private readonly bool $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' : '');
}
}
@@ -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);
}
@@ -0,0 +1,146 @@
<?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\Util\SpanUtil;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
use SourceSpan\FileSpan;
/**
* A binary operator, as in `1 + 2` or `$this and $other`.
*
* @internal
*/
final class BinaryOperationExpression implements Expression
{
private readonly BinaryOperator $operator;
private readonly Expression $left;
private readonly Expression $right;
/**
* Whether this is a dividedBy operation that may be interpreted as slash-separated numbers.
*/
private bool $allowsSlash = false;
public function __construct(BinaryOperator $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;
}
public function getOperator(): BinaryOperator
{
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);
}
/**
* Returns the span that covers only {@see $operator}.
*
* @internal
*/
public function getOperatorSpan(): FileSpan
{
$leftSpan = $this->left->getSpan();
$rightSpan = $this->right->getSpan();
if ($leftSpan->getFile() === $rightSpan->getFile() && $leftSpan->getEnd()->getOffset() < $rightSpan->getStart()->getOffset()) {
return SpanUtil::trim($leftSpan->getFile()->span($leftSpan->getEnd()->getOffset(), $rightSpan->getStart()->getOffset()));
}
return $this->getSpan();
}
public function accept(ExpressionVisitor $visitor)
{
return $visitor->visitBinaryOperationExpression($this);
}
public function __toString(): string
{
$buffer = '';
$leftNeedsParens = ($this->left instanceof BinaryOperationExpression && $this->left->getOperator()->getPrecedence() < $this->operator->getPrecedence()) || ($this->left instanceof ListExpression && !$this->left->hasBrackets() && \count($this->left->getContents()) > 1);
if ($leftNeedsParens) {
$buffer .= '(';
}
$buffer .= $this->left;
if ($leftNeedsParens) {
$buffer .= ')';
}
$buffer .= ' ';
$buffer .= $this->operator->getOperator();
$buffer .= ' ';
$rightNeedsParens = ($this->right instanceof BinaryOperationExpression && $this->right->getOperator()->getPrecedence() <= $this->operator->getPrecedence() && !($this->right->operator === $this->operator && $this->operator->isAssociative())) || ($this->right instanceof ListExpression && !$this->right->hasBrackets() && \count($this->right->getContents()) > 1);
if ($rightNeedsParens) {
$buffer .= '(';
}
$buffer .= $this->right;
if ($rightNeedsParens) {
$buffer .= ')';
}
return $buffer;
}
}
@@ -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\Expression;
/**
* @internal
*/
enum BinaryOperator
{
case SINGLE_EQUALS;
case OR;
case AND;
case EQUALS;
case NOT_EQUALS;
case GREATER_THAN;
case GREATER_THAN_OR_EQUALS;
case LESS_THAN;
case LESS_THAN_OR_EQUALS;
case PLUS;
case MINUS;
case TIMES;
case DIVIDED_BY;
case MODULO;
/**
* The Sass syntax for this operator
*/
public function getOperator(): string
{
return match ($this) {
self::SINGLE_EQUALS => '=',
self::OR => 'or',
self::AND => 'and',
self::EQUALS => '==',
self::NOT_EQUALS => '!=',
self::GREATER_THAN => '>',
self::GREATER_THAN_OR_EQUALS => '>=',
self::LESS_THAN => '<',
self::LESS_THAN_OR_EQUALS => '<=',
self::PLUS => '+',
self::MINUS => '-',
self::TIMES => '*',
self::DIVIDED_BY => '/',
self::MODULO => '%',
};
}
public function getPrecedence(): int
{
return match ($this) {
self::SINGLE_EQUALS => 0,
self::OR => 1,
self::AND => 2,
self::EQUALS, self::NOT_EQUALS => 3,
self::GREATER_THAN, self::GREATER_THAN_OR_EQUALS, self::LESS_THAN, self::LESS_THAN_OR_EQUALS => 4,
self::PLUS, self::MINUS => 5,
self::TIMES, self::DIVIDED_BY, self::MODULO => 6,
};
}
/**
* Whether this operation has the [associative property].
*
* [associative property]: https://en.wikipedia.org/wiki/Associative_property
*/
public function isAssociative(): bool
{
return match ($this) {
self::OR, self::AND, self::PLUS, self::TIMES => true,
default => false,
};
}
}
@@ -0,0 +1,55 @@
<?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\Visitor\ExpressionVisitor;
use SourceSpan\FileSpan;
/**
* A boolean literal, `true` or `false`.
*
* @internal
*/
final class BooleanExpression implements Expression
{
private readonly bool $value;
private readonly FileSpan $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';
}
}
@@ -0,0 +1,56 @@
<?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\Value\SassColor;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
use SourceSpan\FileSpan;
/**
* A color literal.
*
* @internal
*/
final class ColorExpression implements Expression
{
private readonly SassColor $value;
private readonly FileSpan $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;
}
}
@@ -0,0 +1,134 @@
<?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\Util\SpanUtil;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
use SourceSpan\FileSpan;
/**
* 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 converted to
* hyphens.
*
* If this function is a plain CSS function, use {@see $originalName} instead.
*/
private readonly string $name;
/**
* The name of the function being invoked, with underscores left as-is.
*/
private readonly string $originalName;
/**
* The arguments to pass to the function.
*/
private readonly ArgumentInvocation $arguments;
/**
* The namespace of the function being invoked, or `null` if it's invoked
* without a namespace.
*/
private readonly ?string $namespace;
private readonly FileSpan $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;
$this->name = str_replace('_', '-', $this->originalName);
}
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 $this->name;
}
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;
}
}
@@ -0,0 +1,79 @@
<?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\Visitor\ExpressionVisitor;
use SourceSpan\FileSpan;
/**
* 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()`.
*/
private readonly ArgumentInvocation $arguments;
private readonly FileSpan $span;
private static ?ArgumentDeclaration $declaration = null;
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;
}
}
@@ -0,0 +1,74 @@
<?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\Visitor\ExpressionVisitor;
use SourceSpan\FileSpan;
/**
* 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.
*/
private readonly Interpolation $name;
/**
* The arguments to pass to the function.
*/
private readonly ArgumentInvocation $arguments;
private readonly FileSpan $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;
}
}
@@ -0,0 +1,129 @@
<?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\Util\IterableUtil;
use ScssPhp\ScssPhp\Value\ListSeparator;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
/**
* @template-implements ExpressionVisitor<bool>
*
* @internal
*/
final class IsCalculationSafeVisitor implements ExpressionVisitor
{
public function visitBinaryOperationExpression(BinaryOperationExpression $node): bool
{
return \in_array($node->getOperator(), [BinaryOperator::TIMES, BinaryOperator::DIVIDED_BY, BinaryOperator::PLUS, BinaryOperator::MINUS], true) && ($node->getLeft()->accept($this) || $node->getRight()->accept($this));
}
public function visitBooleanExpression(BooleanExpression $node): bool
{
return false;
}
public function visitColorExpression(ColorExpression $node): bool
{
return false;
}
public function visitFunctionExpression(FunctionExpression $node): bool
{
return true;
}
public function visitInterpolatedFunctionExpression(InterpolatedFunctionExpression $node): bool
{
return true;
}
public function visitIfExpression(IfExpression $node): bool
{
return true;
}
public function visitListExpression(ListExpression $node): bool
{
return $node->getSeparator() === ListSeparator::SPACE && !$node->hasBrackets() && \count($node->getContents()) > 1 && IterableUtil::every($node->getContents(), fn(Expression $expression) => $expression->accept($this));
}
public function visitMapExpression(MapExpression $node): bool
{
return false;
}
public function visitNullExpression(NullExpression $node): bool
{
return false;
}
public function visitNumberExpression(NumberExpression $node): bool
{
return true;
}
public function visitParenthesizedExpression(ParenthesizedExpression $node): bool
{
return $node->getExpression()->accept($this);
}
public function visitSelectorExpression(SelectorExpression $node): bool
{
return false;
}
public function visitStringExpression(StringExpression $node): bool
{
if ($node->hasQuotes()) {
return false;
}
/**
* Exclude non-identifier constructs that are parsed as {@see StringExpression}s.
* We could just check if they parse as valid identifiers, but this is
* cheaper.
*/
$text = $node->getText()->getInitialPlain();
// !important
return !str_starts_with($text, '!')
// ID-style identifiers
&& !str_starts_with($text, '#')
// Unicode ranges
&& ($text[1] ?? null) !== '+'
// url()
&& ($text[3] ?? null) !== '(';
}
public function visitSupportsExpression(SupportsExpression $node): bool
{
return false;
}
public function visitUnaryOperationExpression(UnaryOperationExpression $node): bool
{
return false;
}
public function visitValueExpression(ValueExpression $node): bool
{
return false;
}
public function visitVariableExpression(VariableExpression $node): bool
{
return true;
}
}
@@ -0,0 +1,132 @@
<?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\Value\ListSeparator;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
use SourceSpan\FileSpan;
/**
* A list literal.
*
* @internal
*/
final class ListExpression implements Expression
{
/**
* @var list<Expression>
*/
private readonly array $contents;
private readonly ListSeparator $separator;
private readonly FileSpan $span;
private readonly bool $brackets;
/**
* ListExpression constructor.
*
* @param list<Expression> $contents
*/
public function __construct(array $contents, ListSeparator $separator, FileSpan $span, bool $brackets = false)
{
$this->contents = $contents;
$this->separator = $separator;
$this->span = $span;
$this->brackets = $brackets;
}
/**
* @return list<Expression>
*/
public function getContents(): array
{
return $this->contents;
}
public function getSeparator(): ListSeparator
{
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 .= '[';
} elseif (\count($this->contents) === 0 || (\count($this->contents) === 1 && $this->separator === ListSeparator::COMMA)) {
$buffer .= '(';
}
$buffer .= implode(
$this->separator === ListSeparator::COMMA ? ', ' : ' ',
array_map(fn($element) => $this->elementNeedsParens($element) ? "($element)" : (string) $element, $this->contents)
);
if ($this->hasBrackets()) {
$buffer .= ']';
} elseif (\count($this->contents) === 0) {
$buffer .= ')';
} elseif (\count($this->contents) === 1 && $this->separator === ListSeparator::COMMA) {
$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;
}
}
@@ -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\Visitor\ExpressionVisitor;
use SourceSpan\FileSpan;
/**
* A map literal.
*
* @internal
*/
final class MapExpression implements Expression
{
/**
* @var list<array{Expression, Expression}>
*/
private readonly array $pairs;
private readonly FileSpan $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(fn($pair) => $pair[0] . ': ' . $pair[1], $this->pairs)) . ')';
}
}
@@ -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\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
use SourceSpan\FileSpan;
/**
* A null literal.
*
* @internal
*/
final class NullExpression implements Expression
{
private readonly FileSpan $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';
}
}
@@ -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\Value\SassNumber;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
use SourceSpan\FileSpan;
/**
* A number literal.
*
* @internal
*/
final class NumberExpression implements Expression
{
private readonly float $value;
private readonly FileSpan $span;
private readonly ?string $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);
}
}
@@ -0,0 +1,55 @@
<?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\Visitor\ExpressionVisitor;
use SourceSpan\FileSpan;
/**
* An expression wrapped in parentheses.
*
* @internal
*/
final class ParenthesizedExpression implements Expression
{
private readonly Expression $expression;
private readonly FileSpan $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 . ')';
}
}
@@ -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\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
use SourceSpan\FileSpan;
/**
* A parent selector reference, `&`.
*
* @internal
*/
final class SelectorExpression implements Expression
{
private readonly FileSpan $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 '&';
}
}
@@ -0,0 +1,181 @@
<?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\Util\Character;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
use SourceSpan\FileSpan;
/**
* A string literal.
*
* @internal
*/
final class StringExpression implements Expression
{
private readonly Interpolation $text;
private readonly bool $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;
}
/**
* Interpolation that, when evaluated, produces the contents of this string.
*
* Unlike {@see asInterpolation}, escapes are resolved and quotes are not
* included.
* If this is a quoted string, escapes are resolved and quotes are not
* included in this text (unlike {@see asInterpolation}). If it's an unquoted
* string, escapes are *not* resolved.
*/
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
*/
private static function bestQuote(array $parts): string
{
$containsDoubleQuote = false;
foreach ($parts as $part) {
if (!\is_string($part)) {
continue;
}
if (str_contains($part, "'")) {
return '"';
}
if (str_contains($part, '"')) {
$containsDoubleQuote = true;
}
}
return $containsDoubleQuote ? "'" : '"';
}
public function __toString(): string
{
return (string) $this->asInterpolation();
}
}
@@ -0,0 +1,56 @@
<?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\Visitor\ExpressionVisitor;
use SourceSpan\FileSpan;
/**
* 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
{
private readonly SupportsCondition $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;
}
}
@@ -0,0 +1,82 @@
<?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\Visitor\ExpressionVisitor;
use SourceSpan\FileSpan;
/**
* A unary operator, as in `+$var` or `not fn()`.
*
* @internal
*/
final class UnaryOperationExpression implements Expression
{
private readonly UnaryOperator $operator;
private readonly Expression $operand;
private readonly FileSpan $span;
public function __construct(UnaryOperator $operator, Expression $operand, FileSpan $span)
{
$this->operator = $operator;
$this->operand = $operand;
$this->span = $span;
}
public function getOperator(): UnaryOperator
{
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->getOperator();
if ($this->operator === UnaryOperator::NOT) {
$buffer .= ' ';
}
$needsParens = $this->operand instanceof BinaryOperationExpression
|| $this->operand instanceof UnaryOperationExpression
|| ($this->operand instanceof ListExpression && !$this->operand->hasBrackets() && \count($this->operand->getContents()) > 1);
if ($needsParens) {
$buffer .= '(';
}
$buffer .= $this->operand;
if ($needsParens) {
$buffer .= ')';
}
return $buffer;
}
}
@@ -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\Expression;
/**
* @internal
*/
enum UnaryOperator
{
case PLUS;
case MINUS;
case DIVIDE;
case NOT;
/**
* The Sass syntax for this operator
*/
public function getOperator(): string
{
return match ($this) {
self::PLUS => '+',
self::MINUS => '-',
self::DIVIDE => '/',
self::NOT => 'not',
};
}
}
@@ -0,0 +1,59 @@
<?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\Value\Value;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
use SourceSpan\FileSpan;
/**
* 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
{
private readonly Value $value;
private readonly FileSpan $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;
}
}
@@ -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\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\SassReference;
use ScssPhp\ScssPhp\Util\SpanUtil;
use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
use SourceSpan\FileSpan;
/**
* A Sass variable.
*
* @internal
*/
final class VariableExpression implements Expression, SassReference
{
/**
* The name of this variable, with underscores converted to hyphens.
*/
private readonly string $name;
/**
* The namespace of the variable being referenced, or `null` if it's
* referenced without a namespace.
*/
private ?string $namespace;
private readonly FileSpan $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
{
return $this->span->getText();
}
}
@@ -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
{
}
@@ -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 League\Uri\Contracts\UriInterface;
use League\Uri\Uri;
use ScssPhp\ScssPhp\Ast\Sass\Expression\StringExpression;
use ScssPhp\ScssPhp\Ast\Sass\Import;
use 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.
*/
private readonly string $urlString;
private readonly FileSpan $span;
public function __construct(string $urlString, FileSpan $span)
{
$this->urlString = $urlString;
$this->span = $span;
}
public function getUrl(): UriInterface
{
return Uri::new($this->urlString);
}
public function getUrlString(): string
{
return $this->urlString;
}
public function getSpan(): FileSpan
{
return $this->span;
}
public function __toString(): string
{
return StringExpression::quoteText($this->urlString);
}
}
@@ -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\Import;
use ScssPhp\ScssPhp\Ast\Sass\Import;
use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
use 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.
*/
private readonly Interpolation $url;
/**
* The modifiers (such as media or supports queries) attached to this import,
* or `null` if none are attached.
*/
private readonly ?Interpolation $modifiers;
private readonly FileSpan $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;
}
}
@@ -0,0 +1,112 @@
<?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 SourceSpan\FileSpan;
/**
* Plain text interpolated with Sass expressions.
*
* @internal
*/
final class Interpolation implements SassNode
{
/**
* @var list<string|Expression>
*/
private readonly array $contents;
private readonly FileSpan $span;
/**
* @param list<string|Expression> $contents
*/
public function __construct(array $contents, FileSpan $span)
{
for ($i = 0; $i < \count($contents); $i++) {
// Dart-sass has a validation on the type of elements here. This is useless for us because phpstan supports union types, unlike the Dart type system
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;
}
/**
* Returns whether this contains no interpolated expressions.
*/
public function isPlain(): bool
{
return $this->getAsPlain() !== null;
}
/**
* 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(fn($value) => \is_string($value) ? $value : '#{' . $value . '}', $this->contents));
}
}
@@ -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 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;
}
@@ -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
{
}
@@ -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 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;
}
@@ -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);
}
@@ -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\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* A `@at-root` rule.
*
* This moves it contents "up" the tree through parent nodes.
*
* @extends ParentStatement<Statement[]>
*
* @internal
*/
final class AtRootRule extends ParentStatement
{
private readonly ?Interpolation $query;
private readonly FileSpan $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()) . '}';
}
}
@@ -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\Interpolation;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* An unknown at-rule.
*
* @extends ParentStatement<Statement[]|null>
*
* @internal
*/
final class AtRule extends ParentStatement
{
private readonly Interpolation $name;
private readonly ?Interpolation $value;
private readonly FileSpan $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) . '}';
}
}
@@ -0,0 +1,82 @@
<?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 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
{
private readonly string $name;
private readonly string $originalName;
private readonly ArgumentDeclaration $arguments;
private readonly ?SilentComment $comment;
private readonly FileSpan $span;
/**
* @param Statement[] $children
*/
public function __construct(string $originalName, ArgumentDeclaration $arguments, FileSpan $span, array $children, ?SilentComment $comment = null)
{
$this->originalName = $originalName;
$this->name = str_replace('_', '-', $originalName);
$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;
}
/**
* The callable's original name, without underscores converted to hyphens.
*/
public function getOriginalName(): string
{
return $this->originalName;
}
final public function getArguments(): ArgumentDeclaration
{
return $this->arguments;
}
final public function getComment(): ?SilentComment
{
return $this->comment;
}
final public function getSpan(): FileSpan
{
return $this->span;
}
}
@@ -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\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* 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()) . '}';
}
}
@@ -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\Statement;
use ScssPhp\ScssPhp\Ast\Sass\ArgumentInvocation;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* 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.
*/
private readonly ArgumentInvocation $arguments;
private readonly FileSpan $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);";
}
}
@@ -0,0 +1,58 @@
<?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\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* A `@debug` rule.
*
* This prints a Sass value for debugging purposes.
*
* @internal
*/
final class DebugRule implements Statement
{
private readonly Expression $expression;
private readonly FileSpan $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 . ';';
}
}
@@ -0,0 +1,120 @@
<?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\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* A declaration (that is, a `name: value` pair).
*
* @extends ParentStatement<Statement[]|null>
*
* @internal
*/
final class Declaration extends ParentStatement
{
private readonly Interpolation $name;
/**
* The value of this declaration.
*
* If {@see getChildren} is `null`, this is never `null`. Otherwise, it may or may
* not be `null`.
*/
private readonly ?Expression $value;
private readonly FileSpan $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
{
return new self($name, $value, $span);
}
/**
* @param Statement[] $children
*/
public static function nested(Interpolation $name, array $children, FileSpan $span, ?Expression $value = null): self
{
return new self($name, $value, $span, $children);
}
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 str_starts_with($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) . '}';
}
}
@@ -0,0 +1,79 @@
<?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\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* An `@each` rule.
*
* This iterates over values in a list or map.
*
* @extends ParentStatement<Statement[]>
*
* @internal
*/
final class EachRule extends ParentStatement
{
/**
* @var list<string>
*/
private readonly array $variables;
private readonly Expression $list;
private readonly FileSpan $span;
/**
* @param list<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 list<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(fn($variable) => '$' . $variable, $this->variables)) . ' in ' . $this->list . ' {' . implode(' ', $this->getChildren()) . '}';
}
}
@@ -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()) . '}';
}
}
@@ -0,0 +1,58 @@
<?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\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* A `@error` rule.
*
* This emits an error and stops execution.
*
* @internal
*/
final class ErrorRule implements Statement
{
private readonly Expression $expression;
private readonly FileSpan $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 . ';';
}
}
@@ -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\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* An `@extend` rule.
*
* This gives one selector all the styling of another.
*
* @internal
*/
final class ExtendRule implements Statement
{
private readonly Interpolation $selector;
private readonly FileSpan $span;
private readonly bool $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' : '') . ';';
}
}
@@ -0,0 +1,91 @@
<?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\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* A `@for` rule.
*
* This iterates a set number of times.
*
* @extends ParentStatement<Statement[]>
*
* @internal
*/
final class ForRule extends ParentStatement
{
private readonly string $variable;
private readonly Expression $from;
private readonly Expression $to;
private readonly bool $exclusive;
private readonly FileSpan $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()) . '}';
}
}
@@ -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\Util\SpanUtil;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* 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()) . '}';
}
}
@@ -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;
}
}
@@ -0,0 +1,40 @@
<?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
{
private readonly Expression $expression;
/**
* @param Statement[] $children
*/
public function __construct(Expression $expression, array $children)
{
$this->expression = $expression;
parent::__construct($children);
}
public function getExpression(): Expression
{
return $this->expression;
}
}
@@ -0,0 +1,95 @@
<?php
/**
* SCSSPHP
*
* @copyright 2012-2020 Leaf Corcoran
*
* @license http://opensource.org/licenses/MIT MIT
*
* @link http://scssphp.github.io/scssphp
*/
namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* An `@if` rule.
*
* This conditionally executes a block of code.
*
* @internal
*/
final class IfRule implements Statement
{
/**
* @var list<IfClause>
*/
private readonly array $clauses;
private readonly ?ElseClause $lastClause;
private readonly FileSpan $span;
/**
* @param list<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 list<IfClause>
*/
public function getClauses(): array
{
return $this->clauses;
}
/**
* The final, unconditional `@else` clause.
*
* This is `null` if there is no unconditional `@else`.
*/
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);
}
}
@@ -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\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Import\DynamicImport;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Util\IterableUtil;
/**
* The superclass of `@if` and `@else` clauses.
*
* @internal
*/
abstract class IfRuleClause
{
/**
* @var Statement[]
*/
private readonly array $children;
private readonly bool $declarations;
/**
* @param Statement[] $children
*/
public function __construct(array $children)
{
$this->children = $children;
$this->declarations = IterableUtil::any($children, function (Statement $child) {
if ($child instanceof VariableDeclaration || $child instanceof FunctionRule || $child instanceof MixinRule) {
return true;
}
if ($child instanceof ImportRule) {
return IterableUtil::any($child->getImports(), fn ($import) => $import instanceof DynamicImport);
}
return false;
});
}
/**
* @return Statement[]
*/
final public function getChildren(): array
{
return $this->children;
}
final public function hasDeclarations(): bool
{
return $this->declarations;
}
}
@@ -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\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Import;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* An `@import` rule.
*
* @internal
*/
final class ImportRule implements Statement
{
/**
* @var list<Import>
*/
private readonly array $imports;
private readonly FileSpan $span;
/**
* @param list<Import> $imports
*/
public function __construct(array $imports, FileSpan $span)
{
$this->imports = $imports;
$this->span = $span;
}
/**
* @return list<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) . ';';
}
}
@@ -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\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\Util\SpanUtil;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* A mixin invocation.
*
* @internal
*/
final class IncludeRule implements Statement, CallableInvocation, SassReference
{
private readonly ?string $namespace;
private readonly string $name;
private readonly string $originalName;
private readonly ArgumentInvocation $arguments;
private readonly ?ContentBlock $content;
private readonly FileSpan $span;
public function __construct(string $originalName, ArgumentInvocation $arguments, FileSpan $span, ?string $namespace = null, ?ContentBlock $content = null)
{
$this->originalName = $originalName;
$this->name = str_replace('_', '-', $originalName);
$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;
}
/**
* The original name of the mixin being invoked, without underscores
* converted to hyphens.
*/
public function getOriginalName(): string
{
return $this->originalName;
}
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;
}
}
@@ -0,0 +1,53 @@
<?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\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* A loud CSS-style comment.
*
* @internal
*/
final class LoudComment implements Statement
{
private readonly Interpolation $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;
}
}
@@ -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\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* A `@media` rule.
*
* @extends ParentStatement<Statement[]>
*
* @internal
*/
final class MediaRule extends ParentStatement
{
private readonly Interpolation $query;
private readonly FileSpan $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()) . '}';
}
}
@@ -0,0 +1,79 @@
<?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\Util\SpanUtil;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* 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.
*/
private ?bool $content = null;
/**
* @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;
}
}
@@ -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\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
*/
private readonly ?array $children;
private readonly bool $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(): ?array
{
return $this->children;
}
final public function hasDeclarations(): bool
{
return $this->declarations;
}
}
@@ -0,0 +1,58 @@
<?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\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* A `@return` rule.
*
* This exits from the current function body with a return value.
*
* @internal
*/
final class ReturnRule implements Statement
{
private readonly Expression $expression;
private readonly FileSpan $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 . ';';
}
}
@@ -0,0 +1,55 @@
<?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\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* A silent Sass-style comment.
*
* @internal
*/
final class SilentComment implements Statement
{
private readonly string $text;
private readonly FileSpan $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;
}
}
@@ -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\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* A style rule.
*
* This applies style declarations to elements that match a given selector.
*
* @extends ParentStatement<Statement[]>
*
* @internal
*/
final class StyleRule extends ParentStatement
{
private readonly Interpolation $selector;
private readonly FileSpan $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()) . '}';
}
}
@@ -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 League\Uri\Contracts\UriInterface;
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\SassParser;
use ScssPhp\ScssPhp\Parser\ScssParser;
use ScssPhp\ScssPhp\Syntax;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* A Sass stylesheet.
*
* This is the root Sass node. It contains top-level statements.
*
* @extends ParentStatement<Statement[]>
*
* @internal
*/
final class Stylesheet extends ParentStatement
{
private readonly bool $plainCss;
private readonly FileSpan $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);
}
/**
* @throws SassFormatException when parsing fails
*/
public static function parse(string $contents, Syntax $syntax, ?LoggerInterface $logger = null, ?UriInterface $sourceUrl = null): self
{
return match ($syntax) {
Syntax::SASS => self::parseSass($contents, $logger, $sourceUrl),
Syntax::SCSS => self::parseScss($contents, $logger, $sourceUrl),
Syntax::CSS => self::parseCss($contents, $logger, $sourceUrl),
};
}
/**
* @throws SassFormatException when parsing fails
*/
public static function parseSass(string $contents, ?LoggerInterface $logger = null, ?UriInterface $sourceUrl = null): self
{
return (new SassParser($contents, $logger, $sourceUrl))->parse();
}
/**
* @throws SassFormatException when parsing fails
*/
public static function parseScss(string $contents, ?LoggerInterface $logger = null, ?UriInterface $sourceUrl = null): self
{
return (new ScssParser($contents, $logger, $sourceUrl))->parse();
}
/**
* @throws SassFormatException when parsing fails
*/
public static function parseCss(string $contents, ?LoggerInterface $logger = null, ?UriInterface $sourceUrl = null): self
{
return (new CssParser($contents, $logger, $sourceUrl))->parse();
}
public function __toString(): string
{
return implode(' ', $this->getChildren());
}
}
@@ -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\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* A `@supports` rule.
*
* @extends ParentStatement<Statement[]>
*
* @internal
*/
final class SupportsRule extends ParentStatement
{
private readonly SupportsCondition $condition;
private readonly FileSpan $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()) . '}';
}
}
@@ -0,0 +1,146 @@
<?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\Util;
use ScssPhp\ScssPhp\Util\SpanUtil;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* A variable declaration.
*
* This defines or sets a variable.
*
* @internal
*/
final class VariableDeclaration implements Statement, SassDeclaration
{
private readonly ?string $namespace;
private readonly string $name;
private readonly ?SilentComment $comment;
private readonly Expression $expression;
private readonly bool $guarded;
private readonly bool $global;
private readonly FileSpan $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;
}
}
@@ -0,0 +1,58 @@
<?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\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* A `@warn` rule.
*
* This prints a Sass value—usually a string—to warn the user of something.
*
* @internal
*/
final class WarnRule implements Statement
{
private readonly Expression $expression;
private readonly FileSpan $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 . ';';
}
}
@@ -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\Sass\Statement;
use ScssPhp\ScssPhp\Ast\Sass\Expression;
use ScssPhp\ScssPhp\Ast\Sass\Statement;
use ScssPhp\ScssPhp\Visitor\StatementVisitor;
use SourceSpan\FileSpan;
/**
* 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
{
private readonly Expression $condition;
private readonly FileSpan $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