{* colors *}
<div id="colors" class="container containerPadding tabMenuContent">
+ {foreach from=$colors key=itemPrefix item=items}
+ <section>
+ <h1>{$itemPrefix}</h1>
+
+ {foreach from=$items item=colorItems}
+ <ul class="colorList">
+ {foreach from=$colorItems[items] item=colorItem}
+ {capture assign=variableName}{if $colorItems[overridePrefix]|isset}{@$colorItems[overridePrefix]}{else}{@$itemPrefix}{/if}{@$colorItem|ucfirst}{/capture}
+
+ <li>{include file='styleVariableColor' variableName=$variableName languageVariable=$colorItem}</li>
+ {/foreach}
+ </ul>
+ {/foreach}
+ </section>
+ {/foreach}
+ {*
<fieldset>
<legend>{lang}wcf.acp.style.colors.page{/lang}</legend>
- {* page *}
+ {* page *}{*
<ul class="colorList">
<li>{include file='styleVariableColor' variableName='wcfPageBackgroundColor' languageVariable='backgroundColor'}</li>
<li>{include file='styleVariableColor' variableName='wcfPageColor' languageVariable='color'}</li>
<fieldset>
<legend>{lang}wcf.acp.style.colors.content{/lang}</legend>
- {* content *}
+ {* content *}{*
<ul class="colorList">
<li>{include file='styleVariableColor' variableName='wcfContentBackgroundColor' languageVariable='backgroundColor'}</li>
<li>{include file='styleVariableColor' variableName='wcfColor' languageVariable='color'}</li>
<fieldset>
<legend>{lang}wcf.acp.style.colors.container{/lang}</legend>
- {* general *}
+ {* general *}{*
<ul class="colorList">
<li>{include file='styleVariableColor' variableName='wcfContainerBackgroundColor' languageVariable='backgroundColor'}</li>
<li>{include file='styleVariableColor' variableName='wcfContainerAccentBackgroundColor' languageVariable='accentBackgroundColor'}</li>
<fieldset>
<legend>{lang}wcf.acp.style.colors.userPanel{/lang}</legend>
- {* user panel *}
+ {* user panel *}{*
<ul class="colorList">
<li>{include file='styleVariableColor' variableName='wcfUserPanelBackgroundColor' languageVariable='backgroundColor'}</li>
<li>{include file='styleVariableColor' variableName='wcfUserPanelColor' languageVariable='color'}</li>
<fieldset>
<legend>{lang}wcf.acp.style.colors.tabular{/lang}</legend>
- {* general *}
+ {* general *}{*
<ul class="colorList">
<li>{include file='styleVariableColor' variableName='wcfTabularBoxBackgroundColor' languageVariable='backgroundColor'}</li>
<li>{include file='styleVariableColor' variableName='wcfTabularBoxColor' languageVariable='color'}</li>
<fieldset>
<legend>{lang}wcf.acp.style.colors.buttons{/lang}</legend>
- {* default button *}
+ {* default button *}{*
<ul class="colorList">
<li>{include file='styleVariableColor' variableName='wcfButtonBackgroundColor' languageVariable='backgroundColor'}</li>
<li>{include file='styleVariableColor' variableName='wcfButtonBorderColor' languageVariable='borderColor'}</li>
{event name='defaultButtonColorListItems'}
</ul>
- {* button:hover *}
+ {* button:hover *}{*
<ul class="colorList">
<li>{include file='styleVariableColor' variableName='wcfButtonHoverBackgroundColor' languageVariable='hoverBackgroundColor'}</li>
<li>{include file='styleVariableColor' variableName='wcfButtonHoverBorderColor' languageVariable='hoverBorderColor'}</li>
{event name='hoverButtonColorListItems'}
</ul>
- {* primary button *}
+ {* primary button *}{*
<ul class="colorList">
<li>{include file='styleVariableColor' variableName='wcfButtonPrimaryBackgroundColor' languageVariable='primaryBackgroundColor'}</li>
<li>{include file='styleVariableColor' variableName='wcfButtonPrimaryBorderColor' languageVariable='primaryBorderColor'}</li>
<fieldset>
<legend>{lang}wcf.acp.style.colors.formInput{/lang}</legend>
- {* form input *}
+ {* form input *}{*
<ul class="colorList">
<li>{include file='styleVariableColor' variableName='wcfInputBackgroundColor' languageVariable='backgroundColor'}</li>
<li>{include file='styleVariableColor' variableName='wcfInputBorderColor' languageVariable='borderColor'}</li>
</fieldset>
{event name='colorFieldsets'}
+ *}
</div>
{* advanced *}
<figcaption>
{lang}wcf.acp.style.colors.{$languageVariable}{/lang}
<br />
- <span class="dimmed">@{$variableName}</span>
+ <span class="dimmed">${$variableName}</span>
</figcaption>
<div class="colorPreview"><div class="jsColorPicker" style="background-color: {$variables[$variableName]}" data-color="{$variables[$variableName]}" data-store="{$variableName}_value"></div></div>
<input type="hidden" id="{$variableName}_value" name="{$variableName}" value="{$variables[$variableName]}" />
* list of available font families
* @var array<string>
*/
- public $availableFontFamilies = array(
+ public $availableFontFamilies = [
'Arial, Helvetica, sans-serif' => 'Arial',
'Chicago, Impact, Compacta, sans-serif' => 'Chicago',
'"Comic Sans MS", sans-serif' => 'Comic Sans',
'"Times New Roman", Times, Georgia, serif' => 'Times New Roman',
'"Trebuchet MS", Arial, sans-serif' => 'Trebuchet MS',
'Verdana, Helvetica, sans-serif' => 'Verdana'
- );
+ ];
/**
* list of available template groups
* @var array<\wcf\data\template\group\TemplateGroup>
*/
- public $availableTemplateGroups = array();
+ public $availableTemplateGroups = [];
/**
* list of available units
* @var array<string>
*/
- public $availableUnits = array('px', 'em', '%', 'pt');
+ public $availableUnits = ['px', 'em', '%', 'pt'];
/**
* list of color variables
* @var array<string>
*/
- public $colors = array();
+ public $colors = [];
/**
* copyright message
* list of global variables
* @var array
*/
- public $globals = array();
+ public $globals = [];
/**
* image path
/**
* @see \wcf\page\AbstractPage::$neededPermissions
*/
- public $neededPermissions = array('admin.style.canManageStyle');
+ public $neededPermissions = ['admin.style.canManageStyle'];
/**
* style package name
* list of variables and their value
* @var array<string>
*/
- public $variables = array();
+ public $variables = [];
/**
* list of specialized variables
* @var array<string>
*/
- public $specialVariables = array();
+ public $specialVariables = [];
/**
* @see \wcf\page\IPage::readParameters()
I18nHandler::getInstance()->readValues();
+ // @TODO
+ $colors = [];
+ foreach ($this->colors as $k => $v) {
+ foreach ($v as $vv) {
+ $prefix = (isset($vv['overridePrefix'])) ? $vv['overridePrefix'] : $k;
+ foreach ($vv['items'] as $vvv) {
+ $colors[] = $prefix . ucfirst($vvv);
+ }
+ }
+ }
+
// ignore everything except well-formed rgba()
$regEx = new Regex('rgba\(\d{1,3}, \d{1,3}, \d{1,3}, (1|1\.00?|0|0?\.[0-9]{1,2})\)');
- foreach ($this->colors as $variableName) {
+ foreach ($colors as $variableName) {
if (isset($_POST[$variableName]) && $regEx->match($_POST[$variableName])) {
$this->variables[$variableName] = $_POST[$variableName];
}
FROM wcf".WCF_N."_style_variable";
$statement = WCF::getDB()->prepareStatement($sql);
$statement->execute();
- $variables = array();
+ $variables = [];
while ($row = $statement->fetchArray()) {
$variables[] = $row['variableName'];
}
$lines = explode("\n", StringUtil::unifyNewlines($this->variables['overrideLess']));
$regEx = new Regex('^@([a-zA-Z]+): ?([@a-zA-Z0-9 ,\.\(\)\%\#-]+);$');
- $errors = array();
+ $errors = [];
foreach ($lines as $index => &$line) {
$line = StringUtil::trim($line);
// cannot override variables covered by style editor
if (in_array($matches[1], $this->colors) || in_array($matches[1], $this->globals) || in_array($matches[1], $this->specialVariables)) {
- $errors[] = array(
+ $errors[] = [
'error' => 'predefined',
'text' => $matches[1]
- );
+ ];
}
else if (!in_array($matches[1], $variables)) {
// unknown style variable
- $errors[] = array(
+ $errors[] = [
'error' => 'unknown',
'text' => $matches[1]
- );
+ ];
}
else {
$this->variables[$matches[1]] = $matches[2];
}
else {
// not valid
- $errors[] = array(
+ $errors[] = [
'error' => 'notValid',
'text' => $line
- );
+ ];
}
}
*/
protected function setVariables() {
// set color variables
- $this->colors = array(
+ $this->colors = [
+ 'wcfPage' => [
+ [
+ 'overridePrefix' => 'wcfHeader',
+ 'items' => ['background', 'text', 'link', 'linkActive']
+ ],
+ [
+ 'overridePrefix' => 'wcfMenu',
+ 'items' => ['background', 'backgroundActive', 'border', 'text', 'textActive', 'link', 'linkActive']
+ ],
+ [
+ 'overridePrefix' => 'wcfMenuContent',
+ 'items' => ['background', 'backgroundActive', 'border', 'text', 'textActive', 'link', 'linkActive']
+ ],
+ [
+ 'overridePrefix' => 'wcfSearchBox',
+ 'items' => ['background', 'backgroundActive', 'border', 'borderActive', 'text', 'textActive', 'link', 'linkActive']
+ ],
+ [
+ 'overridePrefix' => 'wcfHeaderBox',
+ 'items' => ['background', 'text', 'link', 'linkActive']
+ ],
+ [
+ 'overridePrefix' => 'wcfHeaderNavigation',
+ 'items' => ['background', 'text', 'link', 'linkActive']
+ ],
+ [
+ 'overridePrefix' => 'wcfFooterBox',
+ 'items' => ['background', 'text', 'link', 'linkActive']
+ ],
+ [
+ 'overridePrefix' => 'wcfFooter',
+ 'items' => ['background', 'text', 'link', 'linkActive']
+ ]
+ ],
+ 'wcfContent' => [
+ [
+ 'items' => ['background', 'text', 'link', 'linkActive', 'border'],
+ ],
+ [
+ 'overridePrefix' => 'wcfContentHeadline',
+ 'items' => ['text', 'link', 'linkActive']
+ ],
+ [
+ 'items' => ['textDimmed', 'linkDimmed', 'linkDimmedActive']
+ ],
+ [
+ 'items' => ['backgroundAccent', 'textAccent', 'linkAccent', 'linkAccentActive', 'borderAccent']
+ ]
+ ],
+ 'wcfSidebar' => [
+ [
+ 'items' => ['background', 'border']
+ ],
+ [
+ 'overridePrefix' => 'wcfSidebarBox',
+ 'items' => ['background', 'text', 'link', 'linkActive', 'border', 'textDimmed', 'linkDimmed', 'linkDimmedActive']
+ ],
+ [
+ 'overridePrefix' => 'wcfSidebarBoxHeadline',
+ 'items' => ['text', 'link', 'linkActive', 'border']
+ ]
+ ],
+ 'wcfButton' => [
+ [
+ 'items' => ['background', 'backgroundActive', 'text', 'textActive', 'border', 'borderActive']
+ ],
+ [
+ 'items' => ['backgroundAccent', 'backgroundAccentActive', 'textAccent', 'textAccentActive', 'borderAccent', 'borderAccentActive']
+ ]
+ ],
+ 'wcfInput' => [
+ [
+ 'items' => ['background', 'backgroundActive', 'border', 'borderActive', 'text', 'textActive']
+ ]
+ ],
+ 'wcfDropdown' => [
+ [
+ 'items' => ['background', 'backgroundActive', 'text', 'textActive', 'link', 'linkActive']
+ ]
+ ]
+ ];
+ /*
+ $this->colors = [
'wcfButtonBackgroundColor',
'wcfButtonBorderColor',
'wcfButtonColor',
'wcfUserPanelColor',
'wcfUserPanelHoverBackgroundColor',
'wcfUserPanelHoverColor',
- );
-
+ ];
+ */
// set global variables
- $this->globals = array(
+ $this->globals = [
'wcfBaseFontSize',
'wcfLayoutFixedWidth',
'wcfLayoutMinWidth',
'wcfLayoutMaxWidth'
- );
+ ];
// set specialized variables
- $this->specialVariables = array(
+ $this->specialVariables = [
'individualLess',
'overrideLess',
'pageLogo',
'useFluidLayout',
'wcfBaseFontFamily'
- );
+ ];
EventHandler::getInstance()->fireAction($this, 'setVariables');
}
public function save() {
parent::save();
- $this->objectAction = new StyleAction(array(), 'create', array(
- 'data' => array_merge($this->additionalFields, array(
+ $this->objectAction = new StyleAction([], 'create', [
+ 'data' => array_merge($this->additionalFields, [
'styleName' => $this->styleName,
'templateGroupID' => $this->templateGroupID,
'packageName' => $this->packageName,
'license' => $this->license,
'authorName' => $this->authorName,
'authorURL' => $this->authorURL
- )),
+ ]),
'tmpHash' => $this->tmpHash,
'variables' => $this->variables
- ));
+ ]);
$returnValues = $this->objectAction->executeAction();
$style = $returnValues['returnValues'];
I18nHandler::getInstance()->save('styleDescription', 'wcf.style.styleDescription'.$style->styleID, 'wcf.style');
$styleEditor = new StyleEditor($style);
- $styleEditor->update(array(
+ $styleEditor->update([
'styleDescription' => 'wcf.style.styleDescription'.$style->styleID
- ));
+ ]);
// call saved event
$this->saved();
I18nHandler::getInstance()->assignVariables();
- WCF::getTPL()->assign(array(
+ WCF::getTPL()->assign([
'action' => 'add',
'authorName' => $this->authorName,
'authorURL' => $this->authorURL,
'availableFontFamilies' => $this->availableFontFamilies,
'availableTemplateGroups' => $this->availableTemplateGroups,
'availableUnits' => $this->availableUnits,
+ 'colors' => $this->colors,
'copyright' => $this->copyright,
'imagePath' => $this->imagePath,
'isTainted' => $this->isTainted,
'templateGroupID' => $this->templateGroupID,
'tmpHash' => $this->tmpHash,
'variables' => $this->variables
- ));
+ ]);
}
}
*
* @param \Exception $e
*/
- public static final function handleException(\Exception $e) {
+ public static final function handleException($e) {
try {
+ if (!($e instanceof \Exception)) throw $e;
+
if ($e instanceof IPrintableException) {
$e->show();
exit;
// repack Exception
self::handleException(new SystemException($e->getMessage(), $e->getCode(), '', $e));
}
+ catch (\Throwable $exception) {
+ die("<pre>WCF::handleException() Unhandled exception: ".$exception->getMessage()."\n\n".$exception->getTraceAsString());
+ }
catch (\Exception $exception) {
die("<pre>WCF::handleException() Unhandled exception: ".$exception->getMessage()."\n\n".$exception->getTraceAsString());
}
use wcf\util\StyleUtil;
/**
- * Provides access to the LESS PHP compiler.
+ * Provides access to the SCSS PHP compiler.
*
* @author Alexander Ebert
* @copyright 2001-2015 WoltLab GmbH
*/
class StyleCompiler extends SingletonFactory {
/**
- * less compiler object
- * @var \lessc
+ * SCSS compiler object
+ * @var \Leafo\ScssPhp\Compiler
*/
protected $compiler = null;
* names of option types which are supported as additional variables
* @var array<string>
*/
- public static $supportedOptionType = array('boolean', 'integer');
+ public static $supportedOptionType = ['boolean', 'integer'];
/**
* @see \wcf\system\SingletonFactory::init()
*/
protected function init() {
- require_once(WCF_DIR.'lib/system/style/lessc.inc.php');
- $this->compiler = new \lessc();
- $this->compiler->setImportDir(array(WCF_DIR));
+ require_once(WCF_DIR.'lib/system/style/scssphp/scss.inc.php');
+ $this->compiler = new \Leafo\ScssPhp\Compiler();
+ $this->compiler->setImportPaths([WCF_DIR]);
}
/**
- * Compiles LESS stylesheets.
+ * Compiles SCSS stylesheets.
*
* @param \wcf\data\style\Style $style
*/
public function compile(Style $style) {
// read stylesheets by dependency order
$conditions = new PreparedStatementConditionBuilder();
- $conditions->add("filename REGEXP ?", ['style/([a-zA-Z0-9\_\-\.]+)\.less']);
+ $conditions->add("filename REGEXP ?", ['style/([a-zA-Z0-9\_\-\.]+)\.(less|scss)']);
// TESTING ONLY
$conditions->add("packageID <> ?", [1]);
// get style variables
$variables = $style->getVariables();
- $individualLess = '';
- if (isset($variables['individualLess'])) {
- $individualLess = $variables['individualLess'];
- unset($variables['individualLess']);
+ $individualScss = '';
+ if (isset($variables['individualScss'])) {
+ $individualScss = $variables['individualScss'];
+ unset($variables['individualScss']);
}
// add style image path
$variables['style_image_path'] = "'{$imagePath}'";
// apply overrides
- if (isset($variables['overrideLess'])) {
- $lines = explode("\n", StringUtil::unifyNewlines($variables['overrideLess']));
+ if (isset($variables['overrideScss'])) {
+ $lines = explode("\n", StringUtil::unifyNewlines($variables['overrideScss']));
foreach ($lines as $line) {
if (preg_match('~^@([a-zA-Z]+): ?([@a-zA-Z0-9 ,\.\(\)\%\#-]+);$~', $line, $matches)) {
$variables[$matches[1]] = $matches[2];
}
}
- unset($variables['overrideLess']);
+ unset($variables['overrideScss']);
}
$this->compileStylesheet(
WCF_DIR.'style/style-'.$style->styleID,
$files,
$variables,
- $individualLess,
+ $individualScss,
new Callback(function($content) use ($style) {
return "/* stylesheet for '".$style->styleName."', generated on ".gmdate('r')." -- DO NOT EDIT */\n\n" . $content;
})
}
/**
- * Compiles LESS stylesheets for ACP usage.
+ * Compiles SCSS stylesheets for ACP usage.
*/
public function compileACP() {
$files = glob(WCF_DIR.'style/*.less');
ORDER BY variableID ASC";
$statement = WCF::getDB()->prepareStatement($sql);
$statement->execute();
- $variables = array();
+ $variables = [];
while ($row = $statement->fetchArray()) {
$value = $row['defaultValue'];
if (empty($value)) {
}
// insert blue temptation files
- array_unshift($files, WCF_DIR.'acp/style/blueTemptation/variables.less', WCF_DIR.'acp/style/blueTemptation/override.less');
+ array_unshift($files, WCF_DIR.'acp/style/blueTemptation/variables.scss', WCF_DIR.'acp/style/blueTemptation/override.scss');
$variables['style_image_path'] = "'../images/blueTemptation/'";
WCF_DIR.'acp/style/style',
$files,
$variables,
- file_get_contents(WCF_DIR.'acp/style/blueTemptation/individual.less'),
+ file_get_contents(WCF_DIR.'acp/style/blueTemptation/individual.scss'),
new Callback(function($content) {
// fix relative paths
$content = str_replace('../font/', '../../font/', $content);
*/
protected function bootstrap(array $variables) {
// add reset like a boss
- $content = $this->prepareFile(WCF_DIR.'style/bootstrap/reset.less');
+ $content = $this->prepareFile(WCF_DIR.'style/bootstrap/reset.scss');
// apply style variables
$this->compiler->setVariables($variables);
// add mixins
- $content .= $this->prepareFile(WCF_DIR.'style/bootstrap/mixin.less');
+ $content .= $this->prepareFile(WCF_DIR.'style/bootstrap/mixin.scss');
return $content;
}
/**
- * Prepares a LESS stylesheet for importing.
+ * Prepares a SCSS stylesheet for importing.
*
* @param string $filename
* @return string
}
/**
- * Compiles LESS stylesheets into one CSS-stylesheet and writes them
+ * Compiles SCSS stylesheets into one CSS-stylesheet and writes them
* to filesystem. Please be aware not to append '.css' within $filename!
*
* @param string $filename
* @param array<string> $files
* @param array<string> $variables
- * @param string $individualLess
+ * @param string $individualScss
* @param \wcf\system\Callback $callback
*/
- protected function compileStylesheet($filename, array $files, array $variables, $individualLess, Callback $callback) {
+ protected function compileStylesheet($filename, array $files, array $variables, $individualScss, Callback $callback) {
foreach ($variables as &$value) {
if (StringUtil::startsWith($value, '../')) {
$value = '~"'.$value.'"';
}
unset($value);
- // add options as LESS variables
+ // add options as SCSS variables
if (PACKAGE_ID) {
foreach (Option::getOptions() as $constantName => $option) {
if (in_array($option->optionType, static::$supportedOptionType)) {
$variables['wcf_option_signature_max_image_height'] = '~"150"';
}
- // build LESS bootstrap
- $less = $this->bootstrap($variables);
+ // build SCSS bootstrap
+ $scss = $this->bootstrap($variables);
foreach ($files as $file) {
- $less .= $this->prepareFile($file);
+ $scss .= $this->prepareFile($file);
}
- // append individual CSS/LESS
- if ($individualLess) {
- $less .= $individualLess;
+ // append individual CSS/SCSS
+ if ($individualScss) {
+ $scss .= $individualScss;
}
try {
- $content = $this->compiler->compile($less);
+ $this->compiler->setFormatter('Leafo\ScssPhp\Formatter\Crunched');
+ $content = $this->compiler->compile($scss);
}
catch (\Exception $e) {
- throw new SystemException("Could not compile LESS: ".$e->getMessage(), 0, '', $e);
+ throw new SystemException("Could not compile SCSS: ".$e->getMessage(), 0, '', $e);
}
$content = $callback($content);
// compress stylesheet
- $lines = explode("\n", $content);
+ /*$lines = explode("\n", $content);
$content = $lines[0] . "\n" . $lines[1] . "\n";
for ($i = 2, $length = count($lines); $i < $length; $i++) {
$line = trim($lines[$i]);
$content .= "\n";
}
}
+ */
// write stylesheet
file_put_contents($filename.'.css', $content);
FileUtil::makeWritable($filename.'.css');
// convert stylesheet to RTL
- $content = StyleUtil::convertCSSToRTL($content);
+ //$content = StyleUtil::convertCSSToRTL($content);
// write stylesheet for RTL
file_put_contents($filename.'-rtl.css', $content);
+++ /dev/null
-<?php
-// @codingStandardsIgnoreFile
-/**
- * lessphp v0.4.0
- * http://leafo.net/lessphp
- *
- * LESS css compiler, adapted from http://lesscss.org
- *
- * Copyright 2012, Leaf Corcoran <leafot@gmail.com>
- * Licensed under MIT or GPLv3, see LICENSE
- */
-
-
-/**
- * The less compiler and parser.
- *
- * Converting LESS to CSS is a three stage process. The incoming file is parsed
- * by `lessc_parser` into a syntax tree, then it is compiled into another tree
- * representing the CSS structure by `lessc`. The CSS tree is fed into a
- * formatter, like `lessc_formatter` which then outputs CSS as a string.
- *
- * During the first compile, all values are *reduced*, which means that their
- * types are brought to the lowest form before being dump as strings. This
- * handles math equations, variable dereferences, and the like.
- *
- * The `parse` function of `lessc` is the entry point.
- *
- * In summary:
- *
- * The `lessc` class creates an intstance of the parser, feeds it LESS code,
- * then transforms the resulting tree to a CSS tree. This class also holds the
- * evaluation context, such as all available mixins and variables at any given
- * time.
- *
- * The `lessc_parser` class is only concerned with parsing its input.
- *
- * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string,
- * handling things like indentation.
- */
-class lessc {
- static public $VERSION = "v0.4.0";
- static protected $TRUE = array("keyword", "true");
- static protected $FALSE = array("keyword", "false");
-
- protected $libFunctions = array();
- protected $registeredVars = array();
- protected $preserveComments = false;
-
- public $vPrefix = '@'; // prefix of abstract properties
- public $mPrefix = '$'; // prefix of abstract blocks
- public $parentSelector = '&';
-
- public $importDisabled = false;
- public $importDir = '';
-
- protected $numberPrecision = null;
-
- protected $allParsedFiles = array();
-
- // set to the parser that generated the current line when compiling
- // so we know how to create error messages
- protected $sourceParser = null;
- protected $sourceLoc = null;
-
- static public $defaultValue = array("keyword", "");
-
- static protected $nextImportId = 0; // uniquely identify imports
-
- // attempts to find the path of an import url, returns null for css files
- protected function findImport($url) {
- foreach ((array)$this->importDir as $dir) {
- $full = $dir.(substr($dir, -1) != '/' ? '/' : '').$url;
- if ($this->fileExists($file = $full.'.less') || $this->fileExists($file = $full)) {
- return $file;
- }
- }
-
- return null;
- }
-
- protected function fileExists($name) {
- return is_file($name);
- }
-
- static public function compressList($items, $delim) {
- if (!isset($items[1]) && isset($items[0])) return $items[0];
- else return array('list', $delim, $items);
- }
-
- static public function preg_quote($what) {
- return preg_quote($what, '/');
- }
-
- protected function tryImport($importPath, $parentBlock, $out) {
- if ($importPath[0] == "function" && $importPath[1] == "url") {
- $importPath = $this->flattenList($importPath[2]);
- }
-
- $str = $this->coerceString($importPath);
- if ($str === null) return false;
-
- $url = $this->compileValue($this->lib_e($str));
-
- // don't import if it ends in css
- if (substr_compare($url, '.css', -4, 4) === 0) return false;
-
- $realPath = $this->findImport($url);
-
- if ($realPath === null) return false;
-
- if ($this->importDisabled) {
- return array(false, "/* import disabled */");
- }
-
- if (isset($this->allParsedFiles[realpath($realPath)])) {
- return array(false, null);
- }
-
- $this->addParsedFile($realPath);
- $parser = $this->makeParser($realPath);
- $root = $parser->parse(file_get_contents($realPath));
-
- // set the parents of all the block props
- foreach ($root->props as $prop) {
- if ($prop[0] == "block") {
- $prop[1]->parent = $parentBlock;
- }
- }
-
- // copy mixins into scope, set their parents
- // bring blocks from import into current block
- // TODO: need to mark the source parser these came from this file
- foreach ($root->children as $childName => $child) {
- if (isset($parentBlock->children[$childName])) {
- $parentBlock->children[$childName] = array_merge(
- $parentBlock->children[$childName],
- $child);
- } else {
- $parentBlock->children[$childName] = $child;
- }
- }
-
- $pi = pathinfo($realPath);
- $dir = $pi["dirname"];
-
- list($top, $bottom) = $this->sortProps($root->props, true);
- $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir);
-
- return array(true, $bottom, $parser, $dir);
- }
-
- protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir) {
- $oldSourceParser = $this->sourceParser;
-
- $oldImport = $this->importDir;
-
- // TODO: this is because the importDir api is stupid
- $this->importDir = (array)$this->importDir;
- array_unshift($this->importDir, $importDir);
-
- foreach ($props as $prop) {
- $this->compileProp($prop, $block, $out);
- }
-
- $this->importDir = $oldImport;
- $this->sourceParser = $oldSourceParser;
- }
-
- /**
- * Recursively compiles a block.
- *
- * A block is analogous to a CSS block in most cases. A single LESS document
- * is encapsulated in a block when parsed, but it does not have parent tags
- * so all of it's children appear on the root level when compiled.
- *
- * Blocks are made up of props and children.
- *
- * Props are property instructions, array tuples which describe an action
- * to be taken, eg. write a property, set a variable, mixin a block.
- *
- * The children of a block are just all the blocks that are defined within.
- * This is used to look up mixins when performing a mixin.
- *
- * Compiling the block involves pushing a fresh environment on the stack,
- * and iterating through the props, compiling each one.
- *
- * See lessc::compileProp()
- *
- */
- protected function compileBlock($block) {
- switch ($block->type) {
- case "root":
- $this->compileRoot($block);
- break;
- case null:
- $this->compileCSSBlock($block);
- break;
- case "media":
- $this->compileMedia($block);
- break;
- case "directive":
- $name = "@" . $block->name;
- if (!empty($block->value)) {
- $name .= " " . $this->compileValue($this->reduce($block->value));
- }
-
- $this->compileNestedBlock($block, array($name));
- break;
- default:
- $this->throwError("unknown block type: $block->type\n");
- }
- }
-
- protected function compileCSSBlock($block) {
- $env = $this->pushEnv();
-
- $selectors = $this->compileSelectors($block->tags);
- $env->selectors = $this->multiplySelectors($selectors);
- $out = $this->makeOutputBlock(null, $env->selectors);
-
- $this->scope->children[] = $out;
- $this->compileProps($block, $out);
-
- $block->scope = $env; // mixins carry scope with them!
- $this->popEnv();
- }
-
- protected function compileMedia($media) {
- $env = $this->pushEnv($media);
- $parentScope = $this->mediaParent($this->scope);
-
- $query = $this->compileMediaQuery($this->multiplyMedia($env));
-
- $this->scope = $this->makeOutputBlock($media->type, array($query));
- $parentScope->children[] = $this->scope;
-
- $this->compileProps($media, $this->scope);
-
- if (count($this->scope->lines) > 0) {
- $orphanSelelectors = $this->findClosestSelectors();
- if (!is_null($orphanSelelectors)) {
- $orphan = $this->makeOutputBlock(null, $orphanSelelectors);
- $orphan->lines = $this->scope->lines;
- array_unshift($this->scope->children, $orphan);
- $this->scope->lines = array();
- }
- }
-
- $this->scope = $this->scope->parent;
- $this->popEnv();
- }
-
- protected function mediaParent($scope) {
- while (!empty($scope->parent)) {
- if (!empty($scope->type) && $scope->type != "media") {
- break;
- }
- $scope = $scope->parent;
- }
-
- return $scope;
- }
-
- protected function compileNestedBlock($block, $selectors) {
- $this->pushEnv($block);
- $this->scope = $this->makeOutputBlock($block->type, $selectors);
- $this->scope->parent->children[] = $this->scope;
-
- $this->compileProps($block, $this->scope);
-
- $this->scope = $this->scope->parent;
- $this->popEnv();
- }
-
- protected function compileRoot($root) {
- $this->pushEnv();
- $this->scope = $this->makeOutputBlock($root->type);
- $this->compileProps($root, $this->scope);
- $this->popEnv();
- }
-
- protected function compileProps($block, $out) {
- foreach ($this->sortProps($block->props) as $prop) {
- $this->compileProp($prop, $block, $out);
- }
-
- $out->lines = array_values(array_unique($out->lines));
- }
-
- protected function sortProps($props, $split = false) {
- $vars = array();
- $imports = array();
- $other = array();
-
- foreach ($props as $prop) {
- switch ($prop[0]) {
- case "assign":
- if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) {
- $vars[] = $prop;
- } else {
- $other[] = $prop;
- }
- break;
- case "import":
- $id = self::$nextImportId++;
- $prop[] = $id;
- $imports[] = $prop;
- $other[] = array("import_mixin", $id);
- break;
- default:
- $other[] = $prop;
- }
- }
-
- if ($split) {
- return array(array_merge($vars, $imports), $other);
- } else {
- return array_merge($vars, $imports, $other);
- }
- }
-
- protected function compileMediaQuery($queries) {
- $compiledQueries = array();
- foreach ($queries as $query) {
- $parts = array();
- foreach ($query as $q) {
- switch ($q[0]) {
- case "mediaType":
- $parts[] = implode(" ", array_slice($q, 1));
- break;
- case "mediaExp":
- if (isset($q[2])) {
- $parts[] = "($q[1]: " .
- $this->compileValue($this->reduce($q[2])) . ")";
- } else {
- $parts[] = "($q[1])";
- }
- break;
- case "variable":
- $parts[] = $this->compileValue($this->reduce($q));
- break;
- }
- }
-
- if (count($parts) > 0) {
- $compiledQueries[] = implode(" and ", $parts);
- }
- }
-
- $out = "@media";
- if (!empty($parts)) {
- $out .= " " .
- implode($this->formatter->selectorSeparator, $compiledQueries);
- }
- return $out;
- }
-
- protected function multiplyMedia($env, $childQueries = null) {
- if (is_null($env) ||
- !empty($env->block->type) && $env->block->type != "media")
- {
- return $childQueries;
- }
-
- // plain old block, skip
- if (empty($env->block->type)) {
- return $this->multiplyMedia($env->parent, $childQueries);
- }
-
- $out = array();
- $queries = $env->block->queries;
- if (is_null($childQueries)) {
- $out = $queries;
- } else {
- foreach ($queries as $parent) {
- foreach ($childQueries as $child) {
- $out[] = array_merge($parent, $child);
- }
- }
- }
-
- return $this->multiplyMedia($env->parent, $out);
- }
-
- protected function expandParentSelectors(&$tag, $replace) {
- $parts = explode("$&$", $tag);
- $count = 0;
- foreach ($parts as &$part) {
- $part = str_replace($this->parentSelector, $replace, $part, $c);
- $count += $c;
- }
- $tag = implode($this->parentSelector, $parts);
- return $count;
- }
-
- protected function findClosestSelectors() {
- $env = $this->env;
- $selectors = null;
- while ($env !== null) {
- if (isset($env->selectors)) {
- $selectors = $env->selectors;
- break;
- }
- $env = $env->parent;
- }
-
- return $selectors;
- }
-
-
- // multiply $selectors against the nearest selectors in env
- protected function multiplySelectors($selectors) {
- // find parent selectors
-
- $parentSelectors = $this->findClosestSelectors();
- if (is_null($parentSelectors)) {
- // kill parent reference in top level selector
- foreach ($selectors as &$s) {
- $this->expandParentSelectors($s, "");
- }
-
- return $selectors;
- }
-
- $out = array();
- foreach ($parentSelectors as $parent) {
- foreach ($selectors as $child) {
- $count = $this->expandParentSelectors($child, $parent);
-
- // don't prepend the parent tag if & was used
- if ($count > 0) {
- $out[] = trim($child);
- } else {
- $out[] = trim($parent . ' ' . $child);
- }
- }
- }
-
- return $out;
- }
-
- // reduces selector expressions
- protected function compileSelectors($selectors) {
- $out = array();
-
- foreach ($selectors as $s) {
- if (is_array($s)) {
- list(, $value) = $s;
- $out[] = trim($this->compileValue($this->reduce($value)));
- } else {
- $out[] = $s;
- }
- }
-
- return $out;
- }
-
- protected function eq($left, $right) {
- return $left == $right;
- }
-
- protected function patternMatch($block, $orderedArgs, $keywordArgs) {
- // match the guards if it has them
- // any one of the groups must have all its guards pass for a match
- if (!empty($block->guards)) {
- $groupPassed = false;
- foreach ($block->guards as $guardGroup) {
- foreach ($guardGroup as $guard) {
- $this->pushEnv();
- $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs);
-
- $negate = false;
- if ($guard[0] == "negate") {
- $guard = $guard[1];
- $negate = true;
- }
-
- $passed = $this->reduce($guard) == self::$TRUE;
- if ($negate) $passed = !$passed;
-
- $this->popEnv();
-
- if ($passed) {
- $groupPassed = true;
- } else {
- $groupPassed = false;
- break;
- }
- }
-
- if ($groupPassed) break;
- }
-
- if (!$groupPassed) {
- return false;
- }
- }
-
- if (empty($block->args)) {
- return $block->isVararg || empty($orderedArgs) && empty($keywordArgs);
- }
-
- $remainingArgs = $block->args;
- if ($keywordArgs) {
- $remainingArgs = array();
- foreach ($block->args as $arg) {
- if ($arg[0] == "arg" && isset($keywordArgs[$arg[1]])) {
- continue;
- }
-
- $remainingArgs[] = $arg;
- }
- }
-
- $i = -1; // no args
- // try to match by arity or by argument literal
- foreach ($remainingArgs as $i => $arg) {
- switch ($arg[0]) {
- case "lit":
- if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) {
- return false;
- }
- break;
- case "arg":
- // no arg and no default value
- if (!isset($orderedArgs[$i]) && !isset($arg[2])) {
- return false;
- }
- break;
- case "rest":
- $i--; // rest can be empty
- break 2;
- }
- }
-
- if ($block->isVararg) {
- return true; // not having enough is handled above
- } else {
- $numMatched = $i + 1;
- // greater than becuase default values always match
- return $numMatched >= count($orderedArgs);
- }
- }
-
- protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip=array()) {
- $matches = null;
- foreach ($blocks as $block) {
- // skip seen blocks that don't have arguments
- if (isset($skip[$block->id]) && !isset($block->args)) {
- continue;
- }
-
- if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) {
- $matches[] = $block;
- }
- }
-
- return $matches;
- }
-
- // attempt to find blocks matched by path and args
- protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen=array()) {
- if ($searchIn == null) return null;
- if (isset($seen[$searchIn->id])) return null;
- $seen[$searchIn->id] = true;
-
- $name = $path[0];
-
- if (isset($searchIn->children[$name])) {
- $blocks = $searchIn->children[$name];
- if (count($path) == 1) {
- $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen);
- if (!empty($matches)) {
- // This will return all blocks that match in the closest
- // scope that has any matching block, like lessjs
- return $matches;
- }
- } else {
- $matches = array();
- foreach ($blocks as $subBlock) {
- $subMatches = $this->findBlocks($subBlock,
- array_slice($path, 1), $orderedArgs, $keywordArgs, $seen);
-
- if (!is_null($subMatches)) {
- foreach ($subMatches as $sm) {
- $matches[] = $sm;
- }
- }
- }
-
- return count($matches) > 0 ? $matches : null;
- }
- }
- if ($searchIn->parent === $searchIn) return null;
- return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen);
- }
-
- // sets all argument names in $args to either the default value
- // or the one passed in through $values
- protected function zipSetArgs($args, $orderedValues, $keywordValues) {
- $assignedValues = array();
-
- $i = 0;
- foreach ($args as $a) {
- if ($a[0] == "arg") {
- if (isset($keywordValues[$a[1]])) {
- // has keyword arg
- $value = $keywordValues[$a[1]];
- } elseif (isset($orderedValues[$i])) {
- // has ordered arg
- $value = $orderedValues[$i];
- $i++;
- } elseif (isset($a[2])) {
- // has default value
- $value = $a[2];
- } else {
- $this->throwError("Failed to assign arg " . $a[1]);
- $value = null; // :(
- }
-
- $value = $this->reduce($value);
- $this->set($a[1], $value);
- $assignedValues[] = $value;
- } else {
- // a lit
- $i++;
- }
- }
-
- // check for a rest
- $last = end($args);
- if ($last[0] == "rest") {
- $rest = array_slice($orderedValues, count($args) - 1);
- $this->set($last[1], $this->reduce(array("list", " ", $rest)));
- }
-
- // wow is this the only true use of PHP's + operator for arrays?
- $this->env->arguments = $assignedValues + $orderedValues;
- }
-
- // compile a prop and update $lines or $blocks appropriately
- protected function compileProp($prop, $block, $out) {
- // set error position context
- $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1;
-
- switch ($prop[0]) {
- case 'assign':
- list(, $name, $value) = $prop;
- if ($name[0] == $this->vPrefix) {
- $this->set($name, $value);
- } else {
- $out->lines[] = $this->formatter->property($name,
- $this->compileValue($this->reduce($value)));
- }
- break;
- case 'block':
- list(, $child) = $prop;
- $this->compileBlock($child);
- break;
- case 'mixin':
- list(, $path, $args, $suffix) = $prop;
-
- $orderedArgs = array();
- $keywordArgs = array();
- foreach ((array)$args as $arg) {
- $argval = null;
- switch ($arg[0]) {
- case "arg":
- if (!isset($arg[2])) {
- $orderedArgs[] = $this->reduce(array("variable", $arg[1]));
- } else {
- $keywordArgs[$arg[1]] = $this->reduce($arg[2]);
- }
- break;
-
- case "lit":
- $orderedArgs[] = $this->reduce($arg[1]);
- break;
- default:
- $this->throwError("Unknown arg type: " . $arg[0]);
- }
- }
-
- $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs);
-
- if ($mixins === null) {
- // fwrite(STDERR,"failed to find block: ".implode(" > ", $path)."\n");
- break; // throw error here??
- }
-
- foreach ($mixins as $mixin) {
- if ($mixin === $block && !$orderedArgs) {
- continue;
- }
-
- $haveScope = false;
- if (isset($mixin->parent->scope)) {
- $haveScope = true;
- $mixinParentEnv = $this->pushEnv();
- $mixinParentEnv->storeParent = $mixin->parent->scope;
- }
-
- $haveArgs = false;
- if (isset($mixin->args)) {
- $haveArgs = true;
- $this->pushEnv();
- $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs);
- }
-
- $oldParent = $mixin->parent;
- if ($mixin != $block) $mixin->parent = $block;
-
- foreach ($this->sortProps($mixin->props) as $subProp) {
- if ($suffix !== null &&
- $subProp[0] == "assign" &&
- is_string($subProp[1]) &&
- $subProp[1]{0} != $this->vPrefix)
- {
- $subProp[2] = array(
- 'list', ' ',
- array($subProp[2], array('keyword', $suffix))
- );
- }
-
- $this->compileProp($subProp, $mixin, $out);
- }
-
- $mixin->parent = $oldParent;
-
- if ($haveArgs) $this->popEnv();
- if ($haveScope) $this->popEnv();
- }
-
- break;
- case 'raw':
- $out->lines[] = $prop[1];
- break;
- case "directive":
- list(, $name, $value) = $prop;
- $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)).';';
- break;
- case "comment":
- $out->lines[] = $prop[1];
- break;
- case "import";
- list(, $importPath, $importId) = $prop;
- $importPath = $this->reduce($importPath);
-
- if (!isset($this->env->imports)) {
- $this->env->imports = array();
- }
-
- $result = $this->tryImport($importPath, $block, $out);
-
- $this->env->imports[$importId] = $result === false ?
- array(false, "@import " . $this->compileValue($importPath).";") :
- $result;
-
- break;
- case "import_mixin":
- list(,$importId) = $prop;
- $import = $this->env->imports[$importId];
- if ($import[0] === false) {
- if (isset($import[1])) {
- $out->lines[] = $import[1];
- }
- } else {
- list(, $bottom, $parser, $importDir) = $import;
- $this->compileImportedProps($bottom, $block, $out, $parser, $importDir);
- }
-
- break;
- default:
- $this->throwError("unknown op: {$prop[0]}\n");
- }
- }
-
-
- /**
- * Compiles a primitive value into a CSS property value.
- *
- * Values in lessphp are typed by being wrapped in arrays, their format is
- * typically:
- *
- * array(type, contents [, additional_contents]*)
- *
- * The input is expected to be reduced. This function will not work on
- * things like expressions and variables.
- */
- protected function compileValue($value) {
- switch ($value[0]) {
- case 'list':
- // [1] - delimiter
- // [2] - array of values
- return implode($value[1], array_map(array($this, 'compileValue'), $value[2]));
- case 'raw_color':
- if (!empty($this->formatter->compressColors)) {
- return $this->compileValue($this->coerceColor($value));
- }
- return $value[1];
- case 'keyword':
- // [1] - the keyword
- return $value[1];
- case 'number':
- list(, $num, $unit) = $value;
- // [1] - the number
- // [2] - the unit
- if ($this->numberPrecision !== null) {
- $num = round($num, $this->numberPrecision);
- }
- return $num . $unit;
- case 'string':
- // [1] - contents of string (includes quotes)
- list(, $delim, $content) = $value;
- foreach ($content as &$part) {
- if (is_array($part)) {
- $part = $this->compileValue($part);
- }
- }
- return $delim . implode($content) . $delim;
- case 'color':
- // [1] - red component (either number or a %)
- // [2] - green component
- // [3] - blue component
- // [4] - optional alpha component
- list(, $r, $g, $b) = $value;
- $r = round($r);
- $g = round($g);
- $b = round($b);
-
- if (count($value) == 5 && $value[4] != 1) { // rgba
- return 'rgba('.$r.','.$g.','.$b.','.$value[4].')';
- }
-
- $h = sprintf("#%02x%02x%02x", $r, $g, $b);
-
- if (!empty($this->formatter->compressColors)) {
- // Converting hex color to short notation (e.g. #003399 to #039)
- if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
- $h = '#' . $h[1] . $h[3] . $h[5];
- }
- }
-
- return $h;
-
- case 'function':
- list(, $name, $args) = $value;
- return $name.'('.$this->compileValue($args).')';
- default: // assumed to be unit
- $this->throwError("unknown value type: $value[0]");
- }
- }
-
- protected function lib_pow($args) {
- list($base, $exp) = $this->assertArgs($args, 2, "pow");
- return pow($this->assertNumber($base), $this->assertNumber($exp));
- }
-
- protected function lib_pi() {
- return pi();
- }
-
- protected function lib_mod($args) {
- list($a, $b) = $this->assertArgs($args, 2, "mod");
- return $this->assertNumber($a) % $this->assertNumber($b);
- }
-
- protected function lib_tan($num) {
- return tan($this->assertNumber($num));
- }
-
- protected function lib_sin($num) {
- return sin($this->assertNumber($num));
- }
-
- protected function lib_cos($num) {
- return cos($this->assertNumber($num));
- }
-
- protected function lib_atan($num) {
- $num = atan($this->assertNumber($num));
- return array("number", $num, "rad");
- }
-
- protected function lib_asin($num) {
- $num = asin($this->assertNumber($num));
- return array("number", $num, "rad");
- }
-
- protected function lib_acos($num) {
- $num = acos($this->assertNumber($num));
- return array("number", $num, "rad");
- }
-
- protected function lib_sqrt($num) {
- return sqrt($this->assertNumber($num));
- }
-
- protected function lib_extract($value) {
- list($list, $idx) = $this->assertArgs($value, 2, "extract");
- $idx = $this->assertNumber($idx);
- // 1 indexed
- if ($list[0] == "list" && isset($list[2][$idx - 1])) {
- return $list[2][$idx - 1];
- }
- }
-
- protected function lib_isnumber($value) {
- return $this->toBool($value[0] == "number");
- }
-
- protected function lib_isstring($value) {
- return $this->toBool($value[0] == "string");
- }
-
- protected function lib_iscolor($value) {
- return $this->toBool($this->coerceColor($value));
- }
-
- protected function lib_iskeyword($value) {
- return $this->toBool($value[0] == "keyword");
- }
-
- protected function lib_ispixel($value) {
- return $this->toBool($value[0] == "number" && $value[2] == "px");
- }
-
- protected function lib_ispercentage($value) {
- return $this->toBool($value[0] == "number" && $value[2] == "%");
- }
-
- protected function lib_isem($value) {
- return $this->toBool($value[0] == "number" && $value[2] == "em");
- }
-
- protected function lib_isrem($value) {
- return $this->toBool($value[0] == "number" && $value[2] == "rem");
- }
-
- protected function lib_rgbahex($color) {
- $color = $this->coerceColor($color);
- if (is_null($color))
- $this->throwError("color expected for rgbahex");
-
- return sprintf("#%02x%02x%02x%02x",
- isset($color[4]) ? $color[4]*255 : 255,
- $color[1],$color[2], $color[3]);
- }
-
- protected function lib_argb($color){
- return $this->lib_rgbahex($color);
- }
-
- // utility func to unquote a string
- protected function lib_e($arg) {
- switch ($arg[0]) {
- case "list":
- $items = $arg[2];
- if (isset($items[0])) {
- return $this->lib_e($items[0]);
- }
- return self::$defaultValue;
- case "string":
- $arg[1] = "";
- return $arg;
- case "keyword":
- return $arg;
- default:
- return array("keyword", $this->compileValue($arg));
- }
- }
-
- protected function lib__sprintf($args) {
- if ($args[0] != "list") return $args;
- $values = $args[2];
- $string = array_shift($values);
- $template = $this->compileValue($this->lib_e($string));
-
- $i = 0;
- if (preg_match_all('/%[dsa]/', $template, $m)) {
- foreach ($m[0] as $match) {
- $val = isset($values[$i]) ?
- $this->reduce($values[$i]) : array('keyword', '');
-
- // lessjs compat, renders fully expanded color, not raw color
- if ($color = $this->coerceColor($val)) {
- $val = $color;
- }
-
- $i++;
- $rep = $this->compileValue($this->lib_e($val));
- $template = preg_replace('/'.self::preg_quote($match).'/',
- $rep, $template, 1);
- }
- }
-
- $d = $string[0] == "string" ? $string[1] : '"';
- return array("string", $d, array($template));
- }
-
- protected function lib_floor($arg) {
- $value = $this->assertNumber($arg);
- return array("number", floor($value), $arg[2]);
- }
-
- protected function lib_ceil($arg) {
- $value = $this->assertNumber($arg);
- return array("number", ceil($value), $arg[2]);
- }
-
- protected function lib_round($arg) {
- $value = $this->assertNumber($arg);
- return array("number", round($value), $arg[2]);
- }
-
- protected function lib_unit($arg) {
- if ($arg[0] == "list") {
- list($number, $newUnit) = $arg[2];
- return array("number", $this->assertNumber($number),
- $this->compileValue($this->lib_e($newUnit)));
- } else {
- return array("number", $this->assertNumber($arg), "");
- }
- }
-
- /**
- * Helper function to get arguments for color manipulation functions.
- * takes a list that contains a color like thing and a percentage
- */
- protected function colorArgs($args) {
- if ($args[0] != 'list' || count($args[2]) < 2) {
- return array(array('color', 0, 0, 0), 0);
- }
- list($color, $delta) = $args[2];
- $color = $this->assertColor($color);
- $delta = floatval($delta[1]);
-
- return array($color, $delta);
- }
-
- protected function lib_darken($args) {
- list($color, $delta) = $this->colorArgs($args);
-
- $hsl = $this->toHSL($color);
- $hsl[3] = $this->clamp($hsl[3] - $delta, 100);
- return $this->toRGB($hsl);
- }
-
- protected function lib_lighten($args) {
- list($color, $delta) = $this->colorArgs($args);
-
- $hsl = $this->toHSL($color);
- $hsl[3] = $this->clamp($hsl[3] + $delta, 100);
- return $this->toRGB($hsl);
- }
-
- protected function lib_saturate($args) {
- list($color, $delta) = $this->colorArgs($args);
-
- $hsl = $this->toHSL($color);
- $hsl[2] = $this->clamp($hsl[2] + $delta, 100);
- return $this->toRGB($hsl);
- }
-
- protected function lib_desaturate($args) {
- list($color, $delta) = $this->colorArgs($args);
-
- $hsl = $this->toHSL($color);
- $hsl[2] = $this->clamp($hsl[2] - $delta, 100);
- return $this->toRGB($hsl);
- }
-
- protected function lib_spin($args) {
- list($color, $delta) = $this->colorArgs($args);
-
- $hsl = $this->toHSL($color);
-
- $hsl[1] = $hsl[1] + $delta % 360;
- if ($hsl[1] < 0) $hsl[1] += 360;
-
- return $this->toRGB($hsl);
- }
-
- protected function lib_fadeout($args) {
- list($color, $delta) = $this->colorArgs($args);
- $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100);
- return $color;
- }
-
- protected function lib_fadein($args) {
- list($color, $delta) = $this->colorArgs($args);
- $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100);
- return $color;
- }
-
- protected function lib_hue($color) {
- $hsl = $this->toHSL($this->assertColor($color));
- return round($hsl[1]);
- }
-
- protected function lib_saturation($color) {
- $hsl = $this->toHSL($this->assertColor($color));
- return round($hsl[2]);
- }
-
- protected function lib_lightness($color) {
- $hsl = $this->toHSL($this->assertColor($color));
- return round($hsl[3]);
- }
-
- // get the alpha of a color
- // defaults to 1 for non-colors or colors without an alpha
- protected function lib_alpha($value) {
- if (!is_null($color = $this->coerceColor($value))) {
- return isset($color[4]) ? $color[4] : 1;
- }
- }
-
- // set the alpha of the color
- protected function lib_fade($args) {
- list($color, $alpha) = $this->colorArgs($args);
- $color[4] = $this->clamp($alpha / 100.0);
- return $color;
- }
-
- protected function lib_percentage($arg) {
- $num = $this->assertNumber($arg);
- return array("number", $num*100, "%");
- }
-
- // mixes two colors by weight
- // mix(@color1, @color2, [@weight: 50%]);
- // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method
- protected function lib_mix($args) {
- if ($args[0] != "list" || count($args[2]) < 2)
- $this->throwError("mix expects (color1, color2, weight)");
-
- list($first, $second) = $args[2];
- $first = $this->assertColor($first);
- $second = $this->assertColor($second);
-
- $first_a = $this->lib_alpha($first);
- $second_a = $this->lib_alpha($second);
-
- if (isset($args[2][2])) {
- $weight = $args[2][2][1] / 100.0;
- } else {
- $weight = 0.5;
- }
-
- $w = $weight * 2 - 1;
- $a = $first_a - $second_a;
-
- $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
- $w2 = 1.0 - $w1;
-
- $new = array('color',
- $w1 * $first[1] + $w2 * $second[1],
- $w1 * $first[2] + $w2 * $second[2],
- $w1 * $first[3] + $w2 * $second[3],
- );
-
- if ($first_a != 1.0 || $second_a != 1.0) {
- $new[] = $first_a * $weight + $second_a * ($weight - 1);
- }
-
- return $this->fixColor($new);
- }
-
- protected function lib_contrast($args) {
- if ($args[0] != 'list' || count($args[2]) < 3) {
- return array(array('color', 0, 0, 0), 0);
- }
-
- list($inputColor, $darkColor, $lightColor) = $args[2];
-
- $inputColor = $this->assertColor($inputColor);
- $darkColor = $this->assertColor($darkColor);
- $lightColor = $this->assertColor($lightColor);
- $hsl = $this->toHSL($inputColor);
-
- if ($hsl[3] > 50) {
- return $darkColor;
- }
-
- return $lightColor;
- }
-
- protected function assertColor($value, $error = "expected color value") {
- $color = $this->coerceColor($value);
- if (is_null($color)) $this->throwError($error);
- return $color;
- }
-
- protected function assertNumber($value, $error = "expecting number") {
- if ($value[0] == "number") return $value[1];
- $this->throwError($error);
- }
-
- protected function assertArgs($value, $expectedArgs, $name="") {
- if ($expectedArgs == 1) {
- return $value;
- } else {
- if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list");
- $values = $value[2];
- $numValues = count($values);
- if ($expectedArgs != $numValues) {
- if ($name) {
- $name = $name . ": ";
- }
-
- $this->throwError("${name}expecting $expectedArgs arguments, got $numValues");
- }
-
- return $values;
- }
- }
-
- protected function toHSL($color) {
- if ($color[0] == 'hsl') return $color;
-
- $r = $color[1] / 255;
- $g = $color[2] / 255;
- $b = $color[3] / 255;
-
- $min = min($r, $g, $b);
- $max = max($r, $g, $b);
-
- $L = ($min + $max) / 2;
- if ($min == $max) {
- $S = $H = 0;
- } else {
- if ($L < 0.5)
- $S = ($max - $min)/($max + $min);
- else
- $S = ($max - $min)/(2.0 - $max - $min);
-
- if ($r == $max) $H = ($g - $b)/($max - $min);
- elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min);
- elseif ($b == $max) $H = 4.0 + ($r - $g)/($max - $min);
-
- }
-
- $out = array('hsl',
- ($H < 0 ? $H + 6 : $H)*60,
- $S*100,
- $L*100,
- );
-
- if (count($color) > 4) $out[] = $color[4]; // copy alpha
- return $out;
- }
-
- protected function toRGB_helper($comp, $temp1, $temp2) {
- if ($comp < 0) $comp += 1.0;
- elseif ($comp > 1) $comp -= 1.0;
-
- if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp;
- if (2 * $comp < 1) return $temp2;
- if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6;
-
- return $temp1;
- }
-
- /**
- * Converts a hsl array into a color value in rgb.
- * Expects H to be in range of 0 to 360, S and L in 0 to 100
- */
- protected function toRGB($color) {
- if ($color[0] == 'color') return $color;
-
- $H = $color[1] / 360;
- $S = $color[2] / 100;
- $L = $color[3] / 100;
-
- if ($S == 0) {
- $r = $g = $b = $L;
- } else {
- $temp2 = $L < 0.5 ?
- $L*(1.0 + $S) :
- $L + $S - $L * $S;
-
- $temp1 = 2.0 * $L - $temp2;
-
- $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2);
- $g = $this->toRGB_helper($H, $temp1, $temp2);
- $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2);
- }
-
- // $out = array('color', round($r*255), round($g*255), round($b*255));
- $out = array('color', $r*255, $g*255, $b*255);
- if (count($color) > 4) $out[] = $color[4]; // copy alpha
- return $out;
- }
-
- protected function clamp($v, $max = 1, $min = 0) {
- return min($max, max($min, $v));
- }
-
- /**
- * Convert the rgb, rgba, hsl color literals of function type
- * as returned by the parser into values of color type.
- */
- protected function funcToColor($func) {
- $fname = $func[1];
- if ($func[2][0] != 'list') return false; // need a list of arguments
- $rawComponents = $func[2][2];
-
- if ($fname == 'hsl' || $fname == 'hsla') {
- $hsl = array('hsl');
- $i = 0;
- foreach ($rawComponents as $c) {
- $val = $this->reduce($c);
- $val = isset($val[1]) ? floatval($val[1]) : 0;
-
- if ($i == 0) $clamp = 360;
- elseif ($i < 3) $clamp = 100;
- else $clamp = 1;
-
- $hsl[] = $this->clamp($val, $clamp);
- $i++;
- }
-
- while (count($hsl) < 4) $hsl[] = 0;
- return $this->toRGB($hsl);
-
- } elseif ($fname == 'rgb' || $fname == 'rgba') {
- $components = array();
- $i = 1;
- foreach ($rawComponents as $c) {
- $c = $this->reduce($c);
- if ($i < 4) {
- if ($c[0] == "number" && $c[2] == "%") {
- $components[] = 255 * ($c[1] / 100);
- } else {
- $components[] = floatval($c[1]);
- }
- } elseif ($i == 4) {
- if ($c[0] == "number" && $c[2] == "%") {
- $components[] = 1.0 * ($c[1] / 100);
- } else {
- $components[] = floatval($c[1]);
- }
- } else break;
-
- $i++;
- }
- while (count($components) < 3) $components[] = 0;
- array_unshift($components, 'color');
- return $this->fixColor($components);
- }
-
- return false;
- }
-
- protected function reduce($value, $forExpression = false) {
- switch ($value[0]) {
- case "interpolate":
- $reduced = $this->reduce($value[1]);
- $var = $this->compileValue($reduced);
- $res = $this->reduce(array("variable", $this->vPrefix . $var));
-
- if ($res[0] == "raw_color") {
- $res = $this->coerceColor($res);
- }
-
- if (empty($value[2])) $res = $this->lib_e($res);
-
- return $res;
- case "variable":
- $key = $value[1];
- if (is_array($key)) {
- $key = $this->reduce($key);
- $key = $this->vPrefix . $this->compileValue($this->lib_e($key));
- }
-
- $seen =& $this->env->seenNames;
-
- if (!empty($seen[$key])) {
- $this->throwError("infinite loop detected: $key");
- }
-
- $seen[$key] = true;
- $out = $this->reduce($this->get($key, self::$defaultValue));
- $seen[$key] = false;
- return $out;
- case "list":
- foreach ($value[2] as &$item) {
- $item = $this->reduce($item, $forExpression);
- }
- return $value;
- case "expression":
- return $this->evaluate($value);
- case "string":
- foreach ($value[2] as &$part) {
- if (is_array($part)) {
- $strip = $part[0] == "variable";
- $part = $this->reduce($part);
- if ($strip) $part = $this->lib_e($part);
- }
- }
- return $value;
- case "escape":
- list(,$inner) = $value;
- return $this->lib_e($this->reduce($inner));
- case "function":
- $color = $this->funcToColor($value);
- if ($color) return $color;
-
- list(, $name, $args) = $value;
- if ($name == "%") $name = "_sprintf";
- $f = isset($this->libFunctions[$name]) ?
- $this->libFunctions[$name] : array($this, 'lib_'.$name);
-
- if (is_callable($f)) {
- if ($args[0] == 'list')
- $args = self::compressList($args[2], $args[1]);
-
- $ret = call_user_func($f, $this->reduce($args, true), $this);
-
- if (is_null($ret)) {
- return array("string", "", array(
- $name, "(", $args, ")"
- ));
- }
-
- // convert to a typed value if the result is a php primitive
- if (is_numeric($ret)) $ret = array('number', $ret, "");
- elseif (!is_array($ret)) $ret = array('keyword', $ret);
-
- return $ret;
- }
-
- // plain function, reduce args
- $value[2] = $this->reduce($value[2]);
- return $value;
- case "unary":
- list(, $op, $exp) = $value;
- $exp = $this->reduce($exp);
-
- if ($exp[0] == "number") {
- switch ($op) {
- case "+":
- return $exp;
- case "-":
- $exp[1] *= -1;
- return $exp;
- }
- }
- return array("string", "", array($op, $exp));
- }
-
- if ($forExpression) {
- switch ($value[0]) {
- case "keyword":
- if ($color = $this->coerceColor($value)) {
- return $color;
- }
- break;
- case "raw_color":
- return $this->coerceColor($value);
- }
- }
-
- return $value;
- }
-
-
- // coerce a value for use in color operation
- protected function coerceColor($value) {
- switch($value[0]) {
- case 'color': return $value;
- case 'raw_color':
- $c = array("color", 0, 0, 0);
- $colorStr = substr($value[1], 1);
- $num = hexdec($colorStr);
- $width = strlen($colorStr) == 3 ? 16 : 256;
-
- for ($i = 3; $i > 0; $i--) { // 3 2 1
- $t = $num % $width;
- $num /= $width;
-
- $c[$i] = $t * (256/$width) + $t * floor(16/$width);
- }
-
- return $c;
- case 'keyword':
- $name = $value[1];
- if (isset(self::$cssColors[$name])) {
- $rgba = explode(',', self::$cssColors[$name]);
-
- if(isset($rgba[3]))
- return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]);
-
- return array('color', $rgba[0], $rgba[1], $rgba[2]);
- }
- return null;
- }
- }
-
- // make something string like into a string
- protected function coerceString($value) {
- switch ($value[0]) {
- case "string":
- return $value;
- case "keyword":
- return array("string", "", array($value[1]));
- }
- return null;
- }
-
- // turn list of length 1 into value type
- protected function flattenList($value) {
- if ($value[0] == "list" && count($value[2]) == 1) {
- return $this->flattenList($value[2][0]);
- }
- return $value;
- }
-
- protected function toBool($a) {
- if ($a) return self::$TRUE;
- else return self::$FALSE;
- }
-
- // evaluate an expression
- protected function evaluate($exp) {
- list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
-
- $left = $this->reduce($left, true);
- $right = $this->reduce($right, true);
-
- if ($leftColor = $this->coerceColor($left)) {
- $left = $leftColor;
- }
-
- if ($rightColor = $this->coerceColor($right)) {
- $right = $rightColor;
- }
-
- $ltype = $left[0];
- $rtype = $right[0];
-
- // operators that work on all types
- if ($op == "and") {
- return $this->toBool($left == self::$TRUE && $right == self::$TRUE);
- }
-
- if ($op == "=") {
- return $this->toBool($this->eq($left, $right) );
- }
-
- if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right))) {
- return $str;
- }
-
- // type based operators
- $fname = "op_${ltype}_${rtype}";
- if (is_callable(array($this, $fname))) {
- $out = $this->$fname($op, $left, $right);
- if (!is_null($out)) return $out;
- }
-
- // make the expression look it did before being parsed
- $paddedOp = $op;
- if ($whiteBefore) $paddedOp = " " . $paddedOp;
- if ($whiteAfter) $paddedOp .= " ";
-
- return array("string", "", array($left, $paddedOp, $right));
- }
-
- protected function stringConcatenate($left, $right) {
- if ($strLeft = $this->coerceString($left)) {
- if ($right[0] == "string") {
- $right[1] = "";
- }
- $strLeft[2][] = $right;
- return $strLeft;
- }
-
- if ($strRight = $this->coerceString($right)) {
- array_unshift($strRight[2], $left);
- return $strRight;
- }
- }
-
-
- // make sure a color's components don't go out of bounds
- protected function fixColor($c) {
- foreach (range(1, 3) as $i) {
- if ($c[$i] < 0) $c[$i] = 0;
- if ($c[$i] > 255) $c[$i] = 255;
- }
-
- return $c;
- }
-
- protected function op_number_color($op, $lft, $rgt) {
- if ($op == '+' || $op == '*') {
- return $this->op_color_number($op, $rgt, $lft);
- }
- }
-
- protected function op_color_number($op, $lft, $rgt) {
- if ($rgt[0] == '%') $rgt[1] /= 100;
-
- return $this->op_color_color($op, $lft,
- array_fill(1, count($lft) - 1, $rgt[1]));
- }
-
- protected function op_color_color($op, $left, $right) {
- $out = array('color');
- $max = count($left) > count($right) ? count($left) : count($right);
- foreach (range(1, $max - 1) as $i) {
- $lval = isset($left[$i]) ? $left[$i] : 0;
- $rval = isset($right[$i]) ? $right[$i] : 0;
- switch ($op) {
- case '+':
- $out[] = $lval + $rval;
- break;
- case '-':
- $out[] = $lval - $rval;
- break;
- case '*':
- $out[] = $lval * $rval;
- break;
- case '%':
- $out[] = $lval % $rval;
- break;
- case '/':
- if ($rval == 0) $this->throwError("evaluate error: can't divide by zero");
- $out[] = $lval / $rval;
- break;
- default:
- $this->throwError('evaluate error: color op number failed on op '.$op);
- }
- }
- return $this->fixColor($out);
- }
-
- function lib_red($color){
- $color = $this->coerceColor($color);
- if (is_null($color)) {
- $this->throwError('color expected for red()');
- }
-
- return $color[1];
- }
-
- function lib_green($color){
- $color = $this->coerceColor($color);
- if (is_null($color)) {
- $this->throwError('color expected for green()');
- }
-
- return $color[2];
- }
-
- function lib_blue($color){
- $color = $this->coerceColor($color);
- if (is_null($color)) {
- $this->throwError('color expected for blue()');
- }
-
- return $color[3];
- }
-
-
- // operator on two numbers
- protected function op_number_number($op, $left, $right) {
- $unit = empty($left[2]) ? $right[2] : $left[2];
-
- $value = 0;
- switch ($op) {
- case '+':
- $value = $left[1] + $right[1];
- break;
- case '*':
- $value = $left[1] * $right[1];
- break;
- case '-':
- $value = $left[1] - $right[1];
- break;
- case '%':
- $value = $left[1] % $right[1];
- break;
- case '/':
- if ($right[1] == 0) $this->throwError('parse error: divide by zero');
- $value = $left[1] / $right[1];
- break;
- case '<':
- return $this->toBool($left[1] < $right[1]);
- case '>':
- return $this->toBool($left[1] > $right[1]);
- case '>=':
- return $this->toBool($left[1] >= $right[1]);
- case '=<':
- return $this->toBool($left[1] <= $right[1]);
- default:
- $this->throwError('parse error: unknown number operator: '.$op);
- }
-
- return array("number", $value, $unit);
- }
-
-
- /* environment functions */
-
- protected function makeOutputBlock($type, $selectors = null) {
- $b = new stdclass;
- $b->lines = array();
- $b->children = array();
- $b->selectors = $selectors;
- $b->type = $type;
- $b->parent = $this->scope;
- return $b;
- }
-
- // the state of execution
- protected function pushEnv($block = null) {
- $e = new stdclass;
- $e->parent = $this->env;
- $e->store = array();
- $e->block = $block;
-
- $this->env = $e;
- return $e;
- }
-
- // pop something off the stack
- protected function popEnv() {
- $old = $this->env;
- $this->env = $this->env->parent;
- return $old;
- }
-
- // set something in the current env
- protected function set($name, $value) {
- $this->env->store[$name] = $value;
- }
-
-
- // get the highest occurrence entry for a name
- protected function get($name, $default=null) {
- $current = $this->env;
-
- $isArguments = $name == $this->vPrefix . 'arguments';
- while ($current) {
- if ($isArguments && isset($current->arguments)) {
- return array('list', ' ', $current->arguments);
- }
-
- if (isset($current->store[$name]))
- return $current->store[$name];
- else {
- $current = isset($current->storeParent) ?
- $current->storeParent : $current->parent;
- }
- }
-
- return $default;
- }
-
- // inject array of unparsed strings into environment as variables
- protected function injectVariables($args) {
- $this->pushEnv();
- $parser = new lessc_parser($this, __METHOD__);
- foreach ($args as $name => $strValue) {
- if ($name{0} != '@') $name = '@'.$name;
- $parser->count = 0;
- $parser->buffer = (string)$strValue;
- if (!$parser->propertyValue($value)) {
- throw new Exception("failed to parse passed in variable $name: $strValue");
- }
-
- $this->set($name, $value);
- }
- }
-
- /**
- * Initialize any static state, can initialize parser for a file
- * $opts isn't used yet
- */
- public function __construct($fname = null) {
- if ($fname !== null) {
- // used for deprecated parse method
- $this->_parseFile = $fname;
- }
- }
-
- public function compile($string, $name = null) {
- $locale = setlocale(LC_NUMERIC, 0);
- setlocale(LC_NUMERIC, "C");
-
- $this->parser = $this->makeParser($name);
- $root = $this->parser->parse($string);
-
- $this->env = null;
- $this->scope = null;
-
- $this->formatter = $this->newFormatter();
-
- if (!empty($this->registeredVars)) {
- $this->injectVariables($this->registeredVars);
- }
-
- $this->sourceParser = $this->parser; // used for error messages
- $this->compileBlock($root);
-
- ob_start();
- $this->formatter->block($this->scope);
- $out = ob_get_clean();
- setlocale(LC_NUMERIC, $locale);
- return $out;
- }
-
- public function compileFile($fname, $outFname = null) {
- if (!is_readable($fname)) {
- throw new Exception('load error: failed to find '.$fname);
- }
-
- $pi = pathinfo($fname);
-
- $oldImport = $this->importDir;
-
- $this->importDir = (array)$this->importDir;
- $this->importDir[] = $pi['dirname'].'/';
-
- $this->addParsedFile($fname);
-
- $out = $this->compile(file_get_contents($fname), $fname);
-
- $this->importDir = $oldImport;
-
- if ($outFname !== null) {
- return file_put_contents($outFname, $out);
- }
-
- return $out;
- }
-
- // compile only if changed input has changed or output doesn't exist
- public function checkedCompile($in, $out) {
- if (!is_file($out) || filemtime($in) > filemtime($out)) {
- $this->compileFile($in, $out);
- return true;
- }
- return false;
- }
-
- /**
- * Execute lessphp on a .less file or a lessphp cache structure
- *
- * The lessphp cache structure contains information about a specific
- * less file having been parsed. It can be used as a hint for future
- * calls to determine whether or not a rebuild is required.
- *
- * The cache structure contains two important keys that may be used
- * externally:
- *
- * compiled: The final compiled CSS
- * updated: The time (in seconds) the CSS was last compiled
- *
- * The cache structure is a plain-ol' PHP associative array and can
- * be serialized and unserialized without a hitch.
- *
- * @param mixed $in Input
- * @param bool $force Force rebuild?
- * @return array lessphp cache structure
- */
- public function cachedCompile($in, $force = false) {
- // assume no root
- $root = null;
-
- if (is_string($in)) {
- $root = $in;
- } elseif (is_array($in) and isset($in['root'])) {
- if ($force or ! isset($in['files'])) {
- // If we are forcing a recompile or if for some reason the
- // structure does not contain any file information we should
- // specify the root to trigger a rebuild.
- $root = $in['root'];
- } elseif (isset($in['files']) and is_array($in['files'])) {
- foreach ($in['files'] as $fname => $ftime ) {
- if (!file_exists($fname) or filemtime($fname) > $ftime) {
- // One of the files we knew about previously has changed
- // so we should look at our incoming root again.
- $root = $in['root'];
- break;
- }
- }
- }
- } else {
- // TODO: Throw an exception? We got neither a string nor something
- // that looks like a compatible lessphp cache structure.
- return null;
- }
-
- if ($root !== null) {
- // If we have a root value which means we should rebuild.
- $out = array();
- $out['root'] = $root;
- $out['compiled'] = $this->compileFile($root);
- $out['files'] = $this->allParsedFiles();
- $out['updated'] = time();
- return $out;
- } else {
- // No changes, pass back the structure
- // we were given initially.
- return $in;
- }
-
- }
-
- // parse and compile buffer
- // This is deprecated
- public function parse($str = null, $initialVariables = null) {
- if (is_array($str)) {
- $initialVariables = $str;
- $str = null;
- }
-
- $oldVars = $this->registeredVars;
- if ($initialVariables !== null) {
- $this->setVariables($initialVariables);
- }
-
- if ($str == null) {
- if (empty($this->_parseFile)) {
- throw new exception("nothing to parse");
- }
-
- $out = $this->compileFile($this->_parseFile);
- } else {
- $out = $this->compile($str);
- }
-
- $this->registeredVars = $oldVars;
- return $out;
- }
-
- protected function makeParser($name) {
- $parser = new lessc_parser($this, $name);
- $parser->writeComments = $this->preserveComments;
-
- return $parser;
- }
-
- public function setFormatter($name) {
- $this->formatterName = $name;
- }
-
- protected function newFormatter() {
- $className = "lessc_formatter_lessjs";
- if (!empty($this->formatterName)) {
- if (!is_string($this->formatterName))
- return $this->formatterName;
- $className = "lessc_formatter_$this->formatterName";
- }
-
- return new $className;
- }
-
- public function setPreserveComments($preserve) {
- $this->preserveComments = $preserve;
- }
-
- public function registerFunction($name, $func) {
- $this->libFunctions[$name] = $func;
- }
-
- public function unregisterFunction($name) {
- unset($this->libFunctions[$name]);
- }
-
- public function setVariables($variables) {
- $this->registeredVars = array_merge($this->registeredVars, $variables);
- }
-
- public function unsetVariable($name) {
- unset($this->registeredVars[$name]);
- }
-
- public function setImportDir($dirs) {
- $this->importDir = (array)$dirs;
- }
-
- public function addImportDir($dir) {
- $this->importDir = (array)$this->importDir;
- $this->importDir[] = $dir;
- }
-
- public function allParsedFiles() {
- return $this->allParsedFiles;
- }
-
- protected function addParsedFile($file) {
- $this->allParsedFiles[realpath($file)] = filemtime($file);
- }
-
- /**
- * Uses the current value of $this->count to show line and line number
- */
- protected function throwError($msg = null) {
- if ($this->sourceLoc >= 0) {
- $this->sourceParser->throwError($msg, $this->sourceLoc);
- }
- throw new exception($msg);
- }
-
- // compile file $in to file $out if $in is newer than $out
- // returns true when it compiles, false otherwise
- public static function ccompile($in, $out, $less = null) {
- if ($less === null) {
- $less = new self;
- }
- return $less->checkedCompile($in, $out);
- }
-
- public static function cexecute($in, $force = false, $less = null) {
- if ($less === null) {
- $less = new self;
- }
- return $less->cachedCompile($in, $force);
- }
-
- static protected $cssColors = array(
- 'aliceblue' => '240,248,255',
- 'antiquewhite' => '250,235,215',
- 'aqua' => '0,255,255',
- 'aquamarine' => '127,255,212',
- 'azure' => '240,255,255',
- 'beige' => '245,245,220',
- 'bisque' => '255,228,196',
- 'black' => '0,0,0',
- 'blanchedalmond' => '255,235,205',
- 'blue' => '0,0,255',
- 'blueviolet' => '138,43,226',
- 'brown' => '165,42,42',
- 'burlywood' => '222,184,135',
- 'cadetblue' => '95,158,160',
- 'chartreuse' => '127,255,0',
- 'chocolate' => '210,105,30',
- 'coral' => '255,127,80',
- 'cornflowerblue' => '100,149,237',
- 'cornsilk' => '255,248,220',
- 'crimson' => '220,20,60',
- 'cyan' => '0,255,255',
- 'darkblue' => '0,0,139',
- 'darkcyan' => '0,139,139',
- 'darkgoldenrod' => '184,134,11',
- 'darkgray' => '169,169,169',
- 'darkgreen' => '0,100,0',
- 'darkgrey' => '169,169,169',
- 'darkkhaki' => '189,183,107',
- 'darkmagenta' => '139,0,139',
- 'darkolivegreen' => '85,107,47',
- 'darkorange' => '255,140,0',
- 'darkorchid' => '153,50,204',
- 'darkred' => '139,0,0',
- 'darksalmon' => '233,150,122',
- 'darkseagreen' => '143,188,143',
- 'darkslateblue' => '72,61,139',
- 'darkslategray' => '47,79,79',
- 'darkslategrey' => '47,79,79',
- 'darkturquoise' => '0,206,209',
- 'darkviolet' => '148,0,211',
- 'deeppink' => '255,20,147',
- 'deepskyblue' => '0,191,255',
- 'dimgray' => '105,105,105',
- 'dimgrey' => '105,105,105',
- 'dodgerblue' => '30,144,255',
- 'firebrick' => '178,34,34',
- 'floralwhite' => '255,250,240',
- 'forestgreen' => '34,139,34',
- 'fuchsia' => '255,0,255',
- 'gainsboro' => '220,220,220',
- 'ghostwhite' => '248,248,255',
- 'gold' => '255,215,0',
- 'goldenrod' => '218,165,32',
- 'gray' => '128,128,128',
- 'green' => '0,128,0',
- 'greenyellow' => '173,255,47',
- 'grey' => '128,128,128',
- 'honeydew' => '240,255,240',
- 'hotpink' => '255,105,180',
- 'indianred' => '205,92,92',
- 'indigo' => '75,0,130',
- 'ivory' => '255,255,240',
- 'khaki' => '240,230,140',
- 'lavender' => '230,230,250',
- 'lavenderblush' => '255,240,245',
- 'lawngreen' => '124,252,0',
- 'lemonchiffon' => '255,250,205',
- 'lightblue' => '173,216,230',
- 'lightcoral' => '240,128,128',
- 'lightcyan' => '224,255,255',
- 'lightgoldenrodyellow' => '250,250,210',
- 'lightgray' => '211,211,211',
- 'lightgreen' => '144,238,144',
- 'lightgrey' => '211,211,211',
- 'lightpink' => '255,182,193',
- 'lightsalmon' => '255,160,122',
- 'lightseagreen' => '32,178,170',
- 'lightskyblue' => '135,206,250',
- 'lightslategray' => '119,136,153',
- 'lightslategrey' => '119,136,153',
- 'lightsteelblue' => '176,196,222',
- 'lightyellow' => '255,255,224',
- 'lime' => '0,255,0',
- 'limegreen' => '50,205,50',
- 'linen' => '250,240,230',
- 'magenta' => '255,0,255',
- 'maroon' => '128,0,0',
- 'mediumaquamarine' => '102,205,170',
- 'mediumblue' => '0,0,205',
- 'mediumorchid' => '186,85,211',
- 'mediumpurple' => '147,112,219',
- 'mediumseagreen' => '60,179,113',
- 'mediumslateblue' => '123,104,238',
- 'mediumspringgreen' => '0,250,154',
- 'mediumturquoise' => '72,209,204',
- 'mediumvioletred' => '199,21,133',
- 'midnightblue' => '25,25,112',
- 'mintcream' => '245,255,250',
- 'mistyrose' => '255,228,225',
- 'moccasin' => '255,228,181',
- 'navajowhite' => '255,222,173',
- 'navy' => '0,0,128',
- 'oldlace' => '253,245,230',
- 'olive' => '128,128,0',
- 'olivedrab' => '107,142,35',
- 'orange' => '255,165,0',
- 'orangered' => '255,69,0',
- 'orchid' => '218,112,214',
- 'palegoldenrod' => '238,232,170',
- 'palegreen' => '152,251,152',
- 'paleturquoise' => '175,238,238',
- 'palevioletred' => '219,112,147',
- 'papayawhip' => '255,239,213',
- 'peachpuff' => '255,218,185',
- 'peru' => '205,133,63',
- 'pink' => '255,192,203',
- 'plum' => '221,160,221',
- 'powderblue' => '176,224,230',
- 'purple' => '128,0,128',
- 'red' => '255,0,0',
- 'rosybrown' => '188,143,143',
- 'royalblue' => '65,105,225',
- 'saddlebrown' => '139,69,19',
- 'salmon' => '250,128,114',
- 'sandybrown' => '244,164,96',
- 'seagreen' => '46,139,87',
- 'seashell' => '255,245,238',
- 'sienna' => '160,82,45',
- 'silver' => '192,192,192',
- 'skyblue' => '135,206,235',
- 'slateblue' => '106,90,205',
- 'slategray' => '112,128,144',
- 'slategrey' => '112,128,144',
- 'snow' => '255,250,250',
- 'springgreen' => '0,255,127',
- 'steelblue' => '70,130,180',
- 'tan' => '210,180,140',
- 'teal' => '0,128,128',
- 'thistle' => '216,191,216',
- 'tomato' => '255,99,71',
- 'transparent' => '0,0,0,0',
- 'turquoise' => '64,224,208',
- 'violet' => '238,130,238',
- 'wheat' => '245,222,179',
- 'white' => '255,255,255',
- 'whitesmoke' => '245,245,245',
- 'yellow' => '255,255,0',
- 'yellowgreen' => '154,205,50'
- );
-}
-
-// responsible for taking a string of LESS code and converting it into a
-// syntax tree
-class lessc_parser {
- static protected $nextBlockId = 0; // used to uniquely identify blocks
-
- static protected $precedence = array(
- '=<' => 0,
- '>=' => 0,
- '=' => 0,
- '<' => 0,
- '>' => 0,
-
- '+' => 1,
- '-' => 1,
- '*' => 2,
- '/' => 2,
- '%' => 2,
- );
-
- static protected $whitePattern;
- static protected $commentMulti;
-
- static protected $commentSingle = "//";
- static protected $commentMultiLeft = "/*";
- static protected $commentMultiRight = "*/";
-
- // regex string to match any of the operators
- static protected $operatorString;
-
- // these properties will supress division unless it's inside parenthases
- static protected $supressDivisionProps =
- array('/border-radius$/i', '/^font$/i');
-
- protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport");
- protected $lineDirectives = array("charset");
-
- /**
- * if we are in parens we can be more liberal with whitespace around
- * operators because it must evaluate to a single value and thus is less
- * ambiguous.
- *
- * Consider:
- * property1: 10 -5; // is two numbers, 10 and -5
- * property2: (10 -5); // should evaluate to 5
- */
- protected $inParens = false;
-
- // caches preg escaped literals
- static protected $literalCache = array();
-
- public function __construct($lessc, $sourceName = null) {
- $this->eatWhiteDefault = true;
- // reference to less needed for vPrefix, mPrefix, and parentSelector
- $this->lessc = $lessc;
-
- $this->sourceName = $sourceName; // name used for error messages
-
- $this->writeComments = false;
-
- if (!self::$operatorString) {
- self::$operatorString =
- '('.implode('|', array_map(array('lessc', 'preg_quote'),
- array_keys(self::$precedence))).')';
-
- $commentSingle = lessc::preg_quote(self::$commentSingle);
- $commentMultiLeft = lessc::preg_quote(self::$commentMultiLeft);
- $commentMultiRight = lessc::preg_quote(self::$commentMultiRight);
-
- self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight;
- self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais';
- }
- }
-
- public function parse($buffer) {
- $this->count = 0;
- $this->line = 1;
-
- $this->env = null; // block stack
- $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer);
- $this->pushSpecialBlock("root");
- $this->eatWhiteDefault = true;
- $this->seenComments = array();
-
- // trim whitespace on head
- // if (preg_match('/^\s+/', $this->buffer, $m)) {
- // $this->line += substr_count($m[0], "\n");
- // $this->buffer = ltrim($this->buffer);
- // }
- $this->whitespace();
-
- // parse the entire file
- $lastCount = $this->count;
- while (false !== $this->parseChunk());
-
- if ($this->count != strlen($this->buffer))
- $this->throwError();
-
- // TODO report where the block was opened
- if (!property_exists($this->env, 'parent') || !is_null($this->env->parent))
- throw new exception('parse error: unclosed block');
-
- return $this->env;
- }
-
- /**
- * Parse a single chunk off the head of the buffer and append it to the
- * current parse environment.
- * Returns false when the buffer is empty, or when there is an error.
- *
- * This function is called repeatedly until the entire document is
- * parsed.
- *
- * This parser is most similar to a recursive descent parser. Single
- * functions represent discrete grammatical rules for the language, and
- * they are able to capture the text that represents those rules.
- *
- * Consider the function lessc::keyword(). (all parse functions are
- * structured the same)
- *
- * The function takes a single reference argument. When calling the
- * function it will attempt to match a keyword on the head of the buffer.
- * If it is successful, it will place the keyword in the referenced
- * argument, advance the position in the buffer, and return true. If it
- * fails then it won't advance the buffer and it will return false.
- *
- * All of these parse functions are powered by lessc::match(), which behaves
- * the same way, but takes a literal regular expression. Sometimes it is
- * more convenient to use match instead of creating a new function.
- *
- * Because of the format of the functions, to parse an entire string of
- * grammatical rules, you can chain them together using &&.
- *
- * But, if some of the rules in the chain succeed before one fails, then
- * the buffer position will be left at an invalid state. In order to
- * avoid this, lessc::seek() is used to remember and set buffer positions.
- *
- * Before parsing a chain, use $s = $this->seek() to remember the current
- * position into $s. Then if a chain fails, use $this->seek($s) to
- * go back where we started.
- */
- protected function parseChunk() {
- if (empty($this->buffer)) return false;
- $s = $this->seek();
-
- // setting a property
- if ($this->keyword($key) && $this->assign() &&
- $this->propertyValue($value, $key) && $this->end())
- {
- $this->append(array('assign', $key, $value), $s);
- return true;
- } else {
- $this->seek($s);
- }
-
-
- // look for special css blocks
- if ($this->literal('@', false)) {
- $this->count--;
-
- // media
- if ($this->literal('@media')) {
- if (($this->mediaQueryList($mediaQueries) || true)
- && $this->literal('{'))
- {
- $media = $this->pushSpecialBlock("media");
- $media->queries = is_null($mediaQueries) ? array() : $mediaQueries;
- return true;
- } else {
- $this->seek($s);
- return false;
- }
- }
-
- if ($this->literal("@", false) && $this->keyword($dirName)) {
- if ($this->isDirective($dirName, $this->blockDirectives)) {
- if (($this->openString("{", $dirValue, null, array(";")) || true) &&
- $this->literal("{"))
- {
- $dir = $this->pushSpecialBlock("directive");
- $dir->name = $dirName;
- if (isset($dirValue)) $dir->value = $dirValue;
- return true;
- }
- } elseif ($this->isDirective($dirName, $this->lineDirectives)) {
- if ($this->propertyValue($dirValue) && $this->end()) {
- $this->append(array("directive", $dirName, $dirValue));
- return true;
- }
- }
- }
-
- $this->seek($s);
- }
-
- // setting a variable
- if ($this->variable($var) && $this->assign() &&
- $this->propertyValue($value) && $this->end())
- {
- $this->append(array('assign', $var, $value), $s);
- return true;
- } else {
- $this->seek($s);
- }
-
- if ($this->import($importValue)) {
- $this->append($importValue, $s);
- return true;
- }
-
- // opening parametric mixin
- if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) &&
- ($this->guards($guards) || true) &&
- $this->literal('{'))
- {
- $block = $this->pushBlock($this->fixTags(array($tag)));
- $block->args = $args;
- $block->isVararg = $isVararg;
- if (!empty($guards)) $block->guards = $guards;
- return true;
- } else {
- $this->seek($s);
- }
-
- // opening a simple block
- if ($this->tags($tags) && $this->literal('{')) {
- $tags = $this->fixTags($tags);
- $this->pushBlock($tags);
- return true;
- } else {
- $this->seek($s);
- }
-
- // closing a block
- if ($this->literal('}', false)) {
- try {
- $block = $this->pop();
- } catch (exception $e) {
- $this->seek($s);
- $this->throwError($e->getMessage());
- }
-
- $hidden = false;
- if (is_null($block->type)) {
- $hidden = true;
- if (!isset($block->args)) {
- foreach ($block->tags as $tag) {
- if (!is_string($tag) || $tag{0} != $this->lessc->mPrefix) {
- $hidden = false;
- break;
- }
- }
- }
-
- foreach ($block->tags as $tag) {
- if (is_string($tag)) {
- $this->env->children[$tag][] = $block;
- }
- }
- }
-
- if (!$hidden) {
- $this->append(array('block', $block), $s);
- }
-
- // this is done here so comments aren't bundled into he block that
- // was just closed
- $this->whitespace();
- return true;
- }
-
- // mixin
- if ($this->mixinTags($tags) &&
- ($this->argumentDef($argv, $isVararg) || true) &&
- ($this->keyword($suffix) || true) && $this->end())
- {
- $tags = $this->fixTags($tags);
- $this->append(array('mixin', $tags, $argv, $suffix), $s);
- return true;
- } else {
- $this->seek($s);
- }
-
- // spare ;
- if ($this->literal(';')) return true;
-
- return false; // got nothing, throw error
- }
-
- protected function isDirective($dirname, $directives) {
- // TODO: cache pattern in parser
- $pattern = implode("|",
- array_map(array("lessc", "preg_quote"), $directives));
- $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i';
-
- return preg_match($pattern, $dirname);
- }
-
- protected function fixTags($tags) {
- // move @ tags out of variable namespace
- foreach ($tags as &$tag) {
- if ($tag{0} == $this->lessc->vPrefix)
- $tag[0] = $this->lessc->mPrefix;
- }
- return $tags;
- }
-
- // a list of expressions
- protected function expressionList(&$exps) {
- $values = array();
-
- while ($this->expression($exp)) {
- $values[] = $exp;
- }
-
- if (count($values) == 0) return false;
-
- $exps = lessc::compressList($values, ' ');
- return true;
- }
-
- /**
- * Attempt to consume an expression.
- * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
- */
- protected function expression(&$out) {
- if ($this->value($lhs)) {
- $out = $this->expHelper($lhs, 0);
-
- // look for / shorthand
- if (!empty($this->env->supressedDivision)) {
- unset($this->env->supressedDivision);
- $s = $this->seek();
- if ($this->literal("/") && $this->value($rhs)) {
- $out = array("list", "",
- array($out, array("keyword", "/"), $rhs));
- } else {
- $this->seek($s);
- }
- }
-
- return true;
- }
- return false;
- }
-
- /**
- * recursively parse infix equation with $lhs at precedence $minP
- */
- protected function expHelper($lhs, $minP) {
- $this->inExp = true;
- $ss = $this->seek();
-
- while (true) {
- $whiteBefore = isset($this->buffer[$this->count - 1]) &&
- ctype_space($this->buffer[$this->count - 1]);
-
- // If there is whitespace before the operator, then we require
- // whitespace after the operator for it to be an expression
- $needWhite = $whiteBefore && !$this->inParens;
-
- if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) {
- if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) {
- foreach (self::$supressDivisionProps as $pattern) {
- if (preg_match($pattern, $this->env->currentProperty)) {
- $this->env->supressedDivision = true;
- break 2;
- }
- }
- }
-
-
- $whiteAfter = isset($this->buffer[$this->count - 1]) &&
- ctype_space($this->buffer[$this->count - 1]);
-
- if (!$this->value($rhs)) break;
-
- // peek for next operator to see what to do with rhs
- if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) {
- $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
- }
-
- $lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter);
- $ss = $this->seek();
-
- continue;
- }
-
- break;
- }
-
- $this->seek($ss);
-
- return $lhs;
- }
-
- // consume a list of values for a property
- public function propertyValue(&$value, $keyName = null) {
- $values = array();
-
- if ($keyName !== null) $this->env->currentProperty = $keyName;
-
- $s = null;
- while ($this->expressionList($v)) {
- $values[] = $v;
- $s = $this->seek();
- if (!$this->literal(',')) break;
- }
-
- if ($s) $this->seek($s);
-
- if ($keyName !== null) unset($this->env->currentProperty);
-
- if (count($values) == 0) return false;
-
- $value = lessc::compressList($values, ', ');
- return true;
- }
-
- protected function parenValue(&$out) {
- $s = $this->seek();
-
- // speed shortcut
- if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") {
- return false;
- }
-
- $inParens = $this->inParens;
- if ($this->literal("(") &&
- ($this->inParens = true) && $this->expression($exp) &&
- $this->literal(")"))
- {
- $out = $exp;
- $this->inParens = $inParens;
- return true;
- } else {
- $this->inParens = $inParens;
- $this->seek($s);
- }
-
- return false;
- }
-
- // a single value
- protected function value(&$value) {
- $s = $this->seek();
-
- // speed shortcut
- if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") {
- // negation
- if ($this->literal("-", false) &&
- (($this->variable($inner) && $inner = array("variable", $inner)) ||
- $this->unit($inner) ||
- $this->parenValue($inner)))
- {
- $value = array("unary", "-", $inner);
- return true;
- } else {
- $this->seek($s);
- }
- }
-
- if ($this->parenValue($value)) return true;
- if ($this->unit($value)) return true;
- if ($this->color($value)) return true;
- if ($this->func($value)) return true;
- if ($this->string($value)) return true;
-
- if ($this->keyword($word)) {
- $value = array('keyword', $word);
- return true;
- }
-
- // try a variable
- if ($this->variable($var)) {
- $value = array('variable', $var);
- return true;
- }
-
- // unquote string (should this work on any type?
- if ($this->literal("~") && $this->string($str)) {
- $value = array("escape", $str);
- return true;
- } else {
- $this->seek($s);
- }
-
- // css hack: \0
- if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
- $value = array('keyword', '\\'.$m[1]);
- return true;
- } else {
- $this->seek($s);
- }
-
- return false;
- }
-
- // an import statement
- protected function import(&$out) {
- $s = $this->seek();
- if (!$this->literal('@import')) return false;
-
- // @import "something.css" media;
- // @import url("something.css") media;
- // @import url(something.css) media;
-
- if ($this->propertyValue($value)) {
- $out = array("import", $value);
- return true;
- }
- }
-
- protected function mediaQueryList(&$out) {
- if ($this->genericList($list, "mediaQuery", ",", false)) {
- $out = $list[2];
- return true;
- }
- return false;
- }
-
- protected function mediaQuery(&$out) {
- $s = $this->seek();
-
- $expressions = null;
- $parts = array();
-
- if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) {
- $prop = array("mediaType");
- if (isset($only)) $prop[] = "only";
- if (isset($not)) $prop[] = "not";
- $prop[] = $mediaType;
- $parts[] = $prop;
- } else {
- $this->seek($s);
- }
-
-
- if (!empty($mediaType) && !$this->literal("and")) {
- // ~
- } else {
- $this->genericList($expressions, "mediaExpression", "and", false);
- if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]);
- }
-
- if (count($parts) == 0) {
- $this->seek($s);
- return false;
- }
-
- $out = $parts;
- return true;
- }
-
- protected function mediaExpression(&$out) {
- $s = $this->seek();
- $value = null;
- if ($this->literal("(") &&
- $this->keyword($feature) &&
- ($this->literal(":") && $this->expression($value) || true) &&
- $this->literal(")"))
- {
- $out = array("mediaExp", $feature);
- if ($value) $out[] = $value;
- return true;
- } elseif ($this->variable($variable)) {
- $out = array('variable', $variable);
- return true;
- }
-
- $this->seek($s);
- return false;
- }
-
- // an unbounded string stopped by $end
- protected function openString($end, &$out, $nestingOpen=null, $rejectStrs = null) {
- $oldWhite = $this->eatWhiteDefault;
- $this->eatWhiteDefault = false;
-
- $stop = array("'", '"', "@{", $end);
- $stop = array_map(array("lessc", "preg_quote"), $stop);
- // $stop[] = self::$commentMulti;
-
- if (!is_null($rejectStrs)) {
- $stop = array_merge($stop, $rejectStrs);
- }
-
- $patt = '(.*?)('.implode("|", $stop).')';
-
- $nestingLevel = 0;
-
- $content = array();
- while ($this->match($patt, $m, false)) {
- if (!empty($m[1])) {
- $content[] = $m[1];
- if ($nestingOpen) {
- $nestingLevel += substr_count($m[1], $nestingOpen);
- }
- }
-
- $tok = $m[2];
-
- $this->count-= strlen($tok);
- if ($tok == $end) {
- if ($nestingLevel == 0) {
- break;
- } else {
- $nestingLevel--;
- }
- }
-
- if (($tok == "'" || $tok == '"') && $this->string($str)) {
- $content[] = $str;
- continue;
- }
-
- if ($tok == "@{" && $this->interpolation($inter)) {
- $content[] = $inter;
- continue;
- }
-
- if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) {
- break;
- }
-
- $content[] = $tok;
- $this->count+= strlen($tok);
- }
-
- $this->eatWhiteDefault = $oldWhite;
-
- if (count($content) == 0) return false;
-
- // trim the end
- if (is_string(end($content))) {
- $content[count($content) - 1] = rtrim(end($content));
- }
-
- $out = array("string", "", $content);
- return true;
- }
-
- protected function string(&$out) {
- $s = $this->seek();
- if ($this->literal('"', false)) {
- $delim = '"';
- } elseif ($this->literal("'", false)) {
- $delim = "'";
- } else {
- return false;
- }
-
- $content = array();
-
- // look for either ending delim , escape, or string interpolation
- $patt = '([^\n]*?)(@\{|\\\\|' .
- lessc::preg_quote($delim).')';
-
- $oldWhite = $this->eatWhiteDefault;
- $this->eatWhiteDefault = false;
-
- while ($this->match($patt, $m, false)) {
- $content[] = $m[1];
- if ($m[2] == "@{") {
- $this->count -= strlen($m[2]);
- if ($this->interpolation($inter, false)) {
- $content[] = $inter;
- } else {
- $this->count += strlen($m[2]);
- $content[] = "@{"; // ignore it
- }
- } elseif ($m[2] == '\\') {
- $content[] = $m[2];
- if ($this->literal($delim, false)) {
- $content[] = $delim;
- }
- } else {
- $this->count -= strlen($delim);
- break; // delim
- }
- }
-
- $this->eatWhiteDefault = $oldWhite;
-
- if ($this->literal($delim)) {
- $out = array("string", $delim, $content);
- return true;
- }
-
- $this->seek($s);
- return false;
- }
-
- protected function interpolation(&$out) {
- $oldWhite = $this->eatWhiteDefault;
- $this->eatWhiteDefault = true;
-
- $s = $this->seek();
- if ($this->literal("@{") &&
- $this->openString("}", $interp, null, array("'", '"', ";")) &&
- $this->literal("}", false))
- {
- $out = array("interpolate", $interp);
- $this->eatWhiteDefault = $oldWhite;
- if ($this->eatWhiteDefault) $this->whitespace();
- return true;
- }
-
- $this->eatWhiteDefault = $oldWhite;
- $this->seek($s);
- return false;
- }
-
- protected function unit(&$unit) {
- // speed shortcut
- if (isset($this->buffer[$this->count])) {
- $char = $this->buffer[$this->count];
- if (!ctype_digit($char) && $char != ".") return false;
- }
-
- if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) {
- $unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]);
- return true;
- }
- return false;
- }
-
- // a # color
- protected function color(&$out) {
- if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) {
- if (strlen($m[1]) > 7) {
- $out = array("string", "", array($m[1]));
- } else {
- $out = array("raw_color", $m[1]);
- }
- return true;
- }
-
- return false;
- }
-
- // consume an argument definition list surrounded by ()
- // each argument is a variable name with optional value
- // or at the end a ... or a variable named followed by ...
- // arguments are separated by , unless a ; is in the list, then ; is the
- // delimiter.
- protected function argumentDef(&$args, &$isVararg) {
- $s = $this->seek();
- if (!$this->literal('(')) return false;
-
- $values = array();
- $delim = ",";
- $method = "expressionList";
-
- $isVararg = false;
- while (true) {
- if ($this->literal("...")) {
- $isVararg = true;
- break;
- }
-
- if ($this->$method($value)) {
- if ($value[0] == "variable") {
- $arg = array("arg", $value[1]);
- $ss = $this->seek();
-
- if ($this->assign() && $this->$method($rhs)) {
- $arg[] = $rhs;
- } else {
- $this->seek($ss);
- if ($this->literal("...")) {
- $arg[0] = "rest";
- $isVararg = true;
- }
- }
-
- $values[] = $arg;
- if ($isVararg) break;
- continue;
- } else {
- $values[] = array("lit", $value);
- }
- }
-
-
- if (!$this->literal($delim)) {
- if ($delim == "," && $this->literal(";")) {
- // found new delim, convert existing args
- $delim = ";";
- $method = "propertyValue";
-
- // transform arg list
- if (isset($values[1])) { // 2 items
- $newList = array();
- foreach ($values as $i => $arg) {
- switch($arg[0]) {
- case "arg":
- if ($i) {
- $this->throwError("Cannot mix ; and , as delimiter types");
- }
- $newList[] = $arg[2];
- break;
- case "lit":
- $newList[] = $arg[1];
- break;
- case "rest":
- $this->throwError("Unexpected rest before semicolon");
- }
- }
-
- $newList = array("list", ", ", $newList);
-
- switch ($values[0][0]) {
- case "arg":
- $newArg = array("arg", $values[0][1], $newList);
- break;
- case "lit":
- $newArg = array("lit", $newList);
- break;
- }
-
- } elseif ($values) { // 1 item
- $newArg = $values[0];
- }
-
- if ($newArg) {
- $values = array($newArg);
- }
- } else {
- break;
- }
- }
- }
-
- if (!$this->literal(')')) {
- $this->seek($s);
- return false;
- }
-
- $args = $values;
-
- return true;
- }
-
- // consume a list of tags
- // this accepts a hanging delimiter
- protected function tags(&$tags, $simple = false, $delim = ',') {
- $tags = array();
- while ($this->tag($tt, $simple)) {
- $tags[] = $tt;
- if (!$this->literal($delim)) break;
- }
- if (count($tags) == 0) return false;
-
- return true;
- }
-
- // list of tags of specifying mixin path
- // optionally separated by > (lazy, accepts extra >)
- protected function mixinTags(&$tags) {
- $s = $this->seek();
- $tags = array();
- while ($this->tag($tt, true)) {
- $tags[] = $tt;
- $this->literal(">");
- }
-
- if (count($tags) == 0) return false;
-
- return true;
- }
-
- // a bracketed value (contained within in a tag definition)
- protected function tagBracket(&$parts, &$hasExpression) {
- // speed shortcut
- if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") {
- return false;
- }
-
- $s = $this->seek();
-
- $hasInterpolation = false;
-
- if ($this->literal("[", false)) {
- $attrParts = array("[");
- // keyword, string, operator
- while (true) {
- if ($this->literal("]", false)) {
- $this->count--;
- break; // get out early
- }
-
- if ($this->match('\s+', $m)) {
- $attrParts[] = " ";
- continue;
- }
- if ($this->string($str)) {
- // escape parent selector, (yuck)
- foreach ($str[2] as &$chunk) {
- $chunk = str_replace($this->lessc->parentSelector, "$&$", $chunk);
- }
-
- $attrParts[] = $str;
- $hasInterpolation = true;
- continue;
- }
-
- if ($this->keyword($word)) {
- $attrParts[] = $word;
- continue;
- }
-
- if ($this->interpolation($inter, false)) {
- $attrParts[] = $inter;
- $hasInterpolation = true;
- continue;
- }
-
- // operator, handles attr namespace too
- if ($this->match('[|-~\$\*\^=]+', $m)) {
- $attrParts[] = $m[0];
- continue;
- }
-
- break;
- }
-
- if ($this->literal("]", false)) {
- $attrParts[] = "]";
- foreach ($attrParts as $part) {
- $parts[] = $part;
- }
- $hasExpression = $hasExpression || $hasInterpolation;
- return true;
- }
- $this->seek($s);
- }
-
- $this->seek($s);
- return false;
- }
-
- // a space separated list of selectors
- protected function tag(&$tag, $simple = false) {
- if ($simple)
- $chars = '^@,:;{}\][>\(\) "\'';
- else
- $chars = '^@,;{}["\'';
-
- $s = $this->seek();
-
- $hasExpression = false;
- $parts = array();
- while ($this->tagBracket($parts, $hasExpression));
-
- $oldWhite = $this->eatWhiteDefault;
- $this->eatWhiteDefault = false;
-
- while (true) {
- if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) {
- $parts[] = $m[1];
- if ($simple) break;
-
- while ($this->tagBracket($parts, $hasExpression));
- continue;
- }
-
- if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") {
- if ($this->interpolation($interp)) {
- $hasExpression = true;
- $interp[2] = true; // don't unescape
- $parts[] = $interp;
- continue;
- }
-
- if ($this->literal("@")) {
- $parts[] = "@";
- continue;
- }
- }
-
- if ($this->unit($unit)) { // for keyframes
- $parts[] = $unit[1];
- $parts[] = $unit[2];
- continue;
- }
-
- break;
- }
-
- $this->eatWhiteDefault = $oldWhite;
- if (!$parts) {
- $this->seek($s);
- return false;
- }
-
- if ($hasExpression) {
- $tag = array("exp", array("string", "", $parts));
- } else {
- $tag = trim(implode($parts));
- }
-
- $this->whitespace();
- return true;
- }
-
- // a css function
- protected function func(&$func) {
- $s = $this->seek();
-
- if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) {
- $fname = $m[1];
-
- $sPreArgs = $this->seek();
-
- $args = array();
- while (true) {
- $ss = $this->seek();
- // this ugly nonsense is for ie filter properties
- if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
- $args[] = array("string", "", array($name, "=", $value));
- } else {
- $this->seek($ss);
- if ($this->expressionList($value)) {
- $args[] = $value;
- }
- }
-
- if (!$this->literal(',')) break;
- }
- $args = array('list', ',', $args);
-
- if ($this->literal(')')) {
- $func = array('function', $fname, $args);
- return true;
- } elseif ($fname == 'url') {
- // couldn't parse and in url? treat as string
- $this->seek($sPreArgs);
- if ($this->openString(")", $string) && $this->literal(")")) {
- $func = array('function', $fname, $string);
- return true;
- }
- }
- }
-
- $this->seek($s);
- return false;
- }
-
- // consume a less variable
- protected function variable(&$name) {
- $s = $this->seek();
- if ($this->literal($this->lessc->vPrefix, false) &&
- ($this->variable($sub) || $this->keyword($name)))
- {
- if (!empty($sub)) {
- $name = array('variable', $sub);
- } else {
- $name = $this->lessc->vPrefix.$name;
- }
- return true;
- }
-
- $name = null;
- $this->seek($s);
- return false;
- }
-
- /**
- * Consume an assignment operator
- * Can optionally take a name that will be set to the current property name
- */
- protected function assign($name = null) {
- if ($name) $this->currentProperty = $name;
- return $this->literal(':') || $this->literal('=');
- }
-
- // consume a keyword
- protected function keyword(&$word) {
- if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
- $word = $m[1];
- return true;
- }
- return false;
- }
-
- // consume an end of statement delimiter
- protected function end() {
- if ($this->literal(';')) {
- return true;
- } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
- // if there is end of file or a closing block next then we don't need a ;
- return true;
- }
- return false;
- }
-
- protected function guards(&$guards) {
- $s = $this->seek();
-
- if (!$this->literal("when")) {
- $this->seek($s);
- return false;
- }
-
- $guards = array();
-
- while ($this->guardGroup($g)) {
- $guards[] = $g;
- if (!$this->literal(",")) break;
- }
-
- if (count($guards) == 0) {
- $guards = null;
- $this->seek($s);
- return false;
- }
-
- return true;
- }
-
- // a bunch of guards that are and'd together
- // TODO rename to guardGroup
- protected function guardGroup(&$guardGroup) {
- $s = $this->seek();
- $guardGroup = array();
- while ($this->guard($guard)) {
- $guardGroup[] = $guard;
- if (!$this->literal("and")) break;
- }
-
- if (count($guardGroup) == 0) {
- $guardGroup = null;
- $this->seek($s);
- return false;
- }
-
- return true;
- }
-
- protected function guard(&$guard) {
- $s = $this->seek();
- $negate = $this->literal("not");
-
- if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) {
- $guard = $exp;
- if ($negate) $guard = array("negate", $guard);
- return true;
- }
-
- $this->seek($s);
- return false;
- }
-
- /* raw parsing functions */
-
- protected function literal($what, $eatWhitespace = null) {
- if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
-
- // shortcut on single letter
- if (!isset($what[1]) && isset($this->buffer[$this->count])) {
- if ($this->buffer[$this->count] == $what) {
- if (!$eatWhitespace) {
- $this->count++;
- return true;
- }
- // goes below...
- } else {
- return false;
- }
- }
-
- if (!isset(self::$literalCache[$what])) {
- self::$literalCache[$what] = lessc::preg_quote($what);
- }
-
- return $this->match(self::$literalCache[$what], $m, $eatWhitespace);
- }
-
- protected function genericList(&$out, $parseItem, $delim="", $flatten=true) {
- $s = $this->seek();
- $items = array();
- while ($this->$parseItem($value)) {
- $items[] = $value;
- if ($delim) {
- if (!$this->literal($delim)) break;
- }
- }
-
- if (count($items) == 0) {
- $this->seek($s);
- return false;
- }
-
- if ($flatten && count($items) == 1) {
- $out = $items[0];
- } else {
- $out = array("list", $delim, $items);
- }
-
- return true;
- }
-
-
- // advance counter to next occurrence of $what
- // $until - don't include $what in advance
- // $allowNewline, if string, will be used as valid char set
- protected function to($what, &$out, $until = false, $allowNewline = false) {
- if (is_string($allowNewline)) {
- $validChars = $allowNewline;
- } else {
- $validChars = $allowNewline ? "." : "[^\n]";
- }
- if (!$this->match('('.$validChars.'*?)'.lessc::preg_quote($what), $m, !$until)) return false;
- if ($until) $this->count -= strlen($what); // give back $what
- $out = $m[1];
- return true;
- }
-
- // try to match something on head of buffer
- protected function match($regex, &$out, $eatWhitespace = null) {
- if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
-
- $r = '/'.$regex.($eatWhitespace && !$this->writeComments ? '\s*' : '').'/Ais';
- if (preg_match($r, $this->buffer, $out, null, $this->count)) {
- $this->count += strlen($out[0]);
- if ($eatWhitespace && $this->writeComments) $this->whitespace();
- return true;
- }
- return false;
- }
-
- // match some whitespace
- protected function whitespace() {
- if ($this->writeComments) {
- $gotWhite = false;
- while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
- if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
- $this->append(array("comment", $m[1]));
- $this->commentsSeen[$this->count] = true;
- }
- $this->count += strlen($m[0]);
- $gotWhite = true;
- }
- return $gotWhite;
- } else {
- $this->match("", $m);
- return strlen($m[0]) > 0;
- }
- }
-
- // match something without consuming it
- protected function peek($regex, &$out = null, $from=null) {
- if (is_null($from)) $from = $this->count;
- $r = '/'.$regex.'/Ais';
- $result = preg_match($r, $this->buffer, $out, null, $from);
-
- return $result;
- }
-
- // seek to a spot in the buffer or return where we are on no argument
- protected function seek($where = null) {
- if ($where === null) return $this->count;
- else $this->count = $where;
- return true;
- }
-
- /* misc functions */
-
- public function throwError($msg = "parse error", $count = null) {
- $count = is_null($count) ? $this->count : $count;
-
- $line = $this->line +
- substr_count(substr($this->buffer, 0, $count), "\n");
-
- if (!empty($this->sourceName)) {
- $loc = "$this->sourceName on line $line";
- } else {
- $loc = "line: $line";
- }
-
- // TODO this depends on $this->count
- if ($this->peek("(.*?)(\n|$)", $m, $count)) {
- throw new exception("$msg: failed at `$m[1]` $loc");
- } else {
- throw new exception("$msg: $loc");
- }
- }
-
- protected function pushBlock($selectors=null, $type=null) {
- $b = new stdclass;
- $b->parent = $this->env;
-
- $b->type = $type;
- $b->id = self::$nextBlockId++;
-
- $b->isVararg = false; // TODO: kill me from here
- $b->tags = $selectors;
-
- $b->props = array();
- $b->children = array();
-
- $this->env = $b;
- return $b;
- }
-
- // push a block that doesn't multiply tags
- protected function pushSpecialBlock($type) {
- return $this->pushBlock(null, $type);
- }
-
- // append a property to the current block
- protected function append($prop, $pos = null) {
- if ($pos !== null) $prop[-1] = $pos;
- $this->env->props[] = $prop;
- }
-
- // pop something off the stack
- protected function pop() {
- $old = $this->env;
- $this->env = $this->env->parent;
- return $old;
- }
-
- // remove comments from $text
- // todo: make it work for all functions, not just url
- protected function removeComments($text) {
- $look = array(
- 'url(', '//', '/*', '"', "'"
- );
-
- $out = '';
- $min = null;
- while (true) {
- // find the next item
- foreach ($look as $token) {
- $pos = strpos($text, $token);
- if ($pos !== false) {
- if (!isset($min) || $pos < $min[1]) $min = array($token, $pos);
- }
- }
-
- if (is_null($min)) break;
-
- $count = $min[1];
- $skip = 0;
- $newlines = 0;
- switch ($min[0]) {
- case 'url(':
- if (preg_match('/url\(.*?\)/', $text, $m, 0, $count))
- $count += strlen($m[0]) - strlen($min[0]);
- break;
- case '"':
- case "'":
- if (preg_match('/'.$min[0].'.*?(?<!\\\\)'.$min[0].'/', $text, $m, 0, $count))
- $count += strlen($m[0]) - 1;
- break;
- case '//':
- $skip = strpos($text, "\n", $count);
- if ($skip === false) $skip = strlen($text) - $count;
- else $skip -= $count;
- break;
- case '/*':
- if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) {
- $skip = strlen($m[0]);
- $newlines = substr_count($m[0], "\n");
- }
- break;
- }
-
- if ($skip == 0) $count += strlen($min[0]);
-
- $out .= substr($text, 0, $count).str_repeat("\n", $newlines);
- $text = substr($text, $count + $skip);
-
- $min = null;
- }
-
- return $out.$text;
- }
-
-}
-
-class lessc_formatter_classic {
- public $indentChar = " ";
-
- public $break = "\n";
- public $open = " {";
- public $close = "}";
- public $selectorSeparator = ", ";
- public $assignSeparator = ":";
-
- public $openSingle = " { ";
- public $closeSingle = " }";
-
- public $disableSingle = false;
- public $breakSelectors = false;
-
- public $compressColors = false;
-
- public function __construct() {
- $this->indentLevel = 0;
- }
-
- public function indentStr($n = 0) {
- return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
- }
-
- public function property($name, $value) {
- return $name . $this->assignSeparator . $value . ";";
- }
-
- protected function isEmpty($block) {
- if (empty($block->lines)) {
- foreach ($block->children as $child) {
- if (!$this->isEmpty($child)) return false;
- }
-
- return true;
- }
- return false;
- }
-
- public function block($block) {
- if ($this->isEmpty($block)) return;
-
- $inner = $pre = $this->indentStr();
-
- $isSingle = !$this->disableSingle &&
- is_null($block->type) && count($block->lines) == 1;
-
- if (!empty($block->selectors)) {
- $this->indentLevel++;
-
- if ($this->breakSelectors) {
- $selectorSeparator = $this->selectorSeparator . $this->break . $pre;
- } else {
- $selectorSeparator = $this->selectorSeparator;
- }
-
- echo $pre .
- implode($selectorSeparator, $block->selectors);
- if ($isSingle) {
- echo $this->openSingle;
- $inner = "";
- } else {
- echo $this->open . $this->break;
- $inner = $this->indentStr();
- }
-
- }
-
- if (!empty($block->lines)) {
- $glue = $this->break.$inner;
- echo $inner . implode($glue, $block->lines);
- if (!$isSingle && !empty($block->children)) {
- echo $this->break;
- }
- }
-
- foreach ($block->children as $child) {
- $this->block($child);
- }
-
- if (!empty($block->selectors)) {
- if (!$isSingle && empty($block->children)) echo $this->break;
-
- if ($isSingle) {
- echo $this->closeSingle . $this->break;
- } else {
- echo $pre . $this->close . $this->break;
- }
-
- $this->indentLevel--;
- }
- }
-}
-
-class lessc_formatter_compressed extends lessc_formatter_classic {
- public $disableSingle = true;
- public $open = "{";
- public $selectorSeparator = ",";
- public $assignSeparator = ":";
- public $break = "";
- public $compressColors = true;
-
- public function indentStr($n = 0) {
- return "";
- }
-}
-
-class lessc_formatter_lessjs extends lessc_formatter_classic {
- public $disableSingle = true;
- public $breakSelectors = true;
- public $assignSeparator = ": ";
- public $selectorSeparator = ",";
-}
-
-
--- /dev/null
+<?php
+/**
+ * SCSSPHP
+ *
+ * Stub classes for backward compatibility
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+// @codingStandardsIgnoreStart
+/**
+ * @deprecated since 0.1.0
+ */
+class scssc extends \Leafo\ScssPhp\Compiler
+{
+}
+
+/**
+ * @deprecated since 0.1.0
+ */
+class scss_parser extends \Leafo\ScssPhp\Parser
+{
+}
+
+/**
+ * @deprecated since 0.1.0
+ */
+class scss_formatter extends \Leafo\ScssPhp\Formatter\Expanded
+{
+}
+
+/**
+ * @deprecated since 0.1.0
+ */
+class scss_formatter_nested extends \Leafo\ScssPhp\Formatter\Nested
+{
+}
+
+/**
+ * @deprecated since 0.1.0
+ */
+class scss_formatter_compressed extends \Leafo\ScssPhp\Formatter\Compressed
+{
+}
+
+/**
+ * @deprecated since 0.1.0
+ */
+class scss_formatter_crunched extends \Leafo\ScssPhp\Formatter\Crunched
+{
+}
+
+/**
+ * @deprecated since 0.1.0
+ */
+class scss_server extends \Leafo\ScssPhp\Server
+{
+}
+// @codingStandardsIgnoreEnd
--- /dev/null
+<?php
+if (version_compare(PHP_VERSION, '5.3') < 0) {
+ die('Requires PHP 5.3 or above');
+}
+
+if (! class_exists('scssc', false)) {
+ include_once __DIR__ . '/src/Base/Range.php';
+ include_once __DIR__ . '/src/Colors.php';
+ include_once __DIR__ . '/src/Compiler.php';
+ include_once __DIR__ . '/src/Formatter.php';
+ include_once __DIR__ . '/src/Formatter/Compact.php';
+ include_once __DIR__ . '/src/Formatter/Compressed.php';
+ include_once __DIR__ . '/src/Formatter/Crunched.php';
+ include_once __DIR__ . '/src/Formatter/Expanded.php';
+ include_once __DIR__ . '/src/Formatter/Nested.php';
+ include_once __DIR__ . '/src/Parser.php';
+ include_once __DIR__ . '/src/Util.php';
+ include_once __DIR__ . '/src/Version.php';
+ include_once __DIR__ . '/src/Server.php';
+ include_once __DIR__ . '/classmap.php';
+}
--- /dev/null
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp\Base;
+
+/**
+ * Range class
+ *
+ * @author Anthon Pang <anthon.pang@gmail.com>
+ */
+class Range
+{
+ public $first;
+ public $last;
+
+ /**
+ * Initialize range
+ *
+ * @param integer|float $first
+ * @param integer|float $last
+ */
+ public function __construct($first, $last)
+ {
+ $this->first = $first;
+ $this->last = $last;
+ }
+
+ /**
+ * Test for inclusion in range
+ *
+ * @param integer|float $value
+ *
+ * @return boolean
+ */
+ public function includes($value)
+ {
+ return $value >= $this->first && $value <= $this->last;
+ }
+}
--- /dev/null
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp;
+
+/**
+ * CSS Colors
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class Colors
+{
+ /**
+ * CSS Colors
+ *
+ * @see http://www.w3.org/TR/css3-color
+ */
+ public static $cssColors = array(
+ 'aliceblue' => '240,248,255',
+ 'antiquewhite' => '250,235,215',
+ 'aqua' => '0,255,255',
+ 'aquamarine' => '127,255,212',
+ 'azure' => '240,255,255',
+ 'beige' => '245,245,220',
+ 'bisque' => '255,228,196',
+ 'black' => '0,0,0',
+ 'blanchedalmond' => '255,235,205',
+ 'blue' => '0,0,255',
+ 'blueviolet' => '138,43,226',
+ 'brown' => '165,42,42',
+ 'burlywood' => '222,184,135',
+ 'cadetblue' => '95,158,160',
+ 'chartreuse' => '127,255,0',
+ 'chocolate' => '210,105,30',
+ 'coral' => '255,127,80',
+ 'cornflowerblue' => '100,149,237',
+ 'cornsilk' => '255,248,220',
+ 'crimson' => '220,20,60',
+ 'cyan' => '0,255,255',
+ 'darkblue' => '0,0,139',
+ 'darkcyan' => '0,139,139',
+ 'darkgoldenrod' => '184,134,11',
+ 'darkgray' => '169,169,169',
+ 'darkgreen' => '0,100,0',
+ 'darkgrey' => '169,169,169',
+ 'darkkhaki' => '189,183,107',
+ 'darkmagenta' => '139,0,139',
+ 'darkolivegreen' => '85,107,47',
+ 'darkorange' => '255,140,0',
+ 'darkorchid' => '153,50,204',
+ 'darkred' => '139,0,0',
+ 'darksalmon' => '233,150,122',
+ 'darkseagreen' => '143,188,143',
+ 'darkslateblue' => '72,61,139',
+ 'darkslategray' => '47,79,79',
+ 'darkslategrey' => '47,79,79',
+ 'darkturquoise' => '0,206,209',
+ 'darkviolet' => '148,0,211',
+ 'deeppink' => '255,20,147',
+ 'deepskyblue' => '0,191,255',
+ 'dimgray' => '105,105,105',
+ 'dimgrey' => '105,105,105',
+ 'dodgerblue' => '30,144,255',
+ 'firebrick' => '178,34,34',
+ 'floralwhite' => '255,250,240',
+ 'forestgreen' => '34,139,34',
+ 'fuchsia' => '255,0,255',
+ 'gainsboro' => '220,220,220',
+ 'ghostwhite' => '248,248,255',
+ 'gold' => '255,215,0',
+ 'goldenrod' => '218,165,32',
+ 'gray' => '128,128,128',
+ 'green' => '0,128,0',
+ 'greenyellow' => '173,255,47',
+ 'grey' => '128,128,128',
+ 'honeydew' => '240,255,240',
+ 'hotpink' => '255,105,180',
+ 'indianred' => '205,92,92',
+ 'indigo' => '75,0,130',
+ 'ivory' => '255,255,240',
+ 'khaki' => '240,230,140',
+ 'lavender' => '230,230,250',
+ 'lavenderblush' => '255,240,245',
+ 'lawngreen' => '124,252,0',
+ 'lemonchiffon' => '255,250,205',
+ 'lightblue' => '173,216,230',
+ 'lightcoral' => '240,128,128',
+ 'lightcyan' => '224,255,255',
+ 'lightgoldenrodyellow' => '250,250,210',
+ 'lightgray' => '211,211,211',
+ 'lightgreen' => '144,238,144',
+ 'lightgrey' => '211,211,211',
+ 'lightpink' => '255,182,193',
+ 'lightsalmon' => '255,160,122',
+ 'lightseagreen' => '32,178,170',
+ 'lightskyblue' => '135,206,250',
+ 'lightslategray' => '119,136,153',
+ 'lightslategrey' => '119,136,153',
+ 'lightsteelblue' => '176,196,222',
+ 'lightyellow' => '255,255,224',
+ 'lime' => '0,255,0',
+ 'limegreen' => '50,205,50',
+ 'linen' => '250,240,230',
+ 'magenta' => '255,0,255',
+ 'maroon' => '128,0,0',
+ 'mediumaquamarine' => '102,205,170',
+ 'mediumblue' => '0,0,205',
+ 'mediumorchid' => '186,85,211',
+ 'mediumpurple' => '147,112,219',
+ 'mediumseagreen' => '60,179,113',
+ 'mediumslateblue' => '123,104,238',
+ 'mediumspringgreen' => '0,250,154',
+ 'mediumturquoise' => '72,209,204',
+ 'mediumvioletred' => '199,21,133',
+ 'midnightblue' => '25,25,112',
+ 'mintcream' => '245,255,250',
+ 'mistyrose' => '255,228,225',
+ 'moccasin' => '255,228,181',
+ 'navajowhite' => '255,222,173',
+ 'navy' => '0,0,128',
+ 'oldlace' => '253,245,230',
+ 'olive' => '128,128,0',
+ 'olivedrab' => '107,142,35',
+ 'orange' => '255,165,0',
+ 'orangered' => '255,69,0',
+ 'orchid' => '218,112,214',
+ 'palegoldenrod' => '238,232,170',
+ 'palegreen' => '152,251,152',
+ 'paleturquoise' => '175,238,238',
+ 'palevioletred' => '219,112,147',
+ 'papayawhip' => '255,239,213',
+ 'peachpuff' => '255,218,185',
+ 'peru' => '205,133,63',
+ 'pink' => '255,192,203',
+ 'plum' => '221,160,221',
+ 'powderblue' => '176,224,230',
+ 'purple' => '128,0,128',
+ 'red' => '255,0,0',
+ 'rosybrown' => '188,143,143',
+ 'royalblue' => '65,105,225',
+ 'saddlebrown' => '139,69,19',
+ 'salmon' => '250,128,114',
+ 'sandybrown' => '244,164,96',
+ 'seagreen' => '46,139,87',
+ 'seashell' => '255,245,238',
+ 'sienna' => '160,82,45',
+ 'silver' => '192,192,192',
+ 'skyblue' => '135,206,235',
+ 'slateblue' => '106,90,205',
+ 'slategray' => '112,128,144',
+ 'slategrey' => '112,128,144',
+ 'snow' => '255,250,250',
+ 'springgreen' => '0,255,127',
+ 'steelblue' => '70,130,180',
+ 'tan' => '210,180,140',
+ 'teal' => '0,128,128',
+ 'thistle' => '216,191,216',
+ 'tomato' => '255,99,71',
+ 'transparent' => '0,0,0,0',
+ 'turquoise' => '64,224,208',
+ 'violet' => '238,130,238',
+ 'wheat' => '245,222,179',
+ 'white' => '255,255,255',
+ 'whitesmoke' => '245,245,245',
+ 'yellow' => '255,255,0',
+ 'yellowgreen' => '154,205,50'
+ );
+}
--- /dev/null
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp;
+
+use Leafo\ScssPhp\Base\Range;
+use Leafo\ScssPhp\Colors;
+use Leafo\ScssPhp\Parser;
+use Leafo\ScssPhp\Util;
+
+/**
+ * The scss compiler and parser.
+ *
+ * Converting SCSS to CSS is a three stage process. The incoming file is parsed
+ * by `Parser` into a syntax tree, then it is compiled into another tree
+ * representing the CSS structure by `Compiler`. The CSS tree is fed into a
+ * formatter, like `Formatter` which then outputs CSS as a string.
+ *
+ * During the first compile, all values are *reduced*, which means that their
+ * types are brought to the lowest form before being dump as strings. This
+ * handles math equations, variable dereferences, and the like.
+ *
+ * The `compile` function of `Compiler` is the entry point.
+ *
+ * In summary:
+ *
+ * The `Compiler` class creates an instance of the parser, feeds it SCSS code,
+ * then transforms the resulting tree to a CSS tree. This class also holds the
+ * evaluation context, such as all available mixins and variables at any given
+ * time.
+ *
+ * The `Parser` class is only concerned with parsing its input.
+ *
+ * The `Formatter` takes a CSS tree, and dumps it to a formatted string,
+ * handling things like indentation.
+ */
+
+/**
+ * SCSS compiler
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class Compiler
+{
+ const LINE_COMMENTS = 1;
+ const DEBUG_INFO = 2;
+
+ /**
+ * @var array
+ */
+ static protected $operatorNames = array(
+ '+' => 'add',
+ '-' => 'sub',
+ '*' => 'mul',
+ '/' => 'div',
+ '%' => 'mod',
+
+ '==' => 'eq',
+ '!=' => 'neq',
+ '<' => 'lt',
+ '>' => 'gt',
+
+ '<=' => 'lte',
+ '>=' => 'gte',
+ );
+
+ /**
+ * @var array
+ */
+ static protected $namespaces = array(
+ 'special' => '%',
+ 'mixin' => '@',
+ 'function' => '^',
+ );
+
+ /**
+ * @var array
+ */
+ static protected $unitTable = array(
+ 'in' => array(
+ 'in' => 1,
+ 'pt' => 72,
+ 'pc' => 6,
+ 'cm' => 2.54,
+ 'mm' => 25.4,
+ 'px' => 96,
+ 'q' => 101.6,
+ )
+ );
+
+ static public $true = array('keyword', 'true');
+ static public $false = array('keyword', 'false');
+ static public $null = array('null');
+ static public $defaultValue = array('keyword', '');
+ static public $selfSelector = array('self');
+ static public $emptyList = array('list', '', array());
+ static public $emptyMap = array('map', array(), array());
+ static public $emptyString = array('string', '"', array());
+
+ protected $importPaths = array('');
+ protected $importCache = array();
+ protected $userFunctions = array();
+ protected $registeredVars = array();
+
+ protected $numberPrecision = 5;
+ protected $lineNumberStyle = null;
+
+ protected $formatter = 'Leafo\ScssPhp\Formatter\Nested';
+
+ private $indentLevel;
+ private $commentsSeen;
+ private $extends;
+ private $extendsMap;
+ private $parsedFiles;
+ private $env;
+ private $scope;
+ private $parser;
+ private $sourcePos;
+ private $sourceParser;
+ private $storeEnv;
+ private $charsetSeen;
+ private $stderr;
+ private $shouldEvaluate;
+
+ /**
+ * Compile scss
+ *
+ * @api
+ *
+ * @param string $code
+ * @param string $name
+ *
+ * @return string
+ */
+ public function compile($code, $name = null)
+ {
+ $this->indentLevel = -1;
+ $this->commentsSeen = array();
+ $this->extends = array();
+ $this->extendsMap = array();
+ $this->parsedFiles = array();
+ $this->env = null;
+ $this->scope = null;
+
+ $this->stderr = fopen('php://stderr', 'w');
+
+ $locale = setlocale(LC_NUMERIC, 0);
+ setlocale(LC_NUMERIC, 'C');
+
+ $this->parser = new Parser($name);
+
+ $tree = $this->parser->parse($code);
+
+ $this->formatter = new $this->formatter();
+
+ $this->rootEnv = $this->pushEnv($tree);
+ $this->injectVariables($this->registeredVars);
+ $this->compileRoot($tree);
+ $this->popEnv();
+
+ $out = $this->formatter->format($this->scope);
+
+ setlocale(LC_NUMERIC, $locale);
+
+ return $out;
+ }
+
+ /**
+ * Is self extend?
+ *
+ * @param array $target
+ * @param array $origin
+ *
+ * @return boolean
+ */
+ protected function isSelfExtend($target, $origin)
+ {
+ foreach ($origin as $sel) {
+ if (in_array($target, $sel)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Push extends
+ *
+ * @param array $target
+ * @param array $origin
+ */
+ protected function pushExtends($target, $origin)
+ {
+ if ($this->isSelfExtend($target, $origin)) {
+ return;
+ }
+
+ $i = count($this->extends);
+ $this->extends[] = array($target, $origin);
+
+ foreach ($target as $part) {
+ if (isset($this->extendsMap[$part])) {
+ $this->extendsMap[$part][] = $i;
+ } else {
+ $this->extendsMap[$part] = array($i);
+ }
+ }
+ }
+
+ /**
+ * Make output block
+ *
+ * @param string $type
+ * @param array $selectors
+ *
+ * @return \stdClass
+ */
+ protected function makeOutputBlock($type, $selectors = null)
+ {
+ $out = new \stdClass;
+ $out->type = $type;
+ $out->lines = array();
+ $out->children = array();
+ $out->parent = $this->scope;
+ $out->selectors = $selectors;
+ $out->depth = $this->env->depth;
+
+ return $out;
+ }
+
+ /**
+ * Compile root
+ *
+ * @param \stdClass $rootBlock
+ */
+ protected function compileRoot($rootBlock)
+ {
+ $this->scope = $this->makeOutputBlock('root');
+
+ $this->compileChildren($rootBlock->children, $this->scope);
+ $this->flattenSelectors($this->scope);
+ }
+
+ /**
+ * Flatten selectors
+ *
+ * @param \stdClass $block
+ * @parent string $parentKey
+ */
+ protected function flattenSelectors($block, $parentKey = null)
+ {
+ if ($block->selectors) {
+ $selectors = array();
+
+ foreach ($block->selectors as $s) {
+ $selectors[] = $s;
+
+ if (! is_array($s)) {
+ continue;
+ }
+
+ // check extends
+ if (! empty($this->extendsMap)) {
+ $this->matchExtends($s, $selectors);
+
+ // remove duplicates
+ array_walk($selectors, function (&$value) {
+ $value = json_encode($value);
+ });
+ $selectors = array_unique($selectors);
+ array_walk($selectors, function (&$value) {
+ $value = json_decode($value);
+ });
+ }
+ }
+
+ $block->selectors = array();
+ $placeholderSelector = false;
+
+ foreach ($selectors as $selector) {
+ if ($this->hasSelectorPlaceholder($selector)) {
+ $placeholderSelector = true;
+ continue;
+ }
+
+ $block->selectors[] = $this->compileSelector($selector);
+ }
+
+ if ($placeholderSelector && 0 === count($block->selectors) && null !== $parentKey) {
+ unset($block->parent->children[$parentKey]);
+
+ return;
+ }
+ }
+
+ foreach ($block->children as $key => $child) {
+ $this->flattenSelectors($child, $key);
+ }
+ }
+
+ /**
+ * Match extends
+ *
+ * @param array $selector
+ * @param array $out
+ * @param integer $from
+ * @param boolean $initial
+ */
+ protected function matchExtends($selector, &$out, $from = 0, $initial = true)
+ {
+ foreach ($selector as $i => $part) {
+ if ($i < $from) {
+ continue;
+ }
+
+ if ($this->matchExtendsSingle($part, $origin)) {
+ $before = array_slice($selector, 0, $i);
+ $after = array_slice($selector, $i + 1);
+ $s = count($before);
+
+ foreach ($origin as $new) {
+ $k = 0;
+
+ // remove shared parts
+ if ($initial) {
+ while ($k < $s && isset($new[$k]) && $before[$k] === $new[$k]) {
+ $k++;
+ }
+ }
+
+ $result = array_merge(
+ $before,
+ $k > 0 ? array_slice($new, $k) : $new,
+ $after
+ );
+
+ if ($result === $selector) {
+ continue;
+ }
+
+ $out[] = $result;
+
+ // recursively check for more matches
+ $this->matchExtends($result, $out, $i, false);
+
+ // selector sequence merging
+ if (! empty($before) && count($new) > 1) {
+ $result2 = array_merge(
+ array_slice($new, 0, -1),
+ $k > 0 ? array_slice($before, $k) : $before,
+ array_slice($new, -1),
+ $after
+ );
+
+ $out[] = $result2;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Match extends single
+ *
+ * @param array $rawSingle
+ * @param array $outOrigin
+ *
+ * @return boolean
+ */
+ protected function matchExtendsSingle($rawSingle, &$outOrigin)
+ {
+ $counts = array();
+ $single = array();
+
+ foreach ($rawSingle as $part) {
+ // matches Number
+ if (! is_string($part)) {
+ return false;
+ }
+
+ if (! preg_match('/^[\[.:#%]/', $part) && count($single)) {
+ $single[count($single) - 1] .= $part;
+ } else {
+ $single[] = $part;
+ }
+ }
+
+ foreach ($single as $part) {
+ if (isset($this->extendsMap[$part])) {
+ foreach ($this->extendsMap[$part] as $idx) {
+ $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
+ }
+ }
+ }
+
+ $outOrigin = array();
+ $found = false;
+
+ foreach ($counts as $idx => $count) {
+ list($target, $origin) = $this->extends[$idx];
+
+ // check count
+ if ($count !== count($target)) {
+ continue;
+ }
+
+ $rem = array_diff($single, $target);
+
+ foreach ($origin as $j => $new) {
+ // prevent infinite loop when target extends itself
+ if ($this->isSelfExtend($single, $origin)) {
+ return false;
+ }
+
+ $origin[$j][count($origin[$j]) - 1] = $this->combineSelectorSingle(end($new), $rem);
+ }
+
+ $outOrigin = array_merge($outOrigin, $origin);
+
+ $found = true;
+ }
+
+ return $found;
+ }
+
+ /**
+ * Combine selector single
+ *
+ * @param array $base
+ * @param array $other
+ *
+ * @return array
+ */
+ protected function combineSelectorSingle($base, $other)
+ {
+ $tag = null;
+ $out = array();
+
+ foreach (array($base, $other) as $single) {
+ foreach ($single as $part) {
+ if (preg_match('/^[^\[.#:]/', $part)) {
+ $tag = $part;
+ } else {
+ $out[] = $part;
+ }
+ }
+ }
+
+ if ($tag) {
+ array_unshift($out, $tag);
+ }
+
+ return $out;
+ }
+
+ /**
+ * Compile media
+ *
+ * @param \stdClass $media
+ */
+ protected function compileMedia($media)
+ {
+ $this->pushEnv($media);
+
+ $mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env));
+
+ if (! empty($mediaQuery)) {
+ $this->scope = $this->makeOutputBlock('media', array($mediaQuery));
+
+ $parentScope = $this->mediaParent($this->scope);
+ $parentScope->children[] = $this->scope;
+
+ // top level properties in a media cause it to be wrapped
+ $needsWrap = false;
+
+ foreach ($media->children as $child) {
+ $type = $child[0];
+
+ if ($type !== 'block' && $type !== 'media' && $type !== 'directive') {
+ $needsWrap = true;
+ break;
+ }
+ }
+
+ if ($needsWrap) {
+ $wrapped = (object)array(
+ 'selectors' => array(),
+ 'children' => $media->children
+ );
+ $media->children = array(array('block', $wrapped));
+ }
+
+ $this->compileChildren($media->children, $this->scope);
+
+ $this->scope = $this->scope->parent;
+ }
+
+ $this->popEnv();
+ }
+
+ /**
+ * Media parent
+ *
+ * @param \stdClass $scope
+ *
+ * @return \stdClass
+ */
+ protected function mediaParent($scope)
+ {
+ while (! empty($scope->parent)) {
+ if (! empty($scope->type) && $scope->type !== 'media') {
+ break;
+ }
+
+ $scope = $scope->parent;
+ }
+
+ return $scope;
+ }
+
+ /**
+ * Compile nested block
+ *
+ * @todo refactor compileNestedBlock and compileMedia into same thing?
+ *
+ * @param \stdClass $block
+ * @param array $selectors
+ */
+ protected function compileNestedBlock($block, $selectors)
+ {
+ $this->pushEnv($block);
+
+ $this->scope = $this->makeOutputBlock($block->type, $selectors);
+ $this->scope->parent->children[] = $this->scope;
+
+ $this->compileChildren($block->children, $this->scope);
+
+ $this->scope = $this->scope->parent;
+
+ $this->popEnv();
+ }
+
+ /**
+ * Recursively compiles a block.
+ *
+ * A block is analogous to a CSS block in most cases. A single SCSS document
+ * is encapsulated in a block when parsed, but it does not have parent tags
+ * so all of its children appear on the root level when compiled.
+ *
+ * Blocks are made up of selectors and children.
+ *
+ * The children of a block are just all the blocks that are defined within.
+ *
+ * Compiling the block involves pushing a fresh environment on the stack,
+ * and iterating through the props, compiling each one.
+ *
+ * @see Compiler::compileChild()
+ *
+ * @param \stdClass $block
+ */
+ protected function compileBlock($block)
+ {
+ $env = $this->pushEnv($block);
+
+ $env->selectors = $this->evalSelectors($block->selectors);
+
+ $out = $this->makeOutputBlock(null, $this->multiplySelectors($env));
+
+ if (isset($this->lineNumberStyle) && count($env->selectors) && count($block->children)) {
+ $annotation = $this->makeOutputBlock('comment');
+ $annotation->depth = 0;
+
+ $file = $block->sourceParser->getSourceName();
+ $line = $block->sourceParser->getLineNo($block->sourcePosition);
+
+ switch ($this->lineNumberStyle) {
+ case self::LINE_COMMENTS:
+ $annotation->lines[] = '/* line ' . $line . ', ' . $file . ' */';
+ break;
+
+ case self::DEBUG_INFO:
+ $annotation->lines[] = '@media -sass-debug-info{filename{font-family:"' . $file
+ . '"}line{font-family:' . $line . '}}';
+ break;
+ }
+
+ $this->scope->children[] = $annotation;
+ }
+
+ $this->scope->children[] = $out;
+
+ $this->compileChildren($block->children, $out);
+
+ $this->formatter->stripSemicolon($out->lines);
+
+ $this->popEnv();
+ }
+
+ /**
+ * Compile root level comment
+ *
+ * @param array $block
+ */
+ protected function compileComment($block)
+ {
+ $out = $this->makeOutputBlock('comment');
+ $out->lines[] = $block[1];
+ $this->scope->children[] = $out;
+ }
+
+ /**
+ * Evaluate selectors
+ *
+ * @param array $selectors
+ *
+ * @return array
+ */
+ protected function evalSelectors($selectors)
+ {
+ $this->shouldEvaluate = false;
+
+ $selectors = array_map(array($this, 'evalSelector'), $selectors);
+
+ // after evaluating interpolates, we might need a second pass
+ if ($this->shouldEvaluate) {
+ $buffer = $this->collapseSelectors($selectors);
+ $parser = new Parser(__METHOD__, false);
+
+ if ($parser->parseSelector($buffer, $newSelectors)) {
+ $selectors = array_map(array($this, 'evalSelector'), $newSelectors);
+ }
+ }
+
+ return $selectors;
+ }
+
+ /**
+ * Evaluate selector
+ *
+ * @param array $selector
+ *
+ * @return array
+ */
+ protected function evalSelector($selector)
+ {
+ return array_map(array($this, 'evalSelectorPart'), $selector);
+ }
+
+ /**
+ * Evaluate selector part; replaces all the interpolates, stripping quotes
+ *
+ * @param array $part
+ *
+ * @return array
+ */
+ protected function evalSelectorPart($part)
+ {
+ foreach ($part as &$p) {
+ if (is_array($p) && ($p[0] === 'interpolate' || $p[0] === 'string')) {
+ $p = $this->compileValue($p);
+
+ // force re-evaluation
+ if (strpos($p, '&') !== false || strpos($p, ',') !== false) {
+ $this->shouldEvaluate = true;
+ }
+ } elseif (is_string($p) && strlen($p) >= 2 &&
+ ($first = $p[0]) && ($first === '"' || $first === "'") &&
+ substr($p, -1) === $first
+ ) {
+ $p = substr($p, 1, -1);
+ }
+ }
+
+ return $this->flattenSelectorSingle($part);
+ }
+
+ /**
+ * Collapse selectors
+ *
+ * @param array $selectors
+ *
+ * @return string
+ */
+ protected function collapseSelectors($selectors)
+ {
+ $parts = array();
+
+ foreach ($selectors as $selector) {
+ $output = '';
+
+ array_walk_recursive(
+ $selector,
+ function ($value, $key) use (&$output) {
+ $output .= $value;
+ }
+ );
+
+ $parts[] = $output;
+ }
+
+ return implode(', ', $parts);
+ }
+
+ /**
+ * Flatten selector single; joins together .classes and #ids
+ *
+ * @param array $single
+ *
+ * @return array
+ */
+ protected function flattenSelectorSingle($single)
+ {
+ $joined = array();
+
+ foreach ($single as $part) {
+ if (empty($joined) ||
+ ! is_string($part) ||
+ preg_match('/[\[.:#%]/', $part)
+ ) {
+ $joined[] = $part;
+ continue;
+ }
+
+ if (is_array(end($joined))) {
+ $joined[] = $part;
+ } else {
+ $joined[count($joined) - 1] .= $part;
+ }
+ }
+
+ return $joined;
+ }
+
+ /**
+ * Compile selector to string; self(&) should have been replaced by now
+ *
+ * @param array $selector
+ *
+ * @return string
+ */
+ protected function compileSelector($selector)
+ {
+ if (! is_array($selector)) {
+ return $selector; // media and the like
+ }
+
+ return implode(
+ ' ',
+ array_map(
+ array($this, 'compileSelectorPart'),
+ $selector
+ )
+ );
+ }
+
+ /**
+ * Compile selector part
+ *
+ * @param arary $piece
+ *
+ * @return string
+ */
+ protected function compileSelectorPart($piece)
+ {
+ foreach ($piece as &$p) {
+ if (! is_array($p)) {
+ continue;
+ }
+
+ switch ($p[0]) {
+ case 'self':
+ $p = '&';
+ break;
+
+ default:
+ $p = $this->compileValue($p);
+ break;
+ }
+ }
+
+ return implode($piece);
+ }
+
+ /**
+ * Has selector placeholder?
+ *
+ * @param array $selector
+ *
+ * @return boolean
+ */
+ protected function hasSelectorPlaceholder($selector)
+ {
+ if (! is_array($selector)) {
+ return false;
+ }
+
+ foreach ($selector as $parts) {
+ foreach ($parts as $part) {
+ if ('%' === $part[0]) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Compile children
+ *
+ * @param array $stms
+ * @param array $out
+ *
+ * @return array
+ */
+ protected function compileChildren($stms, $out)
+ {
+ foreach ($stms as $stm) {
+ $ret = $this->compileChild($stm, $out);
+
+ if (isset($ret)) {
+ return $ret;
+ }
+ }
+ }
+
+ /**
+ * Compile media query
+ *
+ * @param array $queryList
+ *
+ * @return string
+ */
+ protected function compileMediaQuery($queryList)
+ {
+ $out = '@media';
+ $first = true;
+
+ foreach ($queryList as $query) {
+ $type = null;
+ $parts = array();
+
+ foreach ($query as $q) {
+ switch ($q[0]) {
+ case 'mediaType':
+ if ($type) {
+ $type = $this->mergeMediaTypes(
+ $type,
+ array_map(array($this, 'compileValue'), array_slice($q, 1))
+ );
+
+ if (empty($type)) { // merge failed
+ return null;
+ }
+ } else {
+ $type = array_map(array($this, 'compileValue'), array_slice($q, 1));
+ }
+ break;
+
+ case 'mediaExp':
+ if (isset($q[2])) {
+ $parts[] = '('
+ . $this->compileValue($q[1])
+ . $this->formatter->assignSeparator
+ . $this->compileValue($q[2])
+ . ')';
+ } else {
+ $parts[] = '('
+ . $this->compileValue($q[1])
+ . ')';
+ }
+ break;
+ }
+ }
+
+ if ($type) {
+ array_unshift($parts, implode(' ', array_filter($type)));
+ }
+
+ if (! empty($parts)) {
+ if ($first) {
+ $first = false;
+ $out .= ' ';
+ } else {
+ $out .= $this->formatter->tagSeparator;
+ }
+
+ $out .= implode(' and ', $parts);
+ }
+ }
+
+ return $out;
+ }
+
+ /**
+ * Merge media types
+ *
+ * @param array $type1
+ * @param array $type2
+ *
+ * @return array|null
+ */
+ protected function mergeMediaTypes($type1, $type2)
+ {
+ if (empty($type1)) {
+ return $type2;
+ }
+
+ if (empty($type2)) {
+ return $type1;
+ }
+
+ $m1 = '';
+ $t1 = '';
+
+ if (count($type1) > 1) {
+ $m1= strtolower($type1[0]);
+ $t1= strtolower($type1[1]);
+ } else {
+ $t1 = strtolower($type1[0]);
+ }
+
+ $m2 = '';
+ $t2 = '';
+
+ if (count($type2) > 1) {
+ $m2 = strtolower($type2[0]);
+ $t2 = strtolower($type2[1]);
+ } else {
+ $t2 = strtolower($type2[0]);
+ }
+
+ if (($m1 === 'not') ^ ($m2 === 'not')) {
+ if ($t1 === $t2) {
+ return null;
+ }
+
+ return array(
+ $m1 === 'not' ? $m2 : $m1,
+ $m1 === 'not' ? $t2 : $t1
+ );
+ }
+
+ if ($m1 === 'not' && $m2 === 'not') {
+ // CSS has no way of representing "neither screen nor print"
+ if ($t1 !== $t2) {
+ return null;
+ }
+
+ return array('not', $t1);
+ }
+
+ if ($t1 !== $t2) {
+ return null;
+ }
+
+ // t1 == t2, neither m1 nor m2 are "not"
+ return array(empty($m1)? $m2 : $m1, $t1);
+ }
+
+ /**
+ * Compile import; returns true if the value was something that could be imported
+ *
+ * @param array $rawPath
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function compileImport($rawPath, $out)
+ {
+ if ($rawPath[0] === 'string') {
+ $path = $this->compileStringContent($rawPath);
+
+ if ($path = $this->findImport($path)) {
+ $this->importFile($path, $out);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ if ($rawPath[0] === 'list') {
+ // handle a list of strings
+ if (count($rawPath[2]) === 0) {
+ return false;
+ }
+
+ foreach ($rawPath[2] as $path) {
+ if ($path[0] !== 'string') {
+ return false;
+ }
+ }
+
+ foreach ($rawPath[2] as $path) {
+ $this->compileImport($path, $out);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Compile child; returns a value to halt execution
+ *
+ * @param array $child
+ * @param \stdClass $out
+ *
+ * @return array
+ */
+ protected function compileChild($child, $out)
+ {
+ $this->sourcePos = isset($child[Parser::SOURCE_POSITION]) ? $child[Parser::SOURCE_POSITION] : -1;
+ $this->sourceParser = isset($child[Parser::SOURCE_PARSER]) ? $child[Parser::SOURCE_PARSER] : $this->parser;
+
+ switch ($child[0]) {
+ case 'import':
+ list(, $rawPath) = $child;
+
+ $rawPath = $this->reduce($rawPath);
+
+ if (! $this->compileImport($rawPath, $out)) {
+ $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
+ }
+ break;
+
+ case 'directive':
+ list(, $directive) = $child;
+
+ $s = '@' . $directive->name;
+
+ if (! empty($directive->value)) {
+ $s .= ' ' . $this->compileValue($directive->value);
+ }
+
+ $this->compileNestedBlock($directive, array($s));
+ break;
+
+ case 'media':
+ $this->compileMedia($child[1]);
+ break;
+
+ case 'block':
+ $this->compileBlock($child[1]);
+ break;
+
+ case 'charset':
+ if (! $this->charsetSeen) {
+ $this->charsetSeen = true;
+
+ $out->lines[] = '@charset ' . $this->compileValue($child[1]) . ';';
+ }
+ break;
+
+ case 'assign':
+ list(, $name, $value) = $child;
+
+ if ($name[0] === 'var') {
+ $flag = isset($child[3]) ? $child[3] : null;
+ $isDefault = $flag === '!default';
+ $isGlobal = $flag === '!global';
+
+ if ($isGlobal) {
+ $this->set($name[1], $this->reduce($value), false, $this->rootEnv);
+ break;
+ }
+
+ $shouldSet = $isDefault &&
+ (($result = $this->get($name[1], false)) === null
+ || $result === self::$null);
+
+ if (! $isDefault || $shouldSet) {
+ $this->set($name[1], $this->reduce($value));
+ }
+ break;
+ }
+
+ $compiledName = $this->compileValue($name);
+
+ // handle shorthand syntax: size / line-height
+ if ($compiledName === 'font') {
+ if ($value[0] === 'exp' && $value[1] === '/') {
+ $value = $this->expToString($value);
+ } elseif ($value[0] === 'list') {
+ foreach ($value[2] as &$item) {
+ if ($item[0] === 'exp' && $item[1] === '/') {
+ $item = $this->expToString($item);
+ }
+ }
+ }
+ }
+
+ // if the value reduces to null from something else then
+ // the property should be discarded
+ if ($value[0] !== 'null') {
+ $value = $this->reduce($value);
+
+ if ($value[0] === 'null') {
+ break;
+ }
+ }
+
+ $compiledValue = $this->compileValue($value);
+
+ $out->lines[] = $this->formatter->property(
+ $compiledName,
+ $compiledValue
+ );
+ break;
+
+ case 'comment':
+ if ($out->type === 'root') {
+ $this->compileComment($child);
+ break;
+ }
+
+ $out->lines[] = $child[1];
+ break;
+
+ case 'mixin':
+ case 'function':
+ list(, $block) = $child;
+
+ $this->set(self::$namespaces[$block->type] . $block->name, $block);
+ break;
+
+ case 'extend':
+ list(, $selectors) = $child;
+
+ foreach ($selectors as $sel) {
+ // only use the first one
+ $result = $this->evalSelectors(array($sel));
+ $result = current($result[0]);
+
+ $this->pushExtends($result, $out->selectors);
+ }
+ break;
+
+ case 'if':
+ list(, $if) = $child;
+
+ if ($this->isTruthy($this->reduce($if->cond, true))) {
+ return $this->compileChildren($if->children, $out);
+ }
+
+ foreach ($if->cases as $case) {
+ if ($case->type === 'else' ||
+ $case->type === 'elseif' && $this->isTruthy($this->reduce($case->cond))
+ ) {
+ return $this->compileChildren($case->children, $out);
+ }
+ }
+ break;
+
+ case 'return':
+ return $this->reduce($child[1], true);
+
+ case 'each':
+ list(, $each) = $child;
+
+ $list = $this->coerceList($this->reduce($each->list));
+
+ $this->pushEnv();
+
+ foreach ($list[2] as $item) {
+ if (count($each->vars) === 1) {
+ $this->set($each->vars[0], $item, true);
+ } else {
+ list(,, $values) = $this->coerceList($item);
+
+ foreach ($each->vars as $i => $var) {
+ $this->set($var, isset($values[$i]) ? $values[$i] : self::$null, true);
+ }
+ }
+
+ $ret = $this->compileChildren($each->children, $out);
+
+ if ($ret) {
+ $this->popEnv();
+
+ return $ret;
+ }
+ }
+
+ $this->popEnv();
+ break;
+
+ case 'while':
+ list(, $while) = $child;
+
+ while ($this->isTruthy($this->reduce($while->cond, true))) {
+ $ret = $this->compileChildren($while->children, $out);
+
+ if ($ret) {
+ return $ret;
+ }
+ }
+ break;
+
+ case 'for':
+ list(, $for) = $child;
+
+ $start = $this->reduce($for->start, true);
+ $start = $start[1];
+ $end = $this->reduce($for->end, true);
+ $end = $end[1];
+ $d = $start < $end ? 1 : -1;
+
+ while (true) {
+ if ((! $for->until && $start - $d == $end) ||
+ ($for->until && $start == $end)
+ ) {
+ break;
+ }
+
+ $this->set($for->var, array('number', $start, ''));
+ $start += $d;
+
+ $ret = $this->compileChildren($for->children, $out);
+
+ if ($ret) {
+ return $ret;
+ }
+ }
+ break;
+
+ case 'nestedprop':
+ list(, $prop) = $child;
+
+ $prefixed = array();
+ $prefix = $this->compileValue($prop->prefix) . '-';
+
+ foreach ($prop->children as $child) {
+ if ($child[0] === 'assign') {
+ array_unshift($child[1][2], $prefix);
+ }
+
+ if ($child[0] === 'nestedprop') {
+ array_unshift($child[1]->prefix[2], $prefix);
+ }
+
+ $prefixed[] = $child;
+ }
+
+ $this->compileChildren($prefixed, $out);
+ break;
+
+ case 'include':
+ // including a mixin
+ list(, $name, $argValues, $content) = $child;
+
+ $mixin = $this->get(self::$namespaces['mixin'] . $name, false);
+
+ if (! $mixin) {
+ $this->throwError("Undefined mixin $name");
+ }
+
+ $callingScope = $this->env;
+
+ // push scope, apply args
+ $this->pushEnv();
+ $this->env->depth--;
+
+ if (isset($content)) {
+ $content->scope = $callingScope;
+
+ $this->setRaw(self::$namespaces['special'] . 'content', $content);
+ }
+
+ if (isset($mixin->args)) {
+ $this->applyArguments($mixin->args, $argValues);
+ }
+
+ $this->env->marker = 'mixin';
+
+ foreach ($mixin->children as $child) {
+ $this->compileChild($child, $out);
+ }
+
+ $this->popEnv();
+ break;
+
+ case 'mixin_content':
+ $content = $this->get(self::$namespaces['special'] . 'content', false);
+
+ if (! $content) {
+ $this->throwError('Expected @content inside of mixin');
+ }
+
+ if (! isset($content->children)) {
+ break;
+ }
+
+ $this->storeEnv = $content->scope;
+
+ foreach ($content->children as $child) {
+ $this->compileChild($child, $out);
+ }
+
+ $this->storeEnv = null;
+
+ break;
+
+ case 'debug':
+ list(, $value) = $child;
+
+ $line = $this->parser->getLineNo($this->sourcePos);
+ $value = $this->compileValue($this->reduce($value, true));
+ fwrite($this->stderr, "Line $line DEBUG: $value\n");
+ break;
+
+ case 'warn':
+ list(, $value) = $child;
+
+ $line = $this->parser->getLineNo($this->sourcePos);
+ $value = $this->compileValue($this->reduce($value, true));
+ echo "Line $line WARN: $value\n";
+ break;
+
+ case 'error':
+ list(, $value) = $child;
+
+ $line = $this->parser->getLineNo($this->sourcePos);
+ $value = $this->compileValue($this->reduce($value, true));
+ $this->throwError("Line $line ERROR: $value\n");
+ break;
+
+ default:
+ $this->throwError("unknown child type: $child[0]");
+ }
+ }
+
+ /**
+ * Reduce expression to string
+ *
+ * @param array $exp
+ *
+ * @return array
+ */
+ protected function expToString($exp)
+ {
+ list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
+
+ $content = array($this->reduce($left));
+
+ if ($whiteLeft) {
+ $content[] = ' ';
+ }
+
+ $content[] = $op;
+
+ if ($whiteRight) {
+ $content[] = ' ';
+ }
+
+ $content[] = $this->reduce($right);
+
+ return array('string', '', $content);
+ }
+
+ /**
+ * Is truthy?
+ *
+ * @param array $value
+ *
+ * @return array
+ */
+ protected function isTruthy($value)
+ {
+ return $value !== self::$false && $value !== self::$null;
+ }
+
+ /**
+ * Should $value cause its operand to eval
+ *
+ * @param array $value
+ *
+ * @return boolean
+ */
+ protected function shouldEval($value)
+ {
+ switch ($value[0]) {
+ case 'exp':
+ if ($value[1] === '/') {
+ return $this->shouldEval($value[2], $value[3]);
+ }
+
+ // fall-thru
+ case 'var':
+ case 'fncall':
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Reduce value
+ *
+ * @param array $value
+ * @param boolean $inExp
+ *
+ * @return array
+ */
+ protected function reduce($value, $inExp = false)
+ {
+ list($type) = $value;
+
+ switch ($type) {
+ case 'exp':
+ list(, $op, $left, $right, $inParens) = $value;
+
+ $opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op;
+ $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
+
+ $left = $this->reduce($left, true);
+ $right = $this->reduce($right, true);
+
+ // special case: looks like css short-hand
+ if ($opName === 'div' && ! $inParens && ! $inExp && isset($right[2]) && $right[2] !== '') {
+ return $this->expToString($value);
+ }
+
+ $left = $this->coerceForExpression($left);
+ $right = $this->coerceForExpression($right);
+
+ $ltype = $left[0];
+ $rtype = $right[0];
+
+ $ucOpName = ucfirst($opName);
+ $ucLType = ucfirst($ltype);
+ $ucRType = ucfirst($rtype);
+
+ // this tries:
+ // 1. op[op name][left type][right type]
+ // 2. op[left type][right type] (passing the op as first arg
+ // 3. op[op name]
+ $fn = "op${ucOpName}${ucLType}${ucRType}";
+
+ if (is_callable(array($this, $fn)) ||
+ (($fn = "op${ucLType}${ucRType}") &&
+ is_callable(array($this, $fn)) &&
+ $passOp = true) ||
+ (($fn = "op${ucOpName}") &&
+ is_callable(array($this, $fn)) &&
+ $genOp = true)
+ ) {
+ $unitChange = false;
+
+ if (! isset($genOp) &&
+ $left[0] === 'number' && $right[0] === 'number'
+ ) {
+ if ($opName === 'mod' && $right[2] !== '') {
+ $this->throwError("Cannot modulo by a number with units: $right[1]$right[2].");
+ }
+
+ $unitChange = true;
+ $emptyUnit = $left[2] === '' || $right[2] === '';
+ $targetUnit = '' !== $left[2] ? $left[2] : $right[2];
+
+ if ($opName !== 'mul') {
+ $left[2] = '' !== $left[2] ? $left[2] : $targetUnit;
+ $right[2] = '' !== $right[2] ? $right[2] : $targetUnit;
+ }
+
+ if ($opName !== 'mod') {
+ $left = $this->normalizeNumber($left);
+ $right = $this->normalizeNumber($right);
+ }
+
+ if ($opName === 'div' && ! $emptyUnit && $left[2] === $right[2]) {
+ $targetUnit = '';
+ }
+
+ if ($opName === 'mul') {
+ $left[2] = '' !== $left[2] ? $left[2] : $right[2];
+ $right[2] = '' !== $right[2] ? $right[2] : $left[2];
+ } elseif ($opName === 'div' && $left[2] === $right[2]) {
+ $left[2] = '';
+ $right[2] = '';
+ }
+ }
+
+ $shouldEval = $inParens || $inExp;
+
+ if (isset($passOp)) {
+ $out = $this->$fn($op, $left, $right, $shouldEval);
+ } else {
+ $out = $this->$fn($left, $right, $shouldEval);
+ }
+
+ if (isset($out)) {
+ if ($unitChange && $out[0] === 'number') {
+ $out = $this->coerceUnit($out, $targetUnit);
+ }
+
+ return $out;
+ }
+ }
+
+ return $this->expToString($value);
+
+ case 'unary':
+ list(, $op, $exp, $inParens) = $value;
+
+ $inExp = $inExp || $this->shouldEval($exp);
+ $exp = $this->reduce($exp);
+
+ if ($exp[0] === 'number') {
+ switch ($op) {
+ case '+':
+ return $exp;
+
+ case '-':
+ $exp[1] *= -1;
+
+ return $exp;
+ }
+ }
+
+ if ($op === 'not') {
+ if ($inExp || $inParens) {
+ if ($exp === self::$false) {
+ return self::$true;
+ }
+
+ return self::$false;
+ }
+
+ $op = $op . ' ';
+ }
+
+ return array('string', '', array($op, $exp));
+
+ case 'var':
+ list(, $name) = $value;
+
+ return $this->reduce($this->get($name));
+
+ case 'list':
+ foreach ($value[2] as &$item) {
+ $item = $this->reduce($item);
+ }
+
+ return $value;
+
+ case 'map':
+ foreach ($value[1] as &$item) {
+ $item = $this->reduce($item);
+ }
+
+ foreach ($value[2] as &$item) {
+ $item = $this->reduce($item);
+ }
+
+ return $value;
+
+ case 'string':
+ foreach ($value[2] as &$item) {
+ if (is_array($item)) {
+ $item = $this->reduce($item);
+ }
+ }
+
+ return $value;
+
+ case 'interpolate':
+ $value[1] = $this->reduce($value[1]);
+
+ return $value;
+
+ case 'fncall':
+ list(, $name, $argValues) = $value;
+
+ // user defined function?
+ $func = $this->get(self::$namespaces['function'] . $name, false);
+
+ if ($func) {
+ $this->pushEnv();
+
+ // set the args
+ if (isset($func->args)) {
+ $this->applyArguments($func->args, $argValues);
+ }
+
+ // throw away lines and children
+ $tmp = (object)array(
+ 'lines' => array(),
+ 'children' => array()
+ );
+
+ $ret = $this->compileChildren($func->children, $tmp);
+
+ $this->popEnv();
+
+ return ! isset($ret) ? self::$defaultValue : $ret;
+ }
+
+ // built in function
+ if ($this->callBuiltin($name, $argValues, $returnValue)) {
+ return $returnValue;
+ }
+
+ // need to flatten the arguments into a list
+ $listArgs = array();
+
+ foreach ((array)$argValues as $arg) {
+ if (empty($arg[0])) {
+ $listArgs[] = $this->reduce($arg[1]);
+ }
+ }
+
+ return array('function', $name, array('list', ',', $listArgs));
+
+ default:
+ return $value;
+ }
+ }
+
+ /**
+ * Normalize name
+ *
+ * @param string $name
+ *
+ * @return string
+ */
+ protected function normalizeName($name)
+ {
+ return str_replace('-', '_', $name);
+ }
+
+ /**
+ * Normalize value
+ *
+ * @param array $value
+ *
+ * @return array
+ */
+ public function normalizeValue($value)
+ {
+ $value = $this->coerceForExpression($this->reduce($value));
+ list($type) = $value;
+
+ switch ($type) {
+ case 'list':
+ $value = $this->extractInterpolation($value);
+
+ if ($value[0] !== 'list') {
+ return array('keyword', $this->compileValue($value));
+ }
+
+ foreach ($value[2] as $key => $item) {
+ $value[2][$key] = $this->normalizeValue($item);
+ }
+
+ return $value;
+
+ case 'string':
+ return array($type, '"', $this->compileStringContent($value));
+
+ case 'number':
+ return $this->normalizeNumber($value);
+
+ default:
+ return $value;
+ }
+ }
+
+ /**
+ * Normalize number; just does physical lengths for now
+ *
+ * @param array $number
+ *
+ * @return array
+ */
+ protected function normalizeNumber($number)
+ {
+ list(, $value, $unit) = $number;
+
+ if (isset(self::$unitTable['in'][$unit])) {
+ $conv = self::$unitTable['in'][$unit];
+
+ return array('number', $value / $conv, 'in');
+ }
+
+ return $number;
+ }
+
+ /**
+ * Add numbers
+ *
+ * @param array $left
+ * @param array $right
+ *
+ * @return array
+ */
+ protected function opAddNumberNumber($left, $right)
+ {
+ return array('number', $left[1] + $right[1], $left[2]);
+ }
+
+ /**
+ * Multiply numbers
+ *
+ * @param array $left
+ * @param array $right
+ *
+ * @return array
+ */
+ protected function opMulNumberNumber($left, $right)
+ {
+ return array('number', $left[1] * $right[1], $left[2]);
+ }
+
+ /**
+ * Subtract numbers
+ *
+ * @param array $left
+ * @param array $right
+ *
+ * @return array
+ */
+ protected function opSubNumberNumber($left, $right)
+ {
+ return array('number', $left[1] - $right[1], $left[2]);
+ }
+
+ /**
+ * Divide numbers
+ *
+ * @param array $left
+ * @param array $right
+ *
+ * @return array
+ */
+ protected function opDivNumberNumber($left, $right)
+ {
+ if ($right[1] == 0) {
+ $this->throwError('Division by zero');
+ }
+
+ return array('number', $left[1] / $right[1], $left[2]);
+ }
+
+ /**
+ * Mod numbers
+ *
+ * @param array $left
+ * @param array $right
+ *
+ * @return array
+ */
+ protected function opModNumberNumber($left, $right)
+ {
+ return array('number', $left[1] % $right[1], $left[2]);
+ }
+
+ /**
+ * Add strings
+ *
+ * @param array $left
+ * @param array $right
+ *
+ * @return array
+ */
+ protected function opAdd($left, $right)
+ {
+ if ($strLeft = $this->coerceString($left)) {
+ if ($right[0] === 'string') {
+ $right[1] = '';
+ }
+
+ $strLeft[2][] = $right;
+
+ return $strLeft;
+ }
+
+ if ($strRight = $this->coerceString($right)) {
+ if ($left[0] === 'string') {
+ $left[1] = '';
+ }
+
+ array_unshift($strRight[2], $left);
+
+ return $strRight;
+ }
+ }
+
+ /**
+ * Boolean and
+ *
+ * @param array $left
+ * @param array $right
+ * @param boolean $shouldEval
+ *
+ * @return array
+ */
+ protected function opAnd($left, $right, $shouldEval)
+ {
+ if (! $shouldEval) {
+ return;
+ }
+
+ if ($left !== self::$false) {
+ return $right;
+ }
+
+ return $left;
+ }
+
+ /**
+ * Boolean or
+ *
+ * @param array $left
+ * @param array $right
+ * @param boolean $shouldEval
+ *
+ * @return array
+ */
+ protected function opOr($left, $right, $shouldEval)
+ {
+ if (! $shouldEval) {
+ return;
+ }
+
+ if ($left !== self::$false) {
+ return $left;
+ }
+
+ return $right;
+ }
+
+ /**
+ * Compare colors
+ *
+ * @param string $op
+ * @param array $left
+ * @param array $right
+ *
+ * @return array
+ */
+ protected function opColorColor($op, $left, $right)
+ {
+ $out = array('color');
+
+ foreach (range(1, 3) as $i) {
+ $lval = isset($left[$i]) ? $left[$i] : 0;
+ $rval = isset($right[$i]) ? $right[$i] : 0;
+
+ switch ($op) {
+ case '+':
+ $out[] = $lval + $rval;
+ break;
+
+ case '-':
+ $out[] = $lval - $rval;
+ break;
+
+ case '*':
+ $out[] = $lval * $rval;
+ break;
+
+ case '%':
+ $out[] = $lval % $rval;
+ break;
+
+ case '/':
+ if ($rval == 0) {
+ $this->throwError("color: Can't divide by zero");
+ }
+
+ $out[] = (int) ($lval / $rval);
+ break;
+
+ case '==':
+ return $this->opEq($left, $right);
+
+ case '!=':
+ return $this->opNeq($left, $right);
+
+ default:
+ $this->throwError("color: unknown op $op");
+ }
+ }
+
+ if (isset($left[4])) {
+ $out[4] = $left[4];
+ } elseif (isset($right[4])) {
+ $out[4] = $right[4];
+ }
+
+ return $this->fixColor($out);
+ }
+
+ /**
+ * Compare color and number
+ *
+ * @param string $op
+ * @param array $left
+ * @param array $right
+ *
+ * @return array
+ */
+ protected function opColorNumber($op, $left, $right)
+ {
+ $value = $right[1];
+
+ return $this->opColorColor(
+ $op,
+ $left,
+ array('color', $value, $value, $value)
+ );
+ }
+
+ /**
+ * Compare number and color
+ *
+ * @param string $op
+ * @param array $left
+ * @param array $right
+ *
+ * @return array
+ */
+ protected function opNumberColor($op, $left, $right)
+ {
+ $value = $left[1];
+
+ return $this->opColorColor(
+ $op,
+ array('color', $value, $value, $value),
+ $right
+ );
+ }
+
+ /**
+ * Compare number1 == number2
+ *
+ * @param array $left
+ * @param array $right
+ *
+ * @return array
+ */
+ protected function opEq($left, $right)
+ {
+ if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
+ $lStr[1] = '';
+ $rStr[1] = '';
+
+ $left = $this->compileValue($lStr);
+ $right = $this->compileValue($rStr);
+ }
+
+ return $this->toBool($left === $right);
+ }
+
+ /**
+ * Compare number1 != number2
+ *
+ * @param array $left
+ * @param array $right
+ *
+ * @return array
+ */
+ protected function opNeq($left, $right)
+ {
+ if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
+ $lStr[1] = '';
+ $rStr[1] = '';
+
+ $left = $this->compileValue($lStr);
+ $right = $this->compileValue($rStr);
+ }
+
+ return $this->toBool($left !== $right);
+ }
+
+ /**
+ * Compare number1 >= number2
+ *
+ * @param array $left
+ * @param array $right
+ *
+ * @return array
+ */
+ protected function opGteNumberNumber($left, $right)
+ {
+ return $this->toBool($left[1] >= $right[1]);
+ }
+
+ /**
+ * Compare number1 > number2
+ *
+ * @param array $left
+ * @param array $right
+ *
+ * @return array
+ */
+ protected function opGtNumberNumber($left, $right)
+ {
+ return $this->toBool($left[1] > $right[1]);
+ }
+
+ /**
+ * Compare number1 <= number2
+ *
+ * @param array $left
+ * @param array $right
+ *
+ * @return array
+ */
+ protected function opLteNumberNumber($left, $right)
+ {
+ return $this->toBool($left[1] <= $right[1]);
+ }
+
+ /**
+ * Compare number1 < number2
+ *
+ * @param array $left
+ * @param array $right
+ *
+ * @return array
+ */
+ protected function opLtNumberNumber($left, $right)
+ {
+ return $this->toBool($left[1] < $right[1]);
+ }
+
+ /**
+ * Cast to boolean
+ *
+ * @api
+ *
+ * @param mixed $thing
+ *
+ * @return array
+ */
+ public function toBool($thing)
+ {
+ return $thing ? self::$true : self::$false;
+ }
+
+ /**
+ * Compiles a primitive value into a CSS property value.
+ *
+ * Values in scssphp are typed by being wrapped in arrays, their format is
+ * typically:
+ *
+ * array(type, contents [, additional_contents]*)
+ *
+ * The input is expected to be reduced. This function will not work on
+ * things like expressions and variables.
+ *
+ * @api
+ *
+ * @param array $value
+ *
+ * @return string
+ */
+ public function compileValue($value)
+ {
+ $value = $this->reduce($value);
+
+ list($type) = $value;
+
+ switch ($type) {
+ case 'keyword':
+ return $value[1];
+
+ case 'color':
+ // [1] - red component (either number for a %)
+ // [2] - green component
+ // [3] - blue component
+ // [4] - optional alpha component
+ list(, $r, $g, $b) = $value;
+
+ $r = round($r);
+ $g = round($g);
+ $b = round($b);
+
+ if (count($value) === 5 && $value[4] !== 1) { // rgba
+ return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $value[4] . ')';
+ }
+
+ $h = sprintf('#%02x%02x%02x', $r, $g, $b);
+
+ // Converting hex color to short notation (e.g. #003399 to #039)
+ if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
+ $h = '#' . $h[1] . $h[3] . $h[5];
+ }
+
+ return $h;
+
+ case 'number':
+ return round($value[1], $this->numberPrecision) . $value[2];
+
+ case 'string':
+ return $value[1] . $this->compileStringContent($value) . $value[1];
+
+ case 'function':
+ $args = ! empty($value[2]) ? $this->compileValue($value[2]) : '';
+
+ return "$value[1]($args)";
+
+ case 'list':
+ $value = $this->extractInterpolation($value);
+
+ if ($value[0] !== 'list') {
+ return $this->compileValue($value);
+ }
+
+ list(, $delim, $items) = $value;
+
+ $filtered = array();
+
+ foreach ($items as $item) {
+ if ($item[0] === 'null') {
+ continue;
+ }
+
+ $filtered[] = $this->compileValue($item);
+ }
+
+ return implode("$delim ", $filtered);
+
+ case 'map':
+ $keys = $value[1];
+ $values = $value[2];
+ $filtered = array();
+
+ for ($i = 0, $s = count($keys); $i < $s; $i++) {
+ $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]);
+ }
+
+ array_walk($filtered, function (&$value, $key) {
+ $value = $key . ': ' . $value;
+ });
+
+ return '(' . implode(', ', $filtered) . ')';
+
+ case 'interpolated':
+ // node created by extractInterpolation
+ list(, $interpolate, $left, $right) = $value;
+ list(,, $whiteLeft, $whiteRight) = $interpolate;
+
+ $left = count($left[2]) > 0 ?
+ $this->compileValue($left) . $whiteLeft : '';
+
+ $right = count($right[2]) > 0 ?
+ $whiteRight . $this->compileValue($right) : '';
+
+ return $left . $this->compileValue($interpolate) . $right;
+
+ case 'interpolate':
+ // raw parse node
+ list(, $exp) = $value;
+
+ // strip quotes if it's a string
+ $reduced = $this->reduce($exp);
+ switch ($reduced[0]) {
+ case 'string':
+ $reduced = array('keyword', $this->compileStringContent($reduced));
+ break;
+
+ case 'null':
+ $reduced = array('keyword', '');
+ }
+
+ return $this->compileValue($reduced);
+
+ case 'null':
+ return 'null';
+
+ default:
+ $this->throwError("unknown value type: $type");
+ }
+ }
+
+ /**
+ * Flatten list
+ *
+ * @param array $list
+ *
+ * @return string
+ */
+ protected function flattenList($list)
+ {
+ return $this->compileValue($list);
+ }
+
+ /**
+ * Compile string content
+ *
+ * @param array $string
+ *
+ * @return string
+ */
+ protected function compileStringContent($string)
+ {
+ $parts = array();
+
+ foreach ($string[2] as $part) {
+ if (is_array($part)) {
+ $parts[] = $this->compileValue($part);
+ } else {
+ $parts[] = $part;
+ }
+ }
+
+ return implode($parts);
+ }
+
+ /**
+ * Extract interpolation; it doesn't need to be recursive, compileValue will handle that
+ *
+ * @param array $list
+ *
+ * @return array
+ */
+ protected function extractInterpolation($list)
+ {
+ $items = $list[2];
+
+ foreach ($items as $i => $item) {
+ if ($item[0] === 'interpolate') {
+ $before = array('list', $list[1], array_slice($items, 0, $i));
+ $after = array('list', $list[1], array_slice($items, $i + 1));
+
+ return array('interpolated', $item, $before, $after);
+ }
+ }
+
+ return $list;
+ }
+
+ /**
+ * Find the final set of selectors
+ *
+ * @param \stdClass $env
+ *
+ * @return array
+ */
+ protected function multiplySelectors($env)
+ {
+ $envs = array();
+
+ while (null !== $env) {
+ if (! empty($env->selectors)) {
+ $envs[] = $env;
+ }
+
+ $env = $env->parent;
+ };
+
+ $selectors = array();
+ $parentSelectors = array(array());
+
+ while ($env = array_pop($envs)) {
+ $selectors = array();
+
+ foreach ($env->selectors as $selector) {
+ foreach ($parentSelectors as $parent) {
+ $selectors[] = $this->joinSelectors($parent, $selector);
+ }
+ }
+
+ $parentSelectors = $selectors;
+ }
+
+ return $selectors;
+ }
+
+ /**
+ * Join selectors; looks for & to replace, or append parent before child
+ *
+ * @param array $parent
+ * @param array $child
+ *
+ * @return array
+ */
+ protected function joinSelectors($parent, $child)
+ {
+ $setSelf = false;
+ $out = array();
+
+ foreach ($child as $part) {
+ $newPart = array();
+
+ foreach ($part as $p) {
+ if ($p === self::$selfSelector) {
+ $setSelf = true;
+
+ foreach ($parent as $i => $parentPart) {
+ if ($i > 0) {
+ $out[] = $newPart;
+ $newPart = array();
+ }
+
+ foreach ($parentPart as $pp) {
+ $newPart[] = $pp;
+ }
+ }
+ } else {
+ $newPart[] = $p;
+ }
+ }
+
+ $out[] = $newPart;
+ }
+
+ return $setSelf ? $out : array_merge($parent, $child);
+ }
+
+ /**
+ * Multiply media
+ *
+ * @param \stdClass $env
+ * @param array $childQueries
+ *
+ * @return array
+ */
+ protected function multiplyMedia($env, $childQueries = null)
+ {
+ if (! isset($env) ||
+ ! empty($env->block->type) && $env->block->type !== 'media'
+ ) {
+ return $childQueries;
+ }
+
+ // plain old block, skip
+ if (empty($env->block->type)) {
+ return $this->multiplyMedia($env->parent, $childQueries);
+ }
+
+ $parentQueries = $env->block->queryList;
+ if ($childQueries === null) {
+ $childQueries = $parentQueries;
+ } else {
+ $originalQueries = $childQueries;
+ $childQueries = array();
+
+ foreach ($parentQueries as $parentQuery) {
+ foreach ($originalQueries as $childQuery) {
+ $childQueries []= array_merge($parentQuery, $childQuery);
+ }
+ }
+ }
+
+ return $this->multiplyMedia($env->parent, $childQueries);
+ }
+
+ /**
+ * Push environment
+ *
+ * @param \stdClass $block
+ *
+ * @return \stdClass
+ */
+ protected function pushEnv($block = null)
+ {
+ $env = new \stdClass;
+ $env->parent = $this->env;
+ $env->store = array();
+ $env->block = $block;
+ $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
+
+ $this->env = $env;
+
+ return $env;
+ }
+
+ /**
+ * Pop environment
+ */
+ protected function popEnv()
+ {
+ $env = $this->env;
+ $this->env = $this->env->parent;
+
+ return $env;
+ }
+
+ /**
+ * Get store environment
+ *
+ * @return \stdClass
+ */
+ protected function getStoreEnv()
+ {
+ return isset($this->storeEnv) ? $this->storeEnv : $this->env;
+ }
+
+ /**
+ * Set variable
+ *
+ * @param string $name
+ * @param mixed $value
+ * @param boolean $shadow
+ * @param \stdClass $env
+ */
+ protected function set($name, $value, $shadow = false, $env = null)
+ {
+ $name = $this->normalizeName($name);
+
+ if ($shadow) {
+ $this->setRaw($name, $value, $env);
+ } else {
+ $this->setExisting($name, $value, $env);
+ }
+ }
+
+ /**
+ * Set existing variable
+ *
+ * @param string $name
+ * @param mixed $value
+ * @param \stdClass $env
+ */
+ protected function setExisting($name, $value, $env = null)
+ {
+ if (! isset($env)) {
+ $env = $this->getStoreEnv();
+ }
+
+ $storeEnv = $env;
+
+ $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
+
+ for (;;) {
+ if (array_key_exists($name, $env->store)) {
+ break;
+ }
+
+ if (! $hasNamespace && isset($env->marker)) {
+ $env = $storeEnv;
+ break;
+ }
+
+ if (! isset($env->parent)) {
+ $env = $storeEnv;
+ break;
+ }
+
+ $env = $env->parent;
+ }
+
+ $env->store[$name] = $value;
+ }
+
+ /**
+ * Set raw variable
+ *
+ * @param string $name
+ * @param mixed $value
+ * @param \stdClass $env
+ */
+ protected function setRaw($name, $value, $env = null)
+ {
+ if (! isset($env)) {
+ $env = $this->getStoreEnv();
+ }
+
+ $env->store[$name] = $value;
+ }
+
+ /**
+ * Get variable
+ *
+ * @api
+ *
+ * @param string $name
+ * @param boolean $shouldThrow
+ * @param \stdClass $env
+ *
+ * @return mixed
+ */
+ public function get($name, $shouldThrow = true, $env = null)
+ {
+ $name = $this->normalizeName($name);
+
+ if (! isset($env)) {
+ $env = $this->getStoreEnv();
+ }
+
+ $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
+
+ for (;;) {
+ if (array_key_exists($name, $env->store)) {
+ return $env->store[$name];
+ }
+
+ if (! $hasNamespace && isset($env->marker)) {
+ $env = $this->rootEnv;
+ continue;
+ }
+
+ if (! isset($env->parent)) {
+ break;
+ }
+
+ $env = $env->parent;
+ }
+
+ if ($shouldThrow) {
+ $this->throwError("Undefined variable \$$name");
+ }
+
+ // found nothing
+ }
+
+ /**
+ * Has variable?
+ *
+ * @param string $name
+ * @param \stdClass $env
+ *
+ * @return boolean
+ */
+ protected function has($name, $env = null)
+ {
+ return $this->get($name, false, $env) !== null;
+ }
+
+ /**
+ * Inject variables
+ *
+ * @param array $args
+ */
+ protected function injectVariables(array $args)
+ {
+ if (empty($args)) {
+ return;
+ }
+
+ $parser = new Parser(__METHOD__, false);
+
+ foreach ($args as $name => $strValue) {
+ if ($name[0] === '$') {
+ $name = substr($name, 1);
+ }
+
+ if (! $parser->parseValue($strValue, $value)) {
+ $value = $this->coerceValue($strValue);
+ }
+
+ $this->set($name, $value);
+ }
+ }
+
+ /**
+ * Set variables
+ *
+ * @api
+ *
+ * @param array $variables
+ */
+ public function setVariables(array $variables)
+ {
+ $this->registeredVars = array_merge($this->registeredVars, $variables);
+ }
+
+ /**
+ * Unset variable
+ *
+ * @api
+ *
+ * @param string $name
+ */
+ public function unsetVariable($name)
+ {
+ unset($this->registeredVars[$name]);
+ }
+
+ /**
+ * Returns list of parsed files
+ *
+ * @api
+ *
+ * @return array
+ */
+ public function getParsedFiles()
+ {
+ return $this->parsedFiles;
+ }
+
+ /**
+ * Add import path
+ *
+ * @api
+ *
+ * @param string $path
+ */
+ public function addImportPath($path)
+ {
+ if (! in_array($path, $this->importPaths)) {
+ $this->importPaths[] = $path;
+ }
+ }
+
+ /**
+ * Set import paths
+ *
+ * @api
+ *
+ * @param string|array $path
+ */
+ public function setImportPaths($path)
+ {
+ $this->importPaths = (array)$path;
+ }
+
+ /**
+ * Set number precision
+ *
+ * @api
+ *
+ * @param integer $numberPrecision
+ */
+ public function setNumberPrecision($numberPrecision)
+ {
+ $this->numberPrecision = $numberPrecision;
+ }
+
+ /**
+ * Set formatter
+ *
+ * @api
+ *
+ * @param string $formatterName
+ */
+ public function setFormatter($formatterName)
+ {
+ $this->formatter = $formatterName;
+ }
+
+ /**
+ * Set line number style
+ *
+ * @api
+ *
+ * @param string $lineNumberStyle
+ */
+ public function setLineNumberStyle($lineNumberStyle)
+ {
+ $this->lineNumberStyle = $lineNumberStyle;
+ }
+
+ /**
+ * Register function
+ *
+ * @api
+ *
+ * @param string $name
+ * @param callable $func
+ */
+ public function registerFunction($name, $func)
+ {
+ $this->userFunctions[$this->normalizeName($name)] = $func;
+ }
+
+ /**
+ * Unregister function
+ *
+ * @api
+ *
+ * @param string $name
+ */
+ public function unregisterFunction($name)
+ {
+ unset($this->userFunctions[$this->normalizeName($name)]);
+ }
+
+ /**
+ * Import file
+ *
+ * @param string $path
+ * @param array $out
+ */
+ protected function importFile($path, $out)
+ {
+ // see if tree is cached
+ $realPath = realpath($path);
+
+ if (isset($this->importCache[$realPath])) {
+ $this->handleImportLoop($realPath);
+
+ $tree = $this->importCache[$realPath];
+ } else {
+ $code = file_get_contents($path);
+ $parser = new Parser($path, false);
+ $tree = $parser->parse($code);
+
+ $this->parsedFiles[$realPath] = filemtime($path);
+ $this->importCache[$realPath] = $tree;
+ }
+
+ $pi = pathinfo($path);
+ array_unshift($this->importPaths, $pi['dirname']);
+ $this->compileChildren($tree->children, $out);
+ array_shift($this->importPaths);
+ }
+
+ /**
+ * Return the file path for an import url if it exists
+ *
+ * @api
+ *
+ * @param string $url
+ *
+ * @return string|null
+ */
+ public function findImport($url)
+ {
+ $urls = array();
+
+ // for "normal" scss imports (ignore vanilla css and external requests)
+ if (! preg_match('/\.css$|^https?:\/\//', $url)) {
+ // try both normal and the _partial filename
+ $urls = array($url, preg_replace('/[^\/]+$/', '_\0', $url));
+ }
+
+ foreach ($this->importPaths as $dir) {
+ if (is_string($dir)) {
+ // check urls for normal import paths
+ foreach ($urls as $full) {
+ $full = $dir .
+ (! empty($dir) && substr($dir, -1) !== '/' ? '/' : '') .
+ $full;
+
+ if ($this->fileExists($file = $full . '.scss') ||
+ $this->fileExists($file = $full)
+ ) {
+ return $file;
+ }
+ }
+ } elseif (is_callable($dir)) {
+ // check custom callback for import path
+ $file = call_user_func($dir, $url, $this);
+
+ if ($file !== null) {
+ return $file;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Throw error (exception)
+ *
+ * @api
+ *
+ * @param string $msg Message with optional sprintf()-style vararg parameters
+ *
+ * @throws \Exception
+ */
+ public function throwError($msg)
+ {
+ if (func_num_args() > 1) {
+ $msg = call_user_func_array('sprintf', func_get_args());
+ }
+
+ if ($this->sourcePos >= 0 && isset($this->sourceParser)) {
+ $this->sourceParser->throwParseError($msg, $this->sourcePos);
+ }
+
+ throw new \Exception($msg);
+ }
+
+ /**
+ * Handle import loop
+ *
+ * @param string $name
+ *
+ * @throws \Exception
+ */
+ private function handleImportLoop($name)
+ {
+ for ($env = $this->env; $env; $env = $env->parent) {
+ $file = $env->block->sourceParser->getSourceName();
+
+ if (realpath($file) === $name) {
+ $this->throwError(
+ 'An @import loop has been found: %s imports %s',
+ $this->env->block->sourceParser->getSourceName(),
+ basename($file)
+ );
+ }
+ }
+ }
+
+ /**
+ * Does file exist?
+ *
+ * @param string $name
+ *
+ * @return boolean
+ */
+ protected function fileExists($name)
+ {
+ return is_file($name);
+ }
+
+ /**
+ * Call built-in and registered (PHP) functions
+ *
+ * @param string $name
+ * @param array $args
+ * @param array $returnValue
+ *
+ * @return boolean Returns true if returnValue is set; otherwise, false
+ */
+ protected function callBuiltin($name, $args, &$returnValue)
+ {
+ // try a lib function
+ $name = $this->normalizeName($name);
+
+ if (isset($this->userFunctions[$name])) {
+ // see if we can find a user function
+ $fn = $this->userFunctions[$name];
+
+ foreach ($args as &$val) {
+ $val = $this->reduce($val[1], true);
+ }
+
+ $returnValue = call_user_func($fn, $args, $this);
+ } else {
+ $f = $this->getBuiltinFunction($name);
+
+ if (is_callable($f)) {
+ $libName = $f[1];
+
+ $prototype = isset(self::$$libName) ? self::$$libName : null;
+ $sorted = $this->sortArgs($prototype, $args);
+
+ foreach ($sorted as &$val) {
+ $val = $this->reduce($val, true);
+ }
+
+ $returnValue = call_user_func($f, $sorted, $this);
+ }
+ }
+
+ if (isset($returnValue)) {
+ $returnValue = $this->coerceValue($returnValue);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get built-in function
+ *
+ * @param string $name Normalized name
+ *
+ * @return array
+ */
+ protected function getBuiltinFunction($name)
+ {
+ $libName = 'lib' . preg_replace_callback(
+ '/_(.)/',
+ function ($m) {
+ return ucfirst($m[1]);
+ },
+ ucfirst($name)
+ );
+
+ return array($this, $libName);
+ }
+
+ /**
+ * Sorts keyword arguments
+ *
+ * @todo Merge with applyArguments()?
+ *
+ * @param array $prototype
+ * @param array $args
+ *
+ * @return array
+ */
+ protected function sortArgs($prototype, $args)
+ {
+ $keyArgs = array();
+ $posArgs = array();
+
+ foreach ($args as $arg) {
+ list($key, $value) = $arg;
+
+ $key = $key[1];
+
+ if (empty($key)) {
+ $posArgs[] = $value;
+ } else {
+ $keyArgs[$key] = $value;
+ }
+ }
+
+ if (! isset($prototype)) {
+ return $posArgs;
+ }
+
+ $finalArgs = array();
+
+ foreach ($prototype as $i => $names) {
+ if (isset($posArgs[$i])) {
+ $finalArgs[] = $posArgs[$i];
+ continue;
+ }
+
+ $set = false;
+
+ foreach ((array)$names as $name) {
+ if (isset($keyArgs[$name])) {
+ $finalArgs[] = $keyArgs[$name];
+ $set = true;
+ break;
+ }
+ }
+
+ if (! $set) {
+ $finalArgs[] = null;
+ }
+ }
+
+ return $finalArgs;
+ }
+
+ /**
+ * Apply argument values per definition
+ *
+ * @param array $argDef
+ * @param array $argValues
+ *
+ * @throws \Exception
+ */
+ protected function applyArguments($argDef, $argValues)
+ {
+ $storeEnv = $this->getStoreEnv();
+
+ $env = new \stdClass;
+ $env->store = $storeEnv->store;
+
+ $hasVariable = false;
+ $args = array();
+
+ foreach ($argDef as $i => $arg) {
+ list($name, $default, $isVariable) = $argDef[$i];
+
+ $args[$name] = array($i, $name, $default, $isVariable);
+ $hasVariable |= $isVariable;
+ }
+
+ $keywordArgs = array();
+ $deferredKeywordArgs = array();
+ $remaining = array();
+
+ // assign the keyword args
+ foreach ((array) $argValues as $arg) {
+ if (! empty($arg[0])) {
+ if (! isset($args[$arg[0][1]])) {
+ if ($hasVariable) {
+ $deferredKeywordArgs[$arg[0][1]] = $arg[1];
+ } else {
+ $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
+ }
+ } elseif ($args[$arg[0][1]][0] < count($remaining)) {
+ $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
+ } else {
+ $keywordArgs[$arg[0][1]] = $arg[1];
+ }
+ } elseif (count($keywordArgs)) {
+ $this->throwError('Positional arguments must come before keyword arguments.');
+ } elseif ($arg[2] === true) {
+ $val = $this->reduce($arg[1], true);
+
+ if ($val[0] === 'list') {
+ foreach ($val[2] as $name => $item) {
+ if (! is_numeric($name)) {
+ $keywordArgs[$name] = $item;
+ } else {
+ $remaining[] = $item;
+ }
+ }
+ } else {
+ $remaining[] = $val;
+ }
+ } else {
+ $remaining[] = $arg[1];
+ }
+ }
+
+ foreach ($args as $arg) {
+ list($i, $name, $default, $isVariable) = $arg;
+
+ if ($isVariable) {
+ $val = array('list', ',', array(), $isVariable);
+
+ for ($count = count($remaining); $i < $count; $i++) {
+ $val[2][] = $remaining[$i];
+ }
+
+ foreach ($deferredKeywordArgs as $itemName => $item) {
+ $val[2][$itemName] = $item;
+ }
+ } elseif (isset($remaining[$i])) {
+ $val = $remaining[$i];
+ } elseif (isset($keywordArgs[$name])) {
+ $val = $keywordArgs[$name];
+ } elseif (! empty($default)) {
+ continue;
+ } else {
+ $this->throwError("Missing argument $name");
+ }
+
+ $this->set($name, $this->reduce($val, true), true, $env);
+ }
+
+ $storeEnv->store = $env->store;
+
+ foreach ($args as $arg) {
+ list($i, $name, $default, $isVariable) = $arg;
+
+ if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) {
+ continue;
+ }
+
+ $this->set($name, $this->reduce($default, true), true);
+ }
+ }
+
+ /**
+ * Coerce a php value into a scss one
+ *
+ * @param mixed $value
+ *
+ * @return array
+ */
+ private function coerceValue($value)
+ {
+ if (is_array($value)) {
+ return $value;
+ }
+
+ if (is_bool($value)) {
+ return $value ? self::$true : self::$false;
+ }
+
+ if ($value === null) {
+ $value = self::$null;
+ }
+
+ if (is_numeric($value)) {
+ return array('number', $value, '');
+ }
+
+ if ($value === '') {
+ return self::$emptyString;
+ }
+
+ return array('keyword', $value);
+ }
+
+ /**
+ * Coerce unit on number to be normalized
+ *
+ * @param array $number
+ * @param string $unit
+ *
+ * @return array
+ */
+ protected function coerceUnit($number, $unit)
+ {
+ list(, $value, $baseUnit) = $number;
+
+ if (isset(self::$unitTable[$baseUnit][$unit])) {
+ $value = $value * self::$unitTable[$baseUnit][$unit];
+ }
+
+ return array('number', $value, $unit);
+ }
+
+ /**
+ * Coerce something to map
+ *
+ * @param array $item
+ *
+ * @return array
+ */
+ protected function coerceMap($item)
+ {
+ if ($item[0] === 'map') {
+ return $item;
+ }
+
+ if ($item === self::$emptyList) {
+ return self::$emptyMap;
+ }
+
+ return array('map', array($item), array(self::$null));
+ }
+
+ /**
+ * Coerce something to list
+ *
+ * @param array $item
+ *
+ * @return array
+ */
+ protected function coerceList($item, $delim = ',')
+ {
+ if (isset($item) && $item[0] === 'list') {
+ return $item;
+ }
+
+ if (isset($item) && $item[0] === 'map') {
+ $keys = $item[1];
+ $values = $item[2];
+ $list = array();
+
+ for ($i = 0, $s = count($keys); $i < $s; $i++) {
+ $key = $keys[$i];
+ $value = $values[$i];
+
+ $list[] = array('list', '', array(array('keyword', $this->compileValue($key)), $value));
+ }
+
+ return array('list', ',', $list);
+ }
+
+ return array('list', $delim, ! isset($item) ? array(): array($item));
+ }
+
+ /**
+ * Coerce color for expression
+ *
+ * @param array $value
+ *
+ * @return array|null
+ */
+ protected function coerceForExpression($value)
+ {
+ if ($color = $this->coerceColor($value)) {
+ return $color;
+ }
+
+ return $value;
+ }
+
+ /**
+ * Coerce value to color
+ *
+ * @param array $value
+ *
+ * @return array|null
+ */
+ protected function coerceColor($value)
+ {
+ switch ($value[0]) {
+ case 'color':
+ return $value;
+
+ case 'keyword':
+ $name = strtolower($value[1]);
+
+ if (isset(Colors::$cssColors[$name])) {
+ $rgba = explode(',', Colors::$cssColors[$name]);
+
+ return isset($rgba[3])
+ ? array('color', (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3])
+ : array('color', (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]);
+ }
+
+ return null;
+ }
+
+ return null;
+ }
+
+ /**
+ * Coerce value to string
+ *
+ * @param array $value
+ *
+ * @return array|null
+ */
+ protected function coerceString($value)
+ {
+ if ($value[0] === 'string') {
+ return $value;
+ }
+
+ return array('string', '', array($this->compileValue($value)));
+ }
+
+ /**
+ * Coerce value to a percentage
+ *
+ * @param array $value
+ *
+ * @return integer|float
+ */
+ protected function coercePercent($value)
+ {
+ if ($value[0] === 'number') {
+ if ($value[2] === '%') {
+ return $value[1] / 100;
+ }
+
+ return $value[1];
+ }
+
+ return 0;
+ }
+
+ /**
+ * Assert value is a map
+ *
+ * @api
+ *
+ * @param array $value
+ *
+ * @return array
+ *
+ * @throws \Exception
+ */
+ public function assertMap($value)
+ {
+ $value = $this->coerceMap($value);
+
+ if ($value[0] !== 'map') {
+ $this->throwError('expecting map');
+ }
+
+ return $value;
+ }
+
+ /**
+ * Assert value is a list
+ *
+ * @api
+ *
+ * @param array $value
+ *
+ * @return array
+ *
+ * @throws \Exception
+ */
+ public function assertList($value)
+ {
+ if ($value[0] !== 'list') {
+ $this->throwError('expecting list');
+ }
+
+ return $value;
+ }
+
+ /**
+ * Assert value is a color
+ *
+ * @api
+ *
+ * @param array $value
+ *
+ * @return array
+ *
+ * @throws \Exception
+ */
+ public function assertColor($value)
+ {
+ if ($color = $this->coerceColor($value)) {
+ return $color;
+ }
+
+ $this->throwError('expecting color');
+ }
+
+ /**
+ * Assert value is a number
+ *
+ * @api
+ *
+ * @param array $value
+ *
+ * @return integer|float
+ *
+ * @throws \Exception
+ */
+ public function assertNumber($value)
+ {
+ if ($value[0] !== 'number') {
+ $this->throwError('expecting number');
+ }
+
+ return $value[1];
+ }
+
+ /**
+ * Make sure a color's components don't go out of bounds
+ *
+ * @param array $c
+ *
+ * @return array
+ */
+ protected function fixColor($c)
+ {
+ foreach (range(1, 3) as $i) {
+ if ($c[$i] < 0) {
+ $c[$i] = 0;
+ }
+
+ if ($c[$i] > 255) {
+ $c[$i] = 255;
+ }
+ }
+
+ return $c;
+ }
+
+ /**
+ * Convert RGB to HSL
+ *
+ * @api
+ *
+ * @param integer $red
+ * @param integer $green
+ * @param integer $blue
+ *
+ * @return array
+ */
+ public function toHSL($red, $green, $blue)
+ {
+ $min = min($red, $green, $blue);
+ $max = max($red, $green, $blue);
+
+ $l = $min + $max;
+ $d = $max - $min;
+
+ if ((int) $d === 0) {
+ $h = $s = 0;
+ } else {
+ if ($l < 255) {
+ $s = $d / $l;
+ } else {
+ $s = $d / (510 - $l);
+ }
+
+ if ($red == $max) {
+ $h = 60 * ($green - $blue) / $d;
+ } elseif ($green == $max) {
+ $h = 60 * ($blue - $red) / $d + 120;
+ } elseif ($blue == $max) {
+ $h = 60 * ($red - $green) / $d + 240;
+ }
+ }
+
+ return array('hsl', fmod($h, 360), $s * 100, $l / 5.1);
+ }
+
+ /**
+ * Hue to RGB helper
+ *
+ * @param float $m1
+ * @param float $m2
+ * @param float $h
+ *
+ * @return float
+ */
+ private function hueToRGB($m1, $m2, $h)
+ {
+ if ($h < 0) {
+ $h += 1;
+ } elseif ($h > 1) {
+ $h -= 1;
+ }
+
+ if ($h * 6 < 1) {
+ return $m1 + ($m2 - $m1) * $h * 6;
+ }
+
+ if ($h * 2 < 1) {
+ return $m2;
+ }
+
+ if ($h * 3 < 2) {
+ return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
+ }
+
+ return $m1;
+ }
+
+ /**
+ * Convert HSL to RGB
+ *
+ * @api
+ *
+ * @param integer $hue H from 0 to 360
+ * @param integer $saturation S from 0 to 100
+ * @param integer $lightness L from 0 to 100
+ *
+ * @return array
+ */
+ public function toRGB($hue, $saturation, $lightness)
+ {
+ if ($hue < 0) {
+ $hue += 360;
+ }
+
+ $h = $hue / 360;
+ $s = min(100, max(0, $saturation)) / 100;
+ $l = min(100, max(0, $lightness)) / 100;
+
+ $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
+ $m1 = $l * 2 - $m2;
+
+ $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
+ $g = $this->hueToRGB($m1, $m2, $h) * 255;
+ $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
+
+ $out = array('color', $r, $g, $b);
+
+ return $out;
+ }
+
+ // Built in functions
+
+ protected static $libIf = array('condition', 'if-true', 'if-false');
+ protected function libIf($args)
+ {
+ list($cond, $t, $f) = $args;
+
+ if (! $this->isTruthy($cond)) {
+ return $f;
+ }
+
+ return $t;
+ }
+
+ protected static $libIndex = array('list', 'value');
+ protected function libIndex($args)
+ {
+ list($list, $value) = $args;
+
+ if ($value[0] === 'map') {
+ return self::$null;
+ }
+
+ if ($list[0] === 'map') {
+ $list = $this->coerceList($list, ' ');
+ }
+
+ if ($list[0] !== 'list') {
+ return self::$null;
+ }
+
+ $values = array();
+
+ foreach ($list[2] as $item) {
+ $values[] = $this->normalizeValue($item);
+ }
+
+ $key = array_search($this->normalizeValue($value), $values);
+
+ return false === $key ? self::$null : $key + 1;
+ }
+
+ protected static $libRgb = array('red', 'green', 'blue');
+ protected function libRgb($args)
+ {
+ list($r, $g, $b) = $args;
+
+ return array('color', $r[1], $g[1], $b[1]);
+ }
+
+ protected static $libRgba = array(
+ array('red', 'color'),
+ 'green', 'blue', 'alpha');
+ protected function libRgba($args)
+ {
+ if ($color = $this->coerceColor($args[0])) {
+ // workaround https://github.com/facebook/hhvm/issues/5457
+ reset($args);
+
+ $num = ! isset($args[1]) ? $args[3] : $args[1];
+ $alpha = $this->assertNumber($num);
+ $color[4] = $alpha;
+
+ return $color;
+ }
+
+ list($r, $g, $b, $a) = $args;
+
+ return array('color', $r[1], $g[1], $b[1], $a[1]);
+ }
+
+ // helper function for adjust_color, change_color, and scale_color
+ protected function alterColor($args, $fn)
+ {
+ $color = $this->assertColor($args[0]);
+
+ // workaround https://github.com/facebook/hhvm/issues/5457
+ reset($args);
+
+ foreach (array(1, 2, 3, 7) as $i) {
+ if (isset($args[$i])) {
+ $val = $this->assertNumber($args[$i]);
+ $ii = $i === 7 ? 4 : $i; // alpha
+ $color[$ii] = call_user_func($fn, isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
+ }
+ }
+
+ if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
+
+ foreach (array(4, 5, 6) as $i) {
+ if (isset($args[$i])) {
+ $val = $this->assertNumber($args[$i]);
+ $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i);
+ }
+ }
+
+ $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
+
+ if (isset($color[4])) {
+ $rgb[4] = $color[4];
+ }
+
+ $color = $rgb;
+ }
+
+ return $color;
+ }
+
+ protected static $libAdjustColor = array(
+ 'color', 'red', 'green', 'blue',
+ 'hue', 'saturation', 'lightness', 'alpha'
+ );
+ protected function libAdjustColor($args)
+ {
+ return $this->alterColor($args, function ($base, $alter, $i) {
+ return $base + $alter;
+ });
+ }
+
+ protected static $libChangeColor = array(
+ 'color', 'red', 'green', 'blue',
+ 'hue', 'saturation', 'lightness', 'alpha'
+ );
+ protected function libChangeColor($args)
+ {
+ return $this->alterColor($args, function ($base, $alter, $i) {
+ return $alter;
+ });
+ }
+
+ protected static $libScaleColor = array(
+ 'color', 'red', 'green', 'blue',
+ 'hue', 'saturation', 'lightness', 'alpha'
+ );
+ protected function libScaleColor($args)
+ {
+ return $this->alterColor($args, function ($base, $scale, $i) {
+ // 1, 2, 3 - rgb
+ // 4, 5, 6 - hsl
+ // 7 - a
+ switch ($i) {
+ case 1:
+ case 2:
+ case 3:
+ $max = 255;
+ break;
+
+ case 4:
+ $max = 360;
+ break;
+
+ case 7:
+ $max = 1;
+ break;
+
+ default:
+ $max = 100;
+ }
+
+ $scale = $scale / 100;
+
+ if ($scale < 0) {
+ return $base * $scale + $base;
+ }
+
+ return ($max - $base) * $scale + $base;
+ });
+ }
+
+ protected static $libIeHexStr = array('color');
+ protected function libIeHexStr($args)
+ {
+ $color = $this->coerceColor($args[0]);
+ $color[4] = isset($color[4]) ? round(255*$color[4]) : 255;
+
+ return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
+ }
+
+ protected static $libRed = array('color');
+ protected function libRed($args)
+ {
+ $color = $this->coerceColor($args[0]);
+
+ return $color[1];
+ }
+
+ protected static $libGreen = array('color');
+ protected function libGreen($args)
+ {
+ $color = $this->coerceColor($args[0]);
+
+ return $color[2];
+ }
+
+ protected static $libBlue = array('color');
+ protected function libBlue($args)
+ {
+ $color = $this->coerceColor($args[0]);
+
+ return $color[3];
+ }
+
+ protected static $libAlpha = array('color');
+ protected function libAlpha($args)
+ {
+ if ($color = $this->coerceColor($args[0])) {
+ return isset($color[4]) ? $color[4] : 1;
+ }
+
+ // this might be the IE function, so return value unchanged
+ return null;
+ }
+
+ protected static $libOpacity = array('color');
+ protected function libOpacity($args)
+ {
+ $value = $args[0];
+
+ if ($value[0] === 'number') {
+ return null;
+ }
+
+ return $this->libAlpha($args);
+ }
+
+ // mix two colors
+ protected static $libMix = array('color-1', 'color-2', 'weight');
+ protected function libMix($args)
+ {
+ list($first, $second, $weight) = $args;
+
+ $first = $this->assertColor($first);
+ $second = $this->assertColor($second);
+
+ if (! isset($weight)) {
+ $weight = 0.5;
+ } else {
+ $weight = $this->coercePercent($weight);
+ }
+
+ $firstAlpha = isset($first[4]) ? $first[4] : 1;
+ $secondAlpha = isset($second[4]) ? $second[4] : 1;
+
+ $w = $weight * 2 - 1;
+ $a = $firstAlpha - $secondAlpha;
+
+ $w1 = (($w * $a === -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0;
+ $w2 = 1.0 - $w1;
+
+ $new = array('color',
+ $w1 * $first[1] + $w2 * $second[1],
+ $w1 * $first[2] + $w2 * $second[2],
+ $w1 * $first[3] + $w2 * $second[3],
+ );
+
+ if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
+ $new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1);
+ }
+
+ return $this->fixColor($new);
+ }
+
+ protected static $libHsl = array('hue', 'saturation', 'lightness');
+ protected function libHsl($args)
+ {
+ list($h, $s, $l) = $args;
+
+ return $this->toRGB($h[1], $s[1], $l[1]);
+ }
+
+ protected static $libHsla = array('hue', 'saturation',
+ 'lightness', 'alpha');
+ protected function libHsla($args)
+ {
+ list($h, $s, $l, $a) = $args;
+
+ $color = $this->toRGB($h[1], $s[1], $l[1]);
+ $color[4] = $a[1];
+
+ return $color;
+ }
+
+ protected static $libHue = array('color');
+ protected function libHue($args)
+ {
+ $color = $this->assertColor($args[0]);
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
+
+ return array('number', $hsl[1], 'deg');
+ }
+
+ protected static $libSaturation = array('color');
+ protected function libSaturation($args)
+ {
+ $color = $this->assertColor($args[0]);
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
+
+ return array('number', $hsl[2], '%');
+ }
+
+ protected static $libLightness = array('color');
+ protected function libLightness($args)
+ {
+ $color = $this->assertColor($args[0]);
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
+
+ return array('number', $hsl[3], '%');
+ }
+
+ protected function adjustHsl($color, $idx, $amount)
+ {
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
+ $hsl[$idx] += $amount;
+ $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
+
+ if (isset($color[4])) {
+ $out[4] = $color[4];
+ }
+
+ return $out;
+ }
+
+ protected static $libAdjustHue = array('color', 'degrees');
+ protected function libAdjustHue($args)
+ {
+ $color = $this->assertColor($args[0]);
+ $degrees = $this->assertNumber($args[1]);
+
+ return $this->adjustHsl($color, 1, $degrees);
+ }
+
+ protected static $libLighten = array('color', 'amount');
+ protected function libLighten($args)
+ {
+ $color = $this->assertColor($args[0]);
+ $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
+
+ return $this->adjustHsl($color, 3, $amount);
+ }
+
+ protected static $libDarken = array('color', 'amount');
+ protected function libDarken($args)
+ {
+ $color = $this->assertColor($args[0]);
+ $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
+
+ return $this->adjustHsl($color, 3, -$amount);
+ }
+
+ protected static $libSaturate = array('color', 'amount');
+ protected function libSaturate($args)
+ {
+ $value = $args[0];
+
+ if ($value[0] === 'number') {
+ return null;
+ }
+
+ $color = $this->assertColor($value);
+ $amount = 100*$this->coercePercent($args[1]);
+
+ return $this->adjustHsl($color, 2, $amount);
+ }
+
+ protected static $libDesaturate = array('color', 'amount');
+ protected function libDesaturate($args)
+ {
+ $color = $this->assertColor($args[0]);
+ $amount = 100*$this->coercePercent($args[1]);
+
+ return $this->adjustHsl($color, 2, -$amount);
+ }
+
+ protected static $libGrayscale = array('color');
+ protected function libGrayscale($args)
+ {
+ $value = $args[0];
+
+ if ($value[0] === 'number') {
+ return null;
+ }
+
+ return $this->adjustHsl($this->assertColor($value), 2, -100);
+ }
+
+ protected static $libComplement = array('color');
+ protected function libComplement($args)
+ {
+ return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
+ }
+
+ protected static $libInvert = array('color');
+ protected function libInvert($args)
+ {
+ $value = $args[0];
+
+ if ($value[0] === 'number') {
+ return null;
+ }
+
+ $color = $this->assertColor($value);
+ $color[1] = 255 - $color[1];
+ $color[2] = 255 - $color[2];
+ $color[3] = 255 - $color[3];
+
+ return $color;
+ }
+
+ // increases opacity by amount
+ protected static $libOpacify = array('color', 'amount');
+ protected function libOpacify($args)
+ {
+ $color = $this->assertColor($args[0]);
+ $amount = $this->coercePercent($args[1]);
+
+ $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
+ $color[4] = min(1, max(0, $color[4]));
+
+ return $color;
+ }
+
+ protected static $libFadeIn = array('color', 'amount');
+ protected function libFadeIn($args)
+ {
+ return $this->libOpacify($args);
+ }
+
+ // decreases opacity by amount
+ protected static $libTransparentize = array('color', 'amount');
+ protected function libTransparentize($args)
+ {
+ $color = $this->assertColor($args[0]);
+ $amount = $this->coercePercent($args[1]);
+
+ $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
+ $color[4] = min(1, max(0, $color[4]));
+
+ return $color;
+ }
+
+ protected static $libFadeOut = array('color', 'amount');
+ protected function libFadeOut($args)
+ {
+ return $this->libTransparentize($args);
+ }
+
+ protected static $libUnquote = array('string');
+ protected function libUnquote($args)
+ {
+ $str = $args[0];
+
+ if ($str[0] === 'string') {
+ $str[1] = '';
+ }
+
+ return $str;
+ }
+
+ protected static $libQuote = array('string');
+ protected function libQuote($args)
+ {
+ $value = $args[0];
+
+ if ($value[0] === 'string' && ! empty($value[1])) {
+ return $value;
+ }
+
+ return array('string', '"', array($value));
+ }
+
+ protected static $libPercentage = array('value');
+ protected function libPercentage($args)
+ {
+ return array('number',
+ $this->coercePercent($args[0]) * 100,
+ '%');
+ }
+
+ protected static $libRound = array('value');
+ protected function libRound($args)
+ {
+ $num = $args[0];
+ $num[1] = round($num[1]);
+
+ return $num;
+ }
+
+ protected static $libFloor = array('value');
+ protected function libFloor($args)
+ {
+ $num = $args[0];
+ $num[1] = floor($num[1]);
+
+ return $num;
+ }
+
+ protected static $libCeil = array('value');
+ protected function libCeil($args)
+ {
+ $num = $args[0];
+ $num[1] = ceil($num[1]);
+
+ return $num;
+ }
+
+ protected static $libAbs = array('value');
+ protected function libAbs($args)
+ {
+ $num = $args[0];
+ $num[1] = abs($num[1]);
+
+ return $num;
+ }
+
+ protected function libMin($args)
+ {
+ $numbers = $this->getNormalizedNumbers($args);
+ $min = null;
+
+ foreach ($numbers as $key => $number) {
+ if (null === $min || $number[1] <= $min[1]) {
+ $min = array($key, $number[1]);
+ }
+ }
+
+ return $args[$min[0]];
+ }
+
+ protected function libMax($args)
+ {
+ $numbers = $this->getNormalizedNumbers($args);
+ $max = null;
+
+ foreach ($numbers as $key => $number) {
+ if (null === $max || $number[1] >= $max[1]) {
+ $max = array($key, $number[1]);
+ }
+ }
+
+ return $args[$max[0]];
+ }
+
+ /**
+ * Helper to normalize args containing numbers
+ *
+ * @param array $args
+ *
+ * @return array
+ */
+ protected function getNormalizedNumbers($args)
+ {
+ $unit = null;
+ $originalUnit = null;
+ $numbers = array();
+
+ foreach ($args as $key => $item) {
+ if ('number' !== $item[0]) {
+ $this->throwError('%s is not a number', $item[0]);
+ }
+
+ $number = $this->normalizeNumber($item);
+
+ if (null === $unit) {
+ $unit = $number[2];
+ $originalUnit = $item[2];
+ } elseif ($unit !== $number[2]) {
+ $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item[2]);
+ }
+
+ $numbers[$key] = $number;
+ }
+
+ return $numbers;
+ }
+
+ protected static $libLength = array('list');
+ protected function libLength($args)
+ {
+ $list = $this->coerceList($args[0]);
+
+ return count($list[2]);
+ }
+
+ // TODO: need a way to declare this built-in as varargs
+ //protected static $libListSeparator = array('list...');
+ protected function libListSeparator($args)
+ {
+ if (count($args) > 1) {
+ return 'comma';
+ }
+
+ $list = $this->coerceList($args[0]);
+
+ if (count($list[2]) <= 1) {
+ return 'space';
+ }
+
+ if ($list[1] === ',') {
+ return 'comma';
+ }
+
+ return 'space';
+ }
+
+ protected static $libNth = array('list', 'n');
+ protected function libNth($args)
+ {
+ $list = $this->coerceList($args[0]);
+ $n = $this->assertNumber($args[1]) - 1;
+
+ return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue;
+ }
+
+ protected static $libSetNth = array('list', 'n', 'value');
+ protected function libSetNth($args)
+ {
+ $list = $this->coerceList($args[0]);
+ $n = $this->assertNumber($args[1]) - 1;
+
+ if (! isset($list[2][$n])) {
+ $this->throwError('Invalid argument for "n"');
+ }
+
+ $list[2][$n] = $args[2];
+
+ return $list;
+ }
+
+ protected static $libMapGet = array('map', 'key');
+ protected function libMapGet($args)
+ {
+ $map = $this->assertMap($args[0]);
+
+ $key = $this->compileStringContent($this->coerceString($args[1]));
+
+ for ($i = count($map[1]) - 1; $i >= 0; $i--) {
+ if ($key === $this->compileValue($map[1][$i])) {
+ return $map[2][$i];
+ }
+ }
+
+ return self::$null;
+ }
+
+ protected static $libMapKeys = array('map');
+ protected function libMapKeys($args)
+ {
+ $map = $this->assertMap($args[0]);
+
+ $keys = $map[1];
+
+ return array('list', ',', $keys);
+ }
+
+ protected static $libMapValues = array('map');
+ protected function libMapValues($args)
+ {
+ $map = $this->assertMap($args[0]);
+
+ $values = $map[2];
+
+ return array('list', ',', $values);
+ }
+
+ protected static $libMapRemove = array('map', 'key');
+ protected function libMapRemove($args)
+ {
+ $map = $this->assertMap($args[0]);
+
+ $key = $this->compileStringContent($this->coerceString($args[1]));
+
+ for ($i = count($map[1]) - 1; $i >= 0; $i--) {
+ if ($key === $this->compileValue($map[1][$i])) {
+ array_splice($map[1], $i, 1);
+ array_splice($map[2], $i, 1);
+ }
+ }
+
+ return $map;
+ }
+
+ protected static $libMapHasKey = array('map', 'key');
+ protected function libMapHasKey($args)
+ {
+ $map = $this->assertMap($args[0]);
+
+ $key = $this->compileStringContent($this->coerceString($args[1]));
+
+ for ($i = count($map[1]) - 1; $i >= 0; $i--) {
+ if ($key === $this->compileValue($map[1][$i])) {
+ return self::$true;
+ }
+ }
+
+ return self::$false;
+ }
+
+ protected static $libMapMerge = array('map-1', 'map-2');
+ protected function libMapMerge($args)
+ {
+ $map1 = $this->assertMap($args[0]);
+ $map2 = $this->assertMap($args[1]);
+
+ return array('map', array_merge($map1[1], $map2[1]), array_merge($map1[2], $map2[2]));
+ }
+
+ protected function listSeparatorForJoin($list1, $sep)
+ {
+ if (! isset($sep)) {
+ return $list1[1];
+ }
+
+ switch ($this->compileValue($sep)) {
+ case 'comma':
+ return ',';
+
+ case 'space':
+ return '';
+
+ default:
+ return $list1[1];
+ }
+ }
+
+ protected static $libJoin = array('list1', 'list2', 'separator');
+ protected function libJoin($args)
+ {
+ list($list1, $list2, $sep) = $args;
+
+ $list1 = $this->coerceList($list1, ' ');
+ $list2 = $this->coerceList($list2, ' ');
+ $sep = $this->listSeparatorForJoin($list1, $sep);
+
+ return array('list', $sep, array_merge($list1[2], $list2[2]));
+ }
+
+ protected static $libAppend = array('list', 'val', 'separator');
+ protected function libAppend($args)
+ {
+ list($list1, $value, $sep) = $args;
+
+ $list1 = $this->coerceList($list1, ' ');
+ $sep = $this->listSeparatorForJoin($list1, $sep);
+
+ return array('list', $sep, array_merge($list1[2], array($value)));
+ }
+
+ protected function libZip($args)
+ {
+ foreach ($args as $arg) {
+ $this->assertList($arg);
+ }
+
+ $lists = array();
+ $firstList = array_shift($args);
+
+ foreach ($firstList[2] as $key => $item) {
+ $list = array('list', '', array($item));
+
+ foreach ($args as $arg) {
+ if (isset($arg[2][$key])) {
+ $list[2][] = $arg[2][$key];
+ } else {
+ break 2;
+ }
+ }
+ $lists[] = $list;
+ }
+
+ return array('list', ',', $lists);
+ }
+
+ protected static $libTypeOf = array('value');
+ protected function libTypeOf($args)
+ {
+ $value = $args[0];
+
+ switch ($value[0]) {
+ case 'keyword':
+ if ($value === self::$true || $value === self::$false) {
+ return 'bool';
+ }
+
+ if ($this->coerceColor($value)) {
+ return 'color';
+ }
+
+ // fall-thru
+ case 'function':
+ return 'string';
+
+ case 'list':
+ if (isset($value[3]) && $value[3]) {
+ return 'arglist';
+ }
+
+ // fall-thru
+ default:
+ return $value[0];
+ }
+ }
+
+ protected static $libUnit = array('number');
+ protected function libUnit($args)
+ {
+ $num = $args[0];
+
+ if ($num[0] === 'number') {
+ return array('string', '"', array($num[2]));
+ }
+
+ return '';
+ }
+
+ protected static $libUnitless = array('number');
+ protected function libUnitless($args)
+ {
+ $value = $args[0];
+
+ return $value[0] === 'number' && empty($value[2]);
+ }
+
+ protected static $libComparable = array('number-1', 'number-2');
+ protected function libComparable($args)
+ {
+ list($number1, $number2) = $args;
+
+ if (! isset($number1[0]) || $number1[0] !== 'number' || ! isset($number2[0]) || $number2[0] !== 'number') {
+ $this->throwError('Invalid argument(s) for "comparable"');
+ }
+
+ $number1 = $this->normalizeNumber($number1);
+ $number2 = $this->normalizeNumber($number2);
+
+ return $number1[2] === $number2[2] || $number1[2] === '' || $number2[2] === '';
+ }
+
+ protected static $libStrIndex = array('string', 'substring');
+ protected function libStrIndex($args)
+ {
+ $string = $this->coerceString($args[0]);
+ $stringContent = $this->compileStringContent($string);
+
+ $substring = $this->coerceString($args[1]);
+ $substringContent = $this->compileStringContent($substring);
+
+ $result = strpos($stringContent, $substringContent);
+
+ return $result === false ? self::$null : array('number', $result + 1, '');
+ }
+
+ protected static $libStrInsert = array('string', 'insert', 'index');
+ protected function libStrInsert($args)
+ {
+ $string = $this->coerceString($args[0]);
+ $stringContent = $this->compileStringContent($string);
+
+ $insert = $this->coerceString($args[1]);
+ $insertContent = $this->compileStringContent($insert);
+
+ list(, $index) = $args[2];
+
+ $string[2] = array(substr_replace($stringContent, $insertContent, $index - 1, 0));
+
+ return $string;
+ }
+
+ protected static $libStrLength = array('string');
+ protected function libStrLength($args)
+ {
+ $string = $this->coerceString($args[0]);
+ $stringContent = $this->compileStringContent($string);
+
+ return array('number', strlen($stringContent), '');
+ }
+
+ protected static $libStrSlice = array('string', 'start-at', 'end-at');
+ protected function libStrSlice($args)
+ {
+ if ($args[2][1] == 0) {
+ return self::$emptyString;
+ }
+
+ $string = $this->coerceString($args[0]);
+ $stringContent = $this->compileStringContent($string);
+
+ $start = (int) $args[1][1] ?: 1;
+ $end = (int) $args[2][1];
+
+ $string[2] = array(substr($stringContent, $start - 1, $end < 0 ? $end : $end - $start + 1));
+
+ return $string;
+ }
+
+ protected static $libToLowerCase = array('string');
+ protected function libToLowerCase($args)
+ {
+ $string = $this->coerceString($args[0]);
+ $stringContent = $this->compileStringContent($string);
+
+ $string[2] = array(mb_strtolower($stringContent));
+
+ return $string;
+ }
+
+ protected static $libToUpperCase = array('string');
+ protected function libToUpperCase($args)
+ {
+ $string = $this->coerceString($args[0]);
+ $stringContent = $this->compileStringContent($string);
+
+ $string[2] = array(mb_strtoupper($stringContent));
+
+ return $string;
+ }
+
+ protected static $libFeatureExists = array('feature');
+ protected function libFeatureExists($args)
+ {
+ /*
+ * The following features not not (yet) supported:
+ * - global-variable-shadowing
+ * - extend-selector-pseudoclass
+ * - units-level-3
+ * - at-error
+ */
+ return self::$false;
+ }
+
+ protected static $libFunctionExists = array('name');
+ protected function libFunctionExists($args)
+ {
+ $string = $this->coerceString($args[0]);
+ $name = $this->compileStringContent($string);
+
+ // user defined functions
+ if ($this->has(self::$namespaces['function'] . $name)) {
+ return self::$true;
+ }
+
+ $name = $this->normalizeName($name);
+
+ if (isset($this->userFunctions[$name])) {
+ return self::$true;
+ }
+
+ // built-in functions
+ $f = $this->getBuiltinFunction($name);
+
+ return $this->toBool(is_callable($f));
+ }
+
+ protected static $libGlobalVariableExists = array('name');
+ protected function libGlobalVariableExists($args)
+ {
+ $string = $this->coerceString($args[0]);
+ $name = $this->compileStringContent($string);
+
+ return $this->has($name, $this->rootEnv) ? self::$true : self::$false;
+ }
+
+ protected static $libMixinExists = array('name');
+ protected function libMixinExists($args)
+ {
+ $string = $this->coerceString($args[0]);
+ $name = $this->compileStringContent($string);
+
+ return $this->has(self::$namespaces['mixin'] . $name) ? self::$true : self::$false;
+ }
+
+ protected static $libVariableExists = array('name');
+ protected function libVariableExists($args)
+ {
+ $string = $this->coerceString($args[0]);
+ $name = $this->compileStringContent($string);
+
+ return $this->has($name) ? self::$true : self::$false;
+ }
+
+ /**
+ * Workaround IE7's content counter bug.
+ *
+ * @param array $args
+ */
+ protected function libCounter($args)
+ {
+ $list = array_map(array($this, 'compileValue'), $args);
+
+ return array('string', '', array('counter(' . implode(',', $list) . ')'));
+ }
+
+ protected function libRandom($args)
+ {
+ if (isset($args[0])) {
+ $n = $this->assertNumber($args[0]);
+
+ if ($n < 1) {
+ $this->throwError("limit must be greater than or equal to 1");
+ }
+
+ return array('number', mt_rand(1, $n), '');
+ }
+
+ return array('number', mt_rand(1, mt_getrandmax()), '');
+ }
+
+ protected function libUniqueId()
+ {
+ static $id;
+
+ if (! isset($id)) {
+ $id = mt_rand(0, pow(36, 8));
+ }
+
+ $id += mt_rand(0, 10) + 1;
+
+ return array('string', '', array('u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)));
+ }
+}
--- /dev/null
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp;
+
+/**
+ * SCSS base formatter
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+abstract class Formatter
+{
+ /**
+ * @var integer
+ */
+ public $indentLevel;
+
+ /**
+ * @var string
+ */
+ public $indentChar;
+
+ /**
+ * @var string
+ */
+ public $break;
+
+ /**
+ * @var string
+ */
+ public $open;
+
+ /**
+ * @var string
+ */
+ public $close;
+
+ /**
+ * @var string
+ */
+ public $tagSeparator;
+
+ /**
+ * @var string
+ */
+ public $assignSeparator;
+
+ abstract public function __construct();
+
+ /**
+ * Return indentation (whitespace)
+ *
+ * @param integer $n
+ *
+ * @return string
+ */
+ protected function indentStr($n = 0)
+ {
+ return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
+ }
+
+ /**
+ * Return property assignment
+ *
+ * @param string $name
+ * @param mixed $value
+ *
+ * @return string
+ */
+ public function property($name, $value)
+ {
+ return rtrim($name) . $this->assignSeparator . $value . ';';
+ }
+
+ /**
+ * Strip semi-colon appended by property(); it's a separator, not a terminator
+ *
+ * @param array $lines
+ */
+ public function stripSemicolon(&$lines)
+ {
+ }
+
+ /**
+ * Output lines inside a block
+ *
+ * @param string $inner
+ * @param \stdClass $block
+ */
+ protected function blockLines($inner, $block)
+ {
+ $glue = $this->break . $inner;
+ echo $inner . implode($glue, $block->lines);
+
+ if (! empty($block->children)) {
+ echo $this->break;
+ }
+ }
+
+ /**
+ * Output non-empty block
+ *
+ * @param \stdClass $block
+ */
+ protected function block($block)
+ {
+ if (empty($block->lines) && empty($block->children)) {
+ return;
+ }
+
+ $inner = $pre = $this->indentStr();
+
+ if (! empty($block->selectors)) {
+ echo $pre
+ . implode($this->tagSeparator, $block->selectors)
+ . $this->open . $this->break;
+
+ $this->indentLevel++;
+
+ $inner = $this->indentStr();
+ }
+
+ if (! empty($block->lines)) {
+ $this->blockLines($inner, $block);
+ }
+
+ foreach ($block->children as $child) {
+ $this->block($child);
+ }
+
+ if (! empty($block->selectors)) {
+ $this->indentLevel--;
+
+ if (empty($block->children)) {
+ echo $this->break;
+ }
+
+ echo $pre . $this->close . $this->break;
+ }
+ }
+
+ /**
+ * Entry point to formatting a block
+ *
+ * @param \stdClass $block An abstract syntax tree
+ *
+ * @return string
+ */
+ public function format($block)
+ {
+ ob_start();
+
+ $this->block($block);
+
+ $out = ob_get_clean();
+
+ return $out;
+ }
+}
--- /dev/null
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp\Formatter;
+
+use Leafo\ScssPhp\Formatter;
+
+/**
+ * SCSS compact formatter
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class Compact extends Formatter
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct()
+ {
+ $this->indentLevel = 0;
+ $this->indentChar = '';
+ $this->break = '';
+ $this->open = ' {';
+ $this->close = "}\n\n";
+ $this->tagSeparator = ',';
+ $this->assignSeparator = ':';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function indentStr($n = 0)
+ {
+ return ' ';
+ }
+}
--- /dev/null
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp\Formatter;
+
+use Leafo\ScssPhp\Formatter;
+
+/**
+ * SCSS compressed formatter
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class Compressed extends Formatter
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct()
+ {
+ $this->indentLevel = 0;
+ $this->indentChar = ' ';
+ $this->break = '';
+ $this->open = '{';
+ $this->close = '}';
+ $this->tagSeparator = ',';
+ $this->assignSeparator = ':';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function indentStr($n = 0)
+ {
+ return '';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function stripSemicolon(&$lines)
+ {
+ if (($count = count($lines))
+ && substr($lines[$count - 1], -1) === ';'
+ ) {
+ $lines[$count - 1] = substr($lines[$count - 1], 0, -1);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function blockLines($inner, $block)
+ {
+ $glue = $this->break . $inner;
+
+ foreach ($block->lines as $index => $line) {
+ if (substr($line, 0, 2) === '/*' && substr($line, 2, 1) !== '!') {
+ unset($block->lines[$index]);
+ } elseif (substr($line, 0, 3) === '/*!') {
+ $block->lines[$index] = '/*' . substr($line, 3);
+ }
+ }
+
+ echo $inner . implode($glue, $block->lines);
+
+ if (! empty($block->children)) {
+ echo $this->break;
+ }
+ }
+
+ /**
+ * {@inherit}
+ */
+ public function format($block)
+ {
+ return parent::format($block);
+
+ // TODO: we need to fix the 2 "compressed" tests where the "close" is applied
+ return trim(str_replace(';}', '}', parent::format($block)));
+ }
+}
--- /dev/null
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp\Formatter;
+
+use Leafo\ScssPhp\Formatter;
+
+/**
+ * SCSS crunched formatter
+ *
+ * @author Anthon Pang <anthon.pang@gmail.com>
+ */
+class Crunched extends Formatter
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct()
+ {
+ $this->indentLevel = 0;
+ $this->indentChar = ' ';
+ $this->break = '';
+ $this->open = '{';
+ $this->close = '}';
+ $this->tagSeparator = ',';
+ $this->assignSeparator = ':';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function indentStr($n = 0)
+ {
+ return '';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function stripSemicolon(&$lines)
+ {
+ if (($count = count($lines))
+ && substr($lines[$count - 1], -1) === ';'
+ ) {
+ $lines[$count - 1] = substr($lines[$count - 1], 0, -1);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function blockLines($inner, $block)
+ {
+ $glue = $this->break . $inner;
+
+ foreach ($block->lines as $index => $line) {
+ if (substr($line, 0, 2) === '/*') {
+ unset($block->lines[$index]);
+ }
+ }
+
+ echo $inner . implode($glue, $block->lines);
+
+ if (! empty($block->children)) {
+ echo $this->break;
+ }
+ }
+}
--- /dev/null
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp\Formatter;
+
+use Leafo\ScssPhp\Formatter;
+
+/**
+ * SCSS expanded formatter
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class Expanded extends Formatter
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct()
+ {
+ $this->indentLevel = 0;
+ $this->indentChar = ' ';
+ $this->break = "\n";
+ $this->open = ' {';
+ $this->close = '}';
+ $this->tagSeparator = ', ';
+ $this->assignSeparator = ': ';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function blockLines($inner, $block)
+ {
+ $glue = $this->break . $inner;
+
+ foreach ($block->lines as $index => $line) {
+ if (substr($line, 0, 2) === '/*') {
+ $block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line);
+ }
+ }
+
+ echo $inner . implode($glue, $block->lines);
+
+ if (empty($block->selectors) || ! empty($block->children)) {
+ echo $this->break;
+ }
+ }
+}
--- /dev/null
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp\Formatter;
+
+use Leafo\ScssPhp\Formatter;
+
+/**
+ * SCSS nested formatter
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class Nested extends Formatter
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct()
+ {
+ $this->indentLevel = 0;
+ $this->indentChar = ' ';
+ $this->break = "\n";
+ $this->open = ' {';
+ $this->close = ' }';
+ $this->tagSeparator = ', ';
+ $this->assignSeparator = ': ';
+ }
+
+ /**
+ * Adjust the depths of all children, depth first
+ *
+ * @param \stdClass $block
+ */
+ public function adjustAllChildren($block)
+ {
+ // flatten empty nested blocks
+ $children = array();
+ foreach ($block->children as $i => $child) {
+ if (empty($child->lines) && empty($child->children)) {
+ if (isset($block->children[$i + 1])) {
+ $block->children[$i + 1]->depth = $child->depth;
+ }
+ continue;
+ }
+ $children[] = $child;
+ }
+
+ $count = count($children);
+ for ($i = 0; $i < $count; $i++) {
+ $depth = $children[$i]->depth;
+ $j = $i + 1;
+ if (isset($children[$j]) && $depth < $children[$j]->depth) {
+ $childDepth = $children[$j]->depth;
+ for (; $j < $count; $j++) {
+ if ($depth < $children[$j]->depth && $childDepth >= $children[$j]->depth) {
+ $children[$j]->depth = $depth + 1;
+ }
+ }
+ }
+ }
+
+ $block->children = $children;
+
+ // make relative to parent
+ foreach ($block->children as $child) {
+ $this->adjustAllChildren($child);
+ $child->depth = $child->depth - $block->depth;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function blockLines($inner, $block)
+ {
+ $glue = $this->break . $inner;
+
+ foreach ($block->lines as $index => $line) {
+ if (substr($line, 0, 2) === '/*') {
+ $block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line);
+ }
+ }
+
+ echo $inner . implode($glue, $block->lines);
+
+ if (! empty($block->children)) {
+ echo $this->break;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function block($block)
+ {
+ if ($block->type === 'root') {
+ $this->adjustAllChildren($block);
+ }
+
+ $inner = $pre = $this->indentStr($block->depth - 1);
+ if (! empty($block->selectors)) {
+ echo $pre .
+ implode($this->tagSeparator, $block->selectors) .
+ $this->open . $this->break;
+ $this->indentLevel++;
+ $inner = $this->indentStr($block->depth - 1);
+ }
+
+ if (! empty($block->lines)) {
+ $this->blockLines($inner, $block);
+ }
+
+ foreach ($block->children as $i => $child) {
+ $this->block($child);
+
+ if ($i < count($block->children) - 1) {
+ echo $this->break;
+
+ if (isset($block->children[$i + 1])) {
+ $next = $block->children[$i + 1];
+ if ($next->depth === max($block->depth, 1) && $child->depth >= $next->depth) {
+ echo $this->break;
+ }
+ }
+ }
+ }
+
+ if (! empty($block->selectors)) {
+ $this->indentLevel--;
+ echo $this->close;
+ }
+
+ if ($block->type === 'root') {
+ echo $this->break;
+ }
+ }
+}
--- /dev/null
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp;
+
+use Leafo\ScssPhp\Compiler;
+
+/**
+ * SCSS parser
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class Parser
+{
+ const SOURCE_POSITION = -1;
+ const SOURCE_PARSER = -2;
+
+ /**
+ * @var array
+ */
+ protected static $precedence = array(
+ '=' => 0,
+ 'or' => 1,
+ 'and' => 2,
+ '==' => 3,
+ '!=' => 3,
+ '<=' => 4,
+ '>=' => 4,
+ '<' => 4,
+ '>' => 4,
+ '+' => 5,
+ '-' => 5,
+ '*' => 6,
+ '/' => 6,
+ '%' => 6,
+ );
+
+ /**
+ * @var array
+ */
+ protected static $operators = array(
+ '+',
+ '-',
+ '*',
+ '/',
+ '%',
+ '==',
+ '!=',
+ '<=',
+ '>=',
+ '<',
+ '>',
+ 'and',
+ 'or',
+ );
+
+ protected static $operatorStr;
+ protected static $whitePattern;
+ protected static $commentMulti;
+
+ protected static $commentSingle = '//';
+ protected static $commentMultiLeft = '/*';
+ protected static $commentMultiRight = '*/';
+
+ private $sourceName;
+ private $rootParser;
+ private $charset;
+ private $count;
+ private $env;
+ private $inParens;
+ private $eatWhiteDefault;
+ private $buffer;
+
+ /**
+ * Constructor
+ *
+ * @param string $sourceName
+ * @param boolean $rootParser
+ */
+ public function __construct($sourceName = null, $rootParser = true)
+ {
+ $this->sourceName = $sourceName;
+ $this->rootParser = $rootParser;
+ $this->charset = null;
+
+ if (empty(self::$operatorStr)) {
+ self::$operatorStr = $this->makeOperatorStr(self::$operators);
+
+ $commentSingle = $this->pregQuote(self::$commentSingle);
+ $commentMultiLeft = $this->pregQuote(self::$commentMultiLeft);
+ $commentMultiRight = $this->pregQuote(self::$commentMultiRight);
+ self::$commentMulti = $commentMultiLeft . '.*?' . $commentMultiRight;
+ self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentMulti . ')\s*|\s+/Ais';
+ }
+ }
+
+ /**
+ * Make operator regex
+ *
+ * @param array $operators
+ *
+ * @return string
+ */
+ protected static function makeOperatorStr($operators)
+ {
+ return '('
+ . implode('|', array_map(array('Leafo\ScssPhp\Parser', 'pregQuote'), $operators))
+ . ')';
+ }
+
+ /**
+ * Parser buffer
+ *
+ * @param string $buffer;
+ *
+ * @return \stdClass
+ */
+ public function parse($buffer)
+ {
+ $this->count = 0;
+ $this->env = null;
+ $this->inParens = false;
+ $this->eatWhiteDefault = true;
+ $this->buffer = $buffer;
+
+ $this->pushBlock(null); // root block
+
+ $this->whitespace();
+ $this->pushBlock(null);
+ $this->popBlock();
+
+ while (false !== $this->parseChunk()) {
+ ;
+ }
+
+ if ($this->count !== strlen($this->buffer)) {
+ $this->throwParseError();
+ }
+
+ if (! empty($this->env->parent)) {
+ $this->throwParseError('unclosed block');
+ }
+
+ if ($this->charset) {
+ array_unshift($this->env->children, $this->charset);
+ }
+
+ $this->env->isRoot = true;
+
+ return $this->env;
+ }
+
+ /**
+ * Parse a value or value list
+ *
+ * @param string $buffer
+ * @param string $out
+ *
+ * @return boolean
+ */
+ public function parseValue($buffer, &$out)
+ {
+ $this->count = 0;
+ $this->env = null;
+ $this->inParens = false;
+ $this->eatWhiteDefault = true;
+ $this->buffer = (string) $buffer;
+
+ return $this->valueList($out);
+ }
+
+ /**
+ * Parse a selector or selector list
+ *
+ * @param string $buffer
+ * @param string $out
+ *
+ * @return boolean
+ */
+ public function parseSelector($buffer, &$out)
+ {
+ $this->count = 0;
+ $this->env = null;
+ $this->inParens = false;
+ $this->eatWhiteDefault = true;
+ $this->buffer = (string) $buffer;
+
+ return $this->selectors($out);
+ }
+
+ /**
+ * Parse a single chunk off the head of the buffer and append it to the
+ * current parse environment.
+ *
+ * Returns false when the buffer is empty, or when there is an error.
+ *
+ * This function is called repeatedly until the entire document is
+ * parsed.
+ *
+ * This parser is most similar to a recursive descent parser. Single
+ * functions represent discrete grammatical rules for the language, and
+ * they are able to capture the text that represents those rules.
+ *
+ * Consider the function Compiler::keyword(). (All parse functions are
+ * structured the same.)
+ *
+ * The function takes a single reference argument. When calling the
+ * function it will attempt to match a keyword on the head of the buffer.
+ * If it is successful, it will place the keyword in the referenced
+ * argument, advance the position in the buffer, and return true. If it
+ * fails then it won't advance the buffer and it will return false.
+ *
+ * All of these parse functions are powered by Compiler::match(), which behaves
+ * the same way, but takes a literal regular expression. Sometimes it is
+ * more convenient to use match instead of creating a new function.
+ *
+ * Because of the format of the functions, to parse an entire string of
+ * grammatical rules, you can chain them together using &&.
+ *
+ * But, if some of the rules in the chain succeed before one fails, then
+ * the buffer position will be left at an invalid state. In order to
+ * avoid this, Compiler::seek() is used to remember and set buffer positions.
+ *
+ * Before parsing a chain, use $s = $this->seek() to remember the current
+ * position into $s. Then if a chain fails, use $this->seek($s) to
+ * go back where we started.
+ *
+ * @return boolean
+ */
+ protected function parseChunk()
+ {
+ $s = $this->seek();
+
+ // the directives
+ if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
+ if ($this->literal('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) {
+ $media = $this->pushSpecialBlock('media', $s);
+ $media->queryList = $mediaQueryList[2];
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ if ($this->literal('@mixin') &&
+ $this->keyword($mixinName) &&
+ ($this->argumentDef($args) || true) &&
+ $this->literal('{')
+ ) {
+ $mixin = $this->pushSpecialBlock('mixin', $s);
+ $mixin->name = $mixinName;
+ $mixin->args = $args;
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ if ($this->literal('@include') &&
+ $this->keyword($mixinName) &&
+ ($this->literal('(') &&
+ ($this->argValues($argValues) || true) &&
+ $this->literal(')') || true) &&
+ ($this->end() ||
+ $this->literal('{') && $hasBlock = true)
+ ) {
+ $child = array('include',
+ $mixinName, isset($argValues) ? $argValues : null, null);
+
+ if (! empty($hasBlock)) {
+ $include = $this->pushSpecialBlock('include', $s);
+ $include->child = $child;
+ } else {
+ $this->append($child, $s);
+ }
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ if ($this->literal('@import') &&
+ $this->valueList($importPath) &&
+ $this->end()
+ ) {
+ $this->append(array('import', $importPath), $s);
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ if ($this->literal('@import') &&
+ $this->url($importPath) &&
+ $this->end()
+ ) {
+ $this->append(array('import', $importPath), $s);
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ if ($this->literal('@extend') &&
+ $this->selectors($selector) &&
+ $this->end()
+ ) {
+ $this->append(array('extend', $selector), $s);
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ if ($this->literal('@function') &&
+ $this->keyword($fnName) &&
+ $this->argumentDef($args) &&
+ $this->literal('{')
+ ) {
+ $func = $this->pushSpecialBlock('function', $s);
+ $func->name = $fnName;
+ $func->args = $args;
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ if ($this->literal('@return') && $this->valueList($retVal) && $this->end()) {
+ $this->append(array('return', $retVal), $s);
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ if ($this->literal('@each') &&
+ $this->genericList($varNames, 'variable', ',', false) &&
+ $this->literal('in') &&
+ $this->valueList($list) &&
+ $this->literal('{')
+ ) {
+ $each = $this->pushSpecialBlock('each', $s);
+
+ foreach ($varNames[2] as $varName) {
+ $each->vars[] = $varName[1];
+ }
+
+ $each->list = $list;
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ if ($this->literal('@while') &&
+ $this->expression($cond) &&
+ $this->literal('{')
+ ) {
+ $while = $this->pushSpecialBlock('while', $s);
+ $while->cond = $cond;
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ if ($this->literal('@for') &&
+ $this->variable($varName) &&
+ $this->literal('from') &&
+ $this->expression($start) &&
+ ($this->literal('through') ||
+ ($forUntil = true && $this->literal('to'))) &&
+ $this->expression($end) &&
+ $this->literal('{')
+ ) {
+ $for = $this->pushSpecialBlock('for', $s);
+ $for->var = $varName[1];
+ $for->start = $start;
+ $for->end = $end;
+ $for->until = isset($forUntil);
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ if ($this->literal('@if') && $this->valueList($cond) && $this->literal('{')) {
+ $if = $this->pushSpecialBlock('if', $s);
+ $if->cond = $cond;
+ $if->cases = array();
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ if ($this->literal('@debug') &&
+ $this->valueList($value) &&
+ $this->end()
+ ) {
+ $this->append(array('debug', $value), $s);
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ if ($this->literal('@warn') &&
+ $this->valueList($value) &&
+ $this->end()
+ ) {
+ $this->append(array('warn', $value), $s);
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ if ($this->literal('@error') &&
+ $this->valueList($value) &&
+ $this->end()
+ ) {
+ $this->append(array('error', $value), $s);
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ if ($this->literal('@content') && $this->end()) {
+ $this->append(array('mixin_content'), $s);
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ $last = $this->last();
+
+ if (isset($last) && $last[0] === 'if') {
+ list(, $if) = $last;
+
+ if ($this->literal('@else')) {
+ if ($this->literal('{')) {
+ $else = $this->pushSpecialBlock('else', $s);
+ } elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) {
+ $else = $this->pushSpecialBlock('elseif', $s);
+ $else->cond = $cond;
+ }
+
+ if (isset($else)) {
+ $else->dontAppend = true;
+ $if->cases[] = $else;
+
+ return true;
+ }
+ }
+
+ $this->seek($s);
+ }
+
+ // only retain the first @charset directive encountered
+ if ($this->literal('@charset') &&
+ $this->valueList($charset) && $this->end()
+ ) {
+ if (! isset($this->charset)) {
+ $statement = array('charset', $charset);
+
+ $statement[self::SOURCE_POSITION] = $s;
+
+ if (! $this->rootParser) {
+ $statement[self::SOURCE_PARSER] = $this;
+ }
+
+ $this->charset = $statement;
+ }
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ // doesn't match built in directive, do generic one
+ if ($this->literal('@', false) && $this->keyword($dirName) &&
+ ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
+ $this->literal('{')
+ ) {
+ $directive = $this->pushSpecialBlock('directive', $s);
+ $directive->name = $dirName;
+
+ if (isset($dirValue)) {
+ $directive->value = $dirValue;
+ }
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ return false;
+ }
+
+ // property shortcut
+ // captures most properties before having to parse a selector
+ if ($this->keyword($name, false) &&
+ $this->literal(': ') &&
+ $this->valueList($value) &&
+ $this->end()
+ ) {
+ $name = array('string', '', array($name));
+ $this->append(array('assign', $name, $value), $s);
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ // variable assigns
+ if ($this->variable($name) &&
+ $this->literal(':') &&
+ $this->valueList($value) && $this->end()
+ ) {
+ // check for '!flag'
+ $assignmentFlag = $this->stripAssignmentFlag($value);
+ $this->append(array('assign', $name, $value, $assignmentFlag), $s);
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ // misc
+ if ($this->literal('-->')) {
+ return true;
+ }
+
+ // opening css block
+ if ($this->selectors($selectors) && $this->literal('{')) {
+ $b = $this->pushBlock($selectors, $s);
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ // property assign, or nested assign
+ if ($this->propertyName($name) && $this->literal(':')) {
+ $foundSomething = false;
+
+ if ($this->valueList($value)) {
+ $this->append(array('assign', $name, $value), $s);
+ $foundSomething = true;
+ }
+
+ if ($this->literal('{')) {
+ $propBlock = $this->pushSpecialBlock('nestedprop', $s);
+ $propBlock->prefix = $name;
+ $foundSomething = true;
+ } elseif ($foundSomething) {
+ $foundSomething = $this->end();
+ }
+
+ if ($foundSomething) {
+ return true;
+ }
+ }
+
+ $this->seek($s);
+
+ // closing a block
+ if ($this->literal('}')) {
+ $block = $this->popBlock();
+
+ if (isset($block->type) && $block->type === 'include') {
+ $include = $block->child;
+ unset($block->child);
+ $include[3] = $block;
+ $this->append($include, $s);
+ } elseif (empty($block->dontAppend)) {
+ $type = isset($block->type) ? $block->type : 'block';
+ $this->append(array($type, $block), $s);
+ }
+
+ return true;
+ }
+
+ // extra stuff
+ if ($this->literal(';') ||
+ $this->literal('<!--')
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Strip assignment flag from the list
+ *
+ * @param array $value
+ *
+ * @return string
+ */
+ protected function stripAssignmentFlag(&$value)
+ {
+ $token = &$value;
+
+ for ($token = &$value; $token[0] === 'list' && ($s = count($token[2])); $token = &$lastNode) {
+ $lastNode = &$token[2][$s - 1];
+
+ if ($lastNode[0] === 'keyword' && in_array($lastNode[1], array('!default', '!global'))) {
+ array_pop($token[2]);
+
+ $token = $this->flattenList($token);
+
+ return $lastNode[1];
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Match literal string
+ *
+ * @param string $what
+ * @param boolean $eatWhitespace
+ *
+ * @return boolean
+ */
+ protected function literal($what, $eatWhitespace = null)
+ {
+ if (! isset($eatWhitespace)) {
+ $eatWhitespace = $this->eatWhiteDefault;
+ }
+
+ // shortcut on single letter
+ if (! isset($what[1]) && isset($this->buffer[$this->count])) {
+ if ($this->buffer[$this->count] === $what) {
+ if (! $eatWhitespace) {
+ $this->count++;
+
+ return true;
+ }
+ // goes below...
+ } else {
+ return false;
+ }
+ }
+
+ return $this->match($this->pregQuote($what), $m, $eatWhitespace);
+ }
+
+ /**
+ * Push block onto parse tree
+ *
+ * @param array $selectors
+ * @param integer $pos
+ *
+ * @return \stdClass
+ */
+ protected function pushBlock($selectors, $pos = 0)
+ {
+ $b = new \stdClass;
+ $b->parent = $this->env; // not sure if we need this yet
+
+ $b->sourcePosition = $pos;
+ $b->sourceParser = $this;
+ $b->selectors = $selectors;
+ $b->comments = array();
+
+ if (! $this->env) {
+ $b->children = array();
+ } elseif (empty($this->env->children)) {
+ $this->env->children = $this->env->comments;
+ $b->children = array();
+ $this->env->comments = array();
+ } else {
+ $b->children = $this->env->comments;
+ $this->env->comments = array();
+ }
+
+ $this->env = $b;
+
+ return $b;
+ }
+
+ /**
+ * Push special (named) block onto parse tree
+ *
+ * @param string $type
+ * @param integer $pos
+ *
+ * @return \stdClass
+ */
+ protected function pushSpecialBlock($type, $pos)
+ {
+ $block = $this->pushBlock(null, $pos);
+ $block->type = $type;
+
+ return $block;
+ }
+
+ /**
+ * Pop scope and return last block
+ *
+ * @return \stdClass
+ *
+ * @throws \Exception
+ */
+ protected function popBlock()
+ {
+ $block = $this->env;
+
+ if (empty($block->parent)) {
+ $this->throwParseError('unexpected }');
+ }
+
+ $this->env = $block->parent;
+ unset($block->parent);
+
+ $comments = $block->comments;
+ if (count($comments)) {
+ $this->env->comments = $comments;
+ unset($block->comments);
+ }
+
+ return $block;
+ }
+
+ /**
+ * Append comment to current block
+ *
+ * @param array $comment
+ */
+ protected function appendComment($comment)
+ {
+ $comment[1] = substr(preg_replace(array('/^\s+/m', '/^(.)/m'), array('', ' \1'), $comment[1]), 1);
+
+ $this->env->comments[] = $comment;
+ }
+
+ /**
+ * Append statement to current block
+ *
+ * @param array $statement
+ * @param integer $pos
+ */
+ protected function append($statement, $pos = null)
+ {
+ if ($pos !== null) {
+ $statement[self::SOURCE_POSITION] = $pos;
+
+ if (! $this->rootParser) {
+ $statement[self::SOURCE_PARSER] = $this;
+ }
+ }
+
+ $this->env->children[] = $statement;
+
+ $comments = $this->env->comments;
+
+ if (count($comments)) {
+ $this->env->children = array_merge($this->env->children, $comments);
+ $this->env->comments = array();
+ }
+ }
+
+ /**
+ * Returns last child was appended
+ *
+ * @return array|null
+ */
+ protected function last()
+ {
+ $i = count($this->env->children) - 1;
+
+ if (isset($this->env->children[$i])) {
+ return $this->env->children[$i];
+ }
+ }
+
+ /**
+ * Parse media query list
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function mediaQueryList(&$out)
+ {
+ return $this->genericList($out, 'mediaQuery', ',', false);
+ }
+
+ /**
+ * Parse media query
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function mediaQuery(&$out)
+ {
+ $s = $this->seek();
+
+ $expressions = null;
+ $parts = array();
+
+ if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) &&
+ $this->mixedKeyword($mediaType)
+ ) {
+ $prop = array('mediaType');
+
+ if (isset($only)) {
+ $prop[] = array('keyword', 'only');
+ }
+
+ if (isset($not)) {
+ $prop[] = array('keyword', 'not');
+ }
+
+ $media = array('list', '', array());
+
+ foreach ((array)$mediaType as $type) {
+ if (is_array($type)) {
+ $media[2][] = $type;
+ } else {
+ $media[2][] = array('keyword', $type);
+ }
+ }
+
+ $prop[] = $media;
+ $parts[] = $prop;
+ }
+
+ if (empty($parts) || $this->literal('and')) {
+ $this->genericList($expressions, 'mediaExpression', 'and', false);
+
+ if (is_array($expressions)) {
+ $parts = array_merge($parts, $expressions[2]);
+ }
+ }
+
+ $out = $parts;
+
+ return true;
+ }
+
+ /**
+ * Parse media expression
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function mediaExpression(&$out)
+ {
+ $s = $this->seek();
+ $value = null;
+
+ if ($this->literal('(') &&
+ $this->expression($feature) &&
+ ($this->literal(':') && $this->expression($value) || true) &&
+ $this->literal(')')
+ ) {
+ $out = array('mediaExp', $feature);
+
+ if ($value) {
+ $out[] = $value;
+ }
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ return false;
+ }
+
+ /**
+ * Parse argument values
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function argValues(&$out)
+ {
+ if ($this->genericList($list, 'argValue', ',', false)) {
+ $out = $list[2];
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse argument value
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function argValue(&$out)
+ {
+ $s = $this->seek();
+
+ $keyword = null;
+
+ if (! $this->variable($keyword) || ! $this->literal(':')) {
+ $this->seek($s);
+ $keyword = null;
+ }
+
+ if ($this->genericList($value, 'expression')) {
+ $out = array($keyword, $value, false);
+ $s = $this->seek();
+
+ if ($this->literal('...')) {
+ $out[2] = true;
+ } else {
+ $this->seek($s);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse list
+ *
+ * @param string $out
+ *
+ * @return boolean
+ */
+ protected function valueList(&$out)
+ {
+ return $this->genericList($out, 'spaceList', ',');
+ }
+
+ /**
+ * Parse space list
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function spaceList(&$out)
+ {
+ return $this->genericList($out, 'expression');
+ }
+
+ /**
+ * Parse generic list
+ *
+ * @param array $out
+ * @param callable $parseItem
+ * @param string $delim
+ * @param boolean $flatten
+ *
+ * @return boolean
+ */
+ protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
+ {
+ $s = $this->seek();
+ $items = array();
+
+ while ($this->$parseItem($value)) {
+ $items[] = $value;
+
+ if ($delim) {
+ if (! $this->literal($delim)) {
+ break;
+ }
+ }
+ }
+
+ if (count($items) === 0) {
+ $this->seek($s);
+
+ return false;
+ }
+
+ if ($flatten && count($items) === 1) {
+ $out = $items[0];
+ } else {
+ $out = array('list', $delim, $items);
+ }
+
+ return true;
+ }
+
+ /**
+ * Parse expression
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function expression(&$out)
+ {
+ $s = $this->seek();
+
+ if ($this->literal('(')) {
+ if ($this->literal(')')) {
+ $out = array('list', '', array());
+
+ return true;
+ }
+
+ if ($this->valueList($out) && $this->literal(')') && $out[0] === 'list') {
+ return true;
+ }
+
+ $this->seek($s);
+
+ if ($this->map($out)) {
+ return true;
+ }
+
+ $this->seek($s);
+ }
+
+ if ($this->value($lhs)) {
+ $out = $this->expHelper($lhs, 0);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse left-hand side of subexpression
+ *
+ * @param array $lhs
+ * @param integer $minP
+ *
+ * @return array
+ */
+ protected function expHelper($lhs, $minP)
+ {
+ $opstr = self::$operatorStr;
+
+ $ss = $this->seek();
+ $whiteBefore = isset($this->buffer[$this->count - 1]) &&
+ ctype_space($this->buffer[$this->count - 1]);
+
+ while ($this->match($opstr, $m, false) && self::$precedence[$m[1]] >= $minP) {
+ $whiteAfter = isset($this->buffer[$this->count]) &&
+ ctype_space($this->buffer[$this->count]);
+ $varAfter = isset($this->buffer[$this->count]) &&
+ $this->buffer[$this->count] === '$';
+
+ $this->whitespace();
+
+ $op = $m[1];
+
+ // don't turn negative numbers into expressions
+ if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) {
+ break;
+ }
+
+ if (! $this->value($rhs)) {
+ break;
+ }
+
+ // peek and see if rhs belongs to next operator
+ if ($this->peek($opstr, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) {
+ $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
+ }
+
+ $lhs = array('exp', $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter);
+ $ss = $this->seek();
+ $whiteBefore = isset($this->buffer[$this->count - 1]) &&
+ ctype_space($this->buffer[$this->count - 1]);
+ }
+
+ $this->seek($ss);
+
+ return $lhs;
+ }
+
+ /**
+ * Parse value
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function value(&$out)
+ {
+ $s = $this->seek();
+
+ if ($this->literal('not', false) && $this->whitespace() && $this->value($inner)) {
+ $out = array('unary', 'not', $inner, $this->inParens);
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ if ($this->literal('+') && $this->value($inner)) {
+ $out = array('unary', '+', $inner, $this->inParens);
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ // negation
+ if ($this->literal('-', false) &&
+ ($this->variable($inner) ||
+ $this->unit($inner) ||
+ $this->parenValue($inner))
+ ) {
+ $out = array('unary', '-', $inner, $this->inParens);
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ if ($this->parenValue($out) ||
+ $this->interpolation($out) ||
+ $this->variable($out) ||
+ $this->color($out) ||
+ $this->unit($out) ||
+ $this->string($out) ||
+ $this->func($out) ||
+ $this->progid($out)
+ ) {
+ return true;
+ }
+
+ if ($this->keyword($keyword)) {
+ if ($keyword === 'null') {
+ $out = array('null');
+ } else {
+ $out = array('keyword', $keyword);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse parenthesized value
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function parenValue(&$out)
+ {
+ $s = $this->seek();
+
+ $inParens = $this->inParens;
+
+ if ($this->literal('(')) {
+ if ($this->literal(')')) {
+ $out = array('list', '', array());
+
+ return true;
+ }
+
+ $this->inParens = true;
+
+ if ($this->expression($exp) && $this->literal(')')) {
+ $out = $exp;
+ $this->inParens = $inParens;
+
+ return true;
+ }
+ }
+
+ $this->inParens = $inParens;
+ $this->seek($s);
+
+ return false;
+ }
+
+ /**
+ * Parse "progid:"
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function progid(&$out)
+ {
+ $s = $this->seek();
+
+ if ($this->literal('progid:', false) &&
+ $this->openString('(', $fn) &&
+ $this->literal('(')
+ ) {
+ $this->openString(')', $args, '(');
+
+ if ($this->literal(')')) {
+ $out = array('string', '', array(
+ 'progid:', $fn, '(', $args, ')'
+ ));
+
+ return true;
+ }
+ }
+
+ $this->seek($s);
+
+ return false;
+ }
+
+ /**
+ * Parse function call
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function func(&$func)
+ {
+ $s = $this->seek();
+
+ if ($this->keyword($name, false) &&
+ $this->literal('(')
+ ) {
+ if ($name === 'alpha' && $this->argumentList($args)) {
+ $func = array('function', $name, array('string', '', $args));
+
+ return true;
+ }
+
+ if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
+ $ss = $this->seek();
+
+ if ($this->argValues($args) && $this->literal(')')) {
+ $func = array('fncall', $name, $args);
+
+ return true;
+ }
+
+ $this->seek($ss);
+ }
+
+ if (($this->openString(')', $str, '(') || true ) &&
+ $this->literal(')')
+ ) {
+ $args = array();
+
+ if (! empty($str)) {
+ $args[] = array(null, array('string', '', array($str)));
+ }
+
+ $func = array('fncall', $name, $args);
+
+ return true;
+ }
+ }
+
+ $this->seek($s);
+
+ return false;
+ }
+
+ /**
+ * Parse function call argument list
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function argumentList(&$out)
+ {
+ $s = $this->seek();
+ $this->literal('(');
+
+ $args = array();
+
+ while ($this->keyword($var)) {
+ $ss = $this->seek();
+
+ if ($this->literal('=') && $this->expression($exp)) {
+ $args[] = array('string', '', array($var . '='));
+ $arg = $exp;
+ } else {
+ break;
+ }
+
+ $args[] = $arg;
+
+ if (! $this->literal(',')) {
+ break;
+ }
+
+ $args[] = array('string', '', array(', '));
+ }
+
+ if (! $this->literal(')') || ! count($args)) {
+ $this->seek($s);
+
+ return false;
+ }
+
+ $out = $args;
+
+ return true;
+ }
+
+ /**
+ * Parse mixin/function definition argument list
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function argumentDef(&$out)
+ {
+ $s = $this->seek();
+ $this->literal('(');
+
+ $args = array();
+
+ while ($this->variable($var)) {
+ $arg = array($var[1], null, false);
+
+ $ss = $this->seek();
+
+ if ($this->literal(':') && $this->genericList($defaultVal, 'expression')) {
+ $arg[1] = $defaultVal;
+ } else {
+ $this->seek($ss);
+ }
+
+ $ss = $this->seek();
+
+ if ($this->literal('...')) {
+ $sss = $this->seek();
+
+ if (! $this->literal(')')) {
+ $this->throwParseError('... has to be after the final argument');
+ }
+
+ $arg[2] = true;
+ $this->seek($sss);
+ } else {
+ $this->seek($ss);
+ }
+
+ $args[] = $arg;
+
+ if (! $this->literal(',')) {
+ break;
+ }
+ }
+
+ if (! $this->literal(')')) {
+ $this->seek($s);
+
+ return false;
+ }
+
+ $out = $args;
+
+ return true;
+ }
+
+ /**
+ * Parse map
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function map(&$out)
+ {
+ $s = $this->seek();
+
+ $this->literal('(');
+
+ $keys = array();
+ $values = array();
+
+ while ($this->genericList($key, 'expression') && $this->literal(':')
+ && $this->genericList($value, 'expression')
+ ) {
+ $keys[] = $key;
+ $values[] = $value;
+
+ if (! $this->literal(',')) {
+ break;
+ }
+ }
+
+ if (! count($keys) || ! $this->literal(')')) {
+ $this->seek($s);
+
+ return false;
+ }
+
+ $out = array('map', $keys, $values);
+
+ return true;
+ }
+
+ /**
+ * Parse color
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function color(&$out)
+ {
+ $color = array('color');
+
+ if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
+ if (isset($m[3])) {
+ $num = $m[3];
+ $width = 16;
+ } else {
+ $num = $m[2];
+ $width = 256;
+ }
+
+ $num = hexdec($num);
+
+ foreach (array(3, 2, 1) as $i) {
+ $t = $num % $width;
+ $num /= $width;
+
+ $color[$i] = $t * (256/$width) + $t * floor(16/$width);
+ }
+
+ $out = $color;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse number with unit
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function unit(&$unit)
+ {
+ if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
+ $unit = array('number', $m[1], empty($m[3]) ? '' : $m[3]);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse string
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function string(&$out)
+ {
+ $s = $this->seek();
+
+ if ($this->literal('"', false)) {
+ $delim = '"';
+ } elseif ($this->literal('\'', false)) {
+ $delim = '\'';
+ } else {
+ return false;
+ }
+
+ $content = array();
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ while ($this->matchString($m, $delim)) {
+ $content[] = $m[1];
+
+ if ($m[2] === '#{') {
+ $this->count -= strlen($m[2]);
+
+ if ($this->interpolation($inter, false)) {
+ $content[] = $inter;
+ } else {
+ $this->count += strlen($m[2]);
+ $content[] = '#{'; // ignore it
+ }
+ } elseif ($m[2] === '\\') {
+ $content[] = $m[2];
+
+ if ($this->literal($delim, false)) {
+ $content[] = $delim;
+ }
+ } else {
+ $this->count -= strlen($delim);
+ break; // delim
+ }
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+
+ if ($this->literal($delim)) {
+ $out = array('string', $delim, $content);
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ return false;
+ }
+
+ /**
+ * Parse keyword or interpolation
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function mixedKeyword(&$out)
+ {
+ $s = $this->seek();
+
+ $parts = array();
+
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ while (true) {
+ if ($this->keyword($key)) {
+ $parts[] = $key;
+ continue;
+ }
+
+ if ($this->interpolation($inter)) {
+ $parts[] = $inter;
+ continue;
+ }
+
+ break;
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+
+ if (count($parts) === 0) {
+ return false;
+ }
+
+ if ($this->eatWhiteDefault) {
+ $this->whitespace();
+ }
+
+ $out = $parts;
+
+ return true;
+ }
+
+ /**
+ * Parse an unbounded string stopped by $end
+ *
+ * @param string $end
+ * @param array $out
+ * @param string $nestingOpen
+ *
+ * @return boolean
+ */
+ protected function openString($end, &$out, $nestingOpen = null)
+ {
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ $stop = array('\'', '"', '#{', $end);
+ $stop = array_map(array($this, 'pregQuote'), $stop);
+ $stop[] = self::$commentMulti;
+
+ $patt = '(.*?)(' . implode('|', $stop) . ')';
+
+ $nestingLevel = 0;
+
+ $content = array();
+
+ while ($this->match($patt, $m, false)) {
+ if (isset($m[1]) && $m[1] !== '') {
+ $content[] = $m[1];
+
+ if ($nestingOpen) {
+ $nestingLevel += substr_count($m[1], $nestingOpen);
+ }
+ }
+
+ $tok = $m[2];
+
+ $this->count-= strlen($tok);
+
+ if ($tok === $end && ! $nestingLevel--) {
+ break;
+ }
+
+ if (($tok === '\'' || $tok === '"') && $this->string($str)) {
+ $content[] = $str;
+ continue;
+ }
+
+ if ($tok === '#{' && $this->interpolation($inter)) {
+ $content[] = $inter;
+ continue;
+ }
+
+ $content[] = $tok;
+ $this->count+= strlen($tok);
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+
+ if (count($content) === 0) {
+ return false;
+ }
+
+ // trim the end
+ if (is_string(end($content))) {
+ $content[count($content) - 1] = rtrim(end($content));
+ }
+
+ $out = array('string', '', $content);
+
+ return true;
+ }
+
+ /**
+ * Parser interpolation
+ *
+ * @param array $out
+ * @param boolean $lookWhite save information about whitespace before and after
+ *
+ * @return boolean
+ */
+ protected function interpolation(&$out, $lookWhite = true)
+ {
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = true;
+
+ $s = $this->seek();
+
+ if ($this->literal('#{') && $this->valueList($value) && $this->literal('}', false)) {
+ // TODO: don't error if out of bounds
+
+ if ($lookWhite) {
+ $left = preg_match('/\s/', $this->buffer[$s - 1]) ? ' ' : '';
+ $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
+ } else {
+ $left = $right = false;
+ }
+
+ $out = array('interpolate', $value, $left, $right);
+ $this->eatWhiteDefault = $oldWhite;
+
+ if ($this->eatWhiteDefault) {
+ $this->whitespace();
+ }
+ return true;
+ }
+
+ $this->seek($s);
+ $this->eatWhiteDefault = $oldWhite;
+ return false;
+ }
+
+ /**
+ * Parse property name (as an array of parts or a string)
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function propertyName(&$out)
+ {
+ $s = $this->seek();
+ $parts = array();
+
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ while (true) {
+ if ($this->interpolation($inter)) {
+ $parts[] = $inter;
+ } elseif ($this->keyword($text)) {
+ $parts[] = $text;
+ } elseif (count($parts) === 0 && $this->match('[:.#]', $m, false)) {
+ // css hacks
+ $parts[] = $m[0];
+ } else {
+ break;
+ }
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+
+ if (count($parts) === 0) {
+ return false;
+ }
+
+ // match comment hack
+ if (preg_match(
+ self::$whitePattern,
+ $this->buffer,
+ $m,
+ null,
+ $this->count
+ )) {
+ if (! empty($m[0])) {
+ $parts[] = $m[0];
+ $this->count += strlen($m[0]);
+ }
+ }
+
+ $this->whitespace(); // get any extra whitespace
+
+ $out = array('string', '', $parts);
+
+ return true;
+ }
+
+ /**
+ * Parse comma separated selector list
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function selectors(&$out)
+ {
+ $s = $this->seek();
+ $selectors = array();
+
+ while ($this->selector($sel)) {
+ $selectors[] = $sel;
+
+ if (! $this->literal(',')) {
+ break;
+ }
+
+ while ($this->literal(',')) {
+ ; // ignore extra
+ }
+ }
+
+ if (count($selectors) === 0) {
+ $this->seek($s);
+
+ return false;
+ }
+
+ $out = $selectors;
+
+ return true;
+ }
+
+ /**
+ * Parse whitespace separated selector list
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function selector(&$out)
+ {
+ $selector = array();
+
+ while (true) {
+ if ($this->match('[>+~]+', $m)) {
+ $selector[] = array($m[0]);
+ } elseif ($this->selectorSingle($part)) {
+ $selector[] = $part;
+ $this->match('\s+', $m);
+ } elseif ($this->match('\/[^\/]+\/', $m)) {
+ $selector[] = array($m[0]);
+ } else {
+ break;
+ }
+
+ }
+
+ if (count($selector) === 0) {
+ return false;
+ }
+
+ $out = $selector;
+ return true;
+ }
+
+ /**
+ * Parse the parts that make up a selector
+ *
+ * {@internal
+ * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
+ * }}
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function selectorSingle(&$out)
+ {
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ $parts = array();
+
+ if ($this->literal('*', false)) {
+ $parts[] = '*';
+ }
+
+ while (true) {
+ // see if we can stop early
+ if ($this->match('\s*[{,]', $m)) {
+ $this->count--;
+ break;
+ }
+
+ $s = $this->seek();
+
+ // self
+ if ($this->literal('&', false)) {
+ $parts[] = Compiler::$selfSelector;
+ continue;
+ }
+
+ if ($this->literal('.', false)) {
+ $parts[] = '.';
+ continue;
+ }
+
+ if ($this->literal('|', false)) {
+ $parts[] = '|';
+ continue;
+ }
+
+ if ($this->match('\\\\\S', $m)) {
+ $parts[] = $m[0];
+ continue;
+ }
+
+ // for keyframes
+ if ($this->unit($unit)) {
+ $parts[] = $unit;
+ continue;
+ }
+
+ if ($this->keyword($name)) {
+ $parts[] = $name;
+ continue;
+ }
+
+ if ($this->interpolation($inter)) {
+ $parts[] = $inter;
+ continue;
+ }
+
+ if ($this->literal('%', false) && $this->placeholder($placeholder)) {
+ $parts[] = '%';
+ $parts[] = $placeholder;
+ continue;
+ }
+
+ if ($this->literal('#', false)) {
+ $parts[] = '#';
+ continue;
+ }
+
+ // a pseudo selector
+ if ($this->match('::?', $m) && $this->mixedKeyword($nameParts)) {
+ $parts[] = $m[0];
+
+ foreach ($nameParts as $sub) {
+ $parts[] = $sub;
+ }
+
+ $ss = $this->seek();
+
+ if ($this->literal('(') &&
+ ($this->openString(')', $str, '(') || true ) &&
+ $this->literal(')')
+ ) {
+ $parts[] = '(';
+
+ if (! empty($str)) {
+ $parts[] = $str;
+ }
+
+ $parts[] = ')';
+ } else {
+ $this->seek($ss);
+ }
+
+ continue;
+ }
+
+ $this->seek($s);
+
+ // attribute selector
+ // TODO: replace with open string?
+ if ($this->literal('[', false)) {
+ $attrParts = array('[');
+
+ // keyword, string, operator
+ while (true) {
+ if ($this->literal(']', false)) {
+ $this->count--;
+ break; // get out early
+ }
+
+ if ($this->match('\s+', $m)) {
+ $attrParts[] = ' ';
+ continue;
+ }
+
+ if ($this->string($str)) {
+ $attrParts[] = $str;
+ continue;
+ }
+
+ if ($this->keyword($word)) {
+ $attrParts[] = $word;
+ continue;
+ }
+
+ if ($this->interpolation($inter, false)) {
+ $attrParts[] = $inter;
+ continue;
+ }
+
+ // operator, handles attr namespace too
+ if ($this->match('[|-~\$\*\^=]+', $m)) {
+ $attrParts[] = $m[0];
+ continue;
+ }
+
+ break;
+ }
+
+ if ($this->literal(']', false)) {
+ $attrParts[] = ']';
+
+ foreach ($attrParts as $part) {
+ $parts[] = $part;
+ }
+
+ continue;
+ }
+
+ $this->seek($s);
+ // TODO: should just break here?
+ }
+
+ break;
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+
+ if (count($parts) === 0) {
+ return false;
+ }
+
+ $out = $parts;
+
+ return true;
+ }
+
+ /**
+ * Parse a variable
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function variable(&$out)
+ {
+ $s = $this->seek();
+
+ if ($this->literal('$', false) && $this->keyword($name)) {
+ $out = array('var', $name);
+
+ return true;
+ }
+
+ $this->seek($s);
+
+ return false;
+ }
+
+ /**
+ * Parse a keyword
+ *
+ * @param string $word
+ * @param boolean $eatWhitespace
+ *
+ * @return boolean
+ */
+ protected function keyword(&$word, $eatWhitespace = null)
+ {
+ if ($this->match(
+ '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
+ $m,
+ $eatWhitespace
+ )) {
+ $word = $m[1];
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse a placeholder
+ *
+ * @param string $placeholder
+ *
+ * @return boolean
+ */
+ protected function placeholder(&$placeholder)
+ {
+ if ($this->match('([\w\-_]+|#[{][$][\w\-_]+[}])', $m)) {
+ $placeholder = $m[1];
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse a url
+ *
+ * @param array $out
+ *
+ * @return boolean
+ */
+ protected function url(&$out)
+ {
+ if ($this->match('(url\(\s*(["\']?)([^)]+)\2\s*\))', $m)) {
+ $out = array('string', '', array('url(' . $m[2] . $m[3] . $m[2] . ')'));
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Consume an end of statement delimiter
+ *
+ * @return boolean
+ */
+ protected function end()
+ {
+ if ($this->literal(';')) {
+ return true;
+ }
+
+ if ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
+ // if there is end of file or a closing block next then we don't need a ;
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @deprecated
+ *
+ * {@internal
+ * advance counter to next occurrence of $what
+ * $until - don't include $what in advance
+ * $allowNewline, if string, will be used as valid char set
+ * }}
+ */
+ protected function to($what, &$out, $until = false, $allowNewline = false)
+ {
+ if (is_string($allowNewline)) {
+ $validChars = $allowNewline;
+ } else {
+ $validChars = $allowNewline ? '.' : "[^\n]";
+ }
+
+ if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) {
+ return false;
+ }
+
+ if ($until) {
+ $this->count -= strlen($what); // give back $what
+ }
+
+ $out = $m[1];
+
+ return true;
+ }
+
+ /**
+ * Throw parser error
+ *
+ * @param string $msg
+ * @param integer $count
+ *
+ * @throws \Exception
+ */
+ public function throwParseError($msg = 'parse error', $count = null)
+ {
+ $count = ! isset($count) ? $this->count : $count;
+
+ $line = $this->getLineNo($count);
+
+ if (! empty($this->sourceName)) {
+ $loc = "$this->sourceName on line $line";
+ } else {
+ $loc = "line: $line";
+ }
+
+ if ($this->peek("(.*?)(\n|$)", $m, $count)) {
+ throw new \Exception("$msg: failed at `$m[1]` $loc");
+ }
+
+ throw new \Exception("$msg: $loc");
+ }
+
+ /**
+ * Get source file name
+ *
+ * @return string
+ */
+ public function getSourceName()
+ {
+ return $this->sourceName;
+ }
+
+ /**
+ * Get source line number (given character position in the buffer)
+ *
+ * @param integer $pos
+ *
+ * @return integer
+ */
+ public function getLineNo($pos)
+ {
+ return 1 + substr_count(substr($this->buffer, 0, $pos), "\n");
+ }
+
+ /**
+ * Match string looking for either ending delim, escape, or string interpolation
+ *
+ * {@internal This is a workaround for preg_match's 250K string match limit. }}
+ *
+ * @param array $m Matches (passed by reference)
+ * @param string $delim Delimeter
+ *
+ * @return boolean True if match; false otherwise
+ */
+ protected function matchString(&$m, $delim)
+ {
+ $token = null;
+
+ $end = strlen($this->buffer);
+
+ // look for either ending delim, escape, or string interpolation
+ foreach (array('#{', '\\', $delim) as $lookahead) {
+ $pos = strpos($this->buffer, $lookahead, $this->count);
+
+ if ($pos !== false && $pos < $end) {
+ $end = $pos;
+ $token = $lookahead;
+ }
+ }
+
+ if (! isset($token)) {
+ return false;
+ }
+
+ $match = substr($this->buffer, $this->count, $end - $this->count);
+ $m = array(
+ $match . $token,
+ $match,
+ $token
+ );
+ $this->count = $end + strlen($token);
+
+ return true;
+ }
+
+ /**
+ * Try to match something on head of buffer
+ *
+ * @param string $regex
+ * @param array $out
+ * @param boolean $eatWhitespace
+ *
+ * @return boolean
+ */
+ protected function match($regex, &$out, $eatWhitespace = null)
+ {
+ if (! isset($eatWhitespace)) {
+ $eatWhitespace = $this->eatWhiteDefault;
+ }
+
+ $r = '/' . $regex . '/Ais';
+
+ if (preg_match($r, $this->buffer, $out, null, $this->count)) {
+ $this->count += strlen($out[0]);
+
+ if ($eatWhitespace) {
+ $this->whitespace();
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Match some whitespace
+ *
+ * @return boolean
+ */
+ protected function whitespace()
+ {
+ $gotWhite = false;
+
+ while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
+ if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
+ $this->appendComment(array('comment', $m[1]));
+
+ $this->commentsSeen[$this->count] = true;
+ }
+
+ $this->count += strlen($m[0]);
+ $gotWhite = true;
+ }
+
+ return $gotWhite;
+ }
+
+ /**
+ * Peek input stream
+ *
+ * @param string $regex
+ * @param array $out
+ * @param integer $from
+ *
+ * @return integer
+ */
+ protected function peek($regex, &$out, $from = null)
+ {
+ if (! isset($from)) {
+ $from = $this->count;
+ }
+
+ $r = '/' . $regex . '/Ais';
+ $result = preg_match($r, $this->buffer, $out, null, $from);
+
+ return $result;
+ }
+
+ /**
+ * Seek to position in input stream (or return current position in input stream)
+ *
+ * @param integer $where
+ *
+ * @return integer
+ */
+ protected function seek($where = null)
+ {
+ if ($where === null) {
+ return $this->count;
+ }
+
+ $this->count = $where;
+
+ return true;
+ }
+
+ /**
+ * Quote regular expression
+ *
+ * @param string $what
+ *
+ * @return string
+ */
+ public static function pregQuote($what)
+ {
+ return preg_quote($what, '/');
+ }
+
+ /**
+ * @deprecated
+ */
+ protected function show()
+ {
+ if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
+ return $m[1];
+ }
+
+ return '';
+ }
+
+ /**
+ * Turn list of length 1 into value type
+ *
+ * @param array $value
+ *
+ * @return array
+ */
+ protected function flattenList($value)
+ {
+ if ($value[0] === 'list' && count($value[2]) === 1) {
+ return $this->flattenList($value[2][0]);
+ }
+
+ return $value;
+ }
+}
--- /dev/null
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp;
+
+use Leafo\ScssPhp\Compiler;
+use Leafo\ScssPhp\Version;
+
+/**
+ * SCSS server
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class Server
+{
+ /**
+ * @var boolean
+ */
+ private $showErrorsAsCSS;
+
+ /**
+ * @var string
+ */
+ private $dir;
+
+ /**
+ * @var string
+ */
+ private $cacheDir;
+
+ /**
+ * @var \Leafo\ScssPhp\Compiler
+ */
+ private $scss;
+
+ /**
+ * Join path components
+ *
+ * @param string $left Path component, left of the directory separator
+ * @param string $right Path component, right of the directory separator
+ *
+ * @return string
+ */
+ protected function join($left, $right)
+ {
+ return rtrim($left, '/\\') . DIRECTORY_SEPARATOR . ltrim($right, '/\\');
+ }
+
+ /**
+ * Get name of requested .scss file
+ *
+ * @return string|null
+ */
+ protected function inputName()
+ {
+ switch (true) {
+ case isset($_GET['p']):
+ return $_GET['p'];
+ case isset($_SERVER['PATH_INFO']):
+ return $_SERVER['PATH_INFO'];
+ case isset($_SERVER['DOCUMENT_URI']):
+ return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME']));
+ }
+ }
+
+ /**
+ * Get path to requested .scss file
+ *
+ * @return string
+ */
+ protected function findInput()
+ {
+ if (($input = $this->inputName())
+ && strpos($input, '..') === false
+ && substr($input, -5) === '.scss'
+ ) {
+ $name = $this->join($this->dir, $input);
+
+ if (is_file($name) && is_readable($name)) {
+ return $name;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get path to cached .css file
+ *
+ * @return string
+ */
+ protected function cacheName($fname)
+ {
+ return $this->join($this->cacheDir, md5($fname) . '.css');
+ }
+
+ /**
+ * Get path to meta data
+ *
+ * @return string
+ */
+ protected function metadataName($out)
+ {
+ return $out . '.meta';
+ }
+
+ /**
+ * Determine whether .scss file needs to be re-compiled.
+ *
+ * @param string $in Input path
+ * @param string $out Output path
+ * @param string $etag ETag
+ *
+ * @return boolean True if compile required.
+ */
+ protected function needsCompile($in, $out, &$etag)
+ {
+ if (! is_file($out)) {
+ return true;
+ }
+
+ $mtime = filemtime($out);
+
+ if (filemtime($in) > $mtime) {
+ return true;
+ }
+
+ $metadataName = $this->metadataName($out);
+
+ if (is_readable($metadataName)) {
+ $metadata = unserialize(file_get_contents($metadataName));
+
+ foreach ($metadata['imports'] as $import => $importMtime) {
+ if ($importMtime > $mtime) {
+ return true;
+ }
+ }
+
+ $etag = $metadata['etag'];
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get If-Modified-Since header from client request
+ *
+ * @return string|null
+ */
+ protected function getIfModifiedSinceHeader()
+ {
+ $modifiedSince = null;
+
+ if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
+ $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
+
+ if (false !== ($semicolonPos = strpos($modifiedSince, ';'))) {
+ $modifiedSince = substr($modifiedSince, 0, $semicolonPos);
+ }
+ }
+
+ return $modifiedSince;
+ }
+
+ /**
+ * Get If-None-Match header from client request
+ *
+ * @return string|null
+ */
+ protected function getIfNoneMatchHeader()
+ {
+ $noneMatch = null;
+
+ if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
+ $noneMatch = $_SERVER['HTTP_IF_NONE_MATCH'];
+ }
+
+ return $noneMatch;
+ }
+
+ /**
+ * Compile .scss file
+ *
+ * @param string $in Input path (.scss)
+ * @param string $out Output path (.css)
+ *
+ * @return array
+ */
+ protected function compile($in, $out)
+ {
+ $start = microtime(true);
+ $css = $this->scss->compile(file_get_contents($in), $in);
+ $elapsed = round((microtime(true) - $start), 4);
+
+ $v = Version::VERSION;
+ $t = @date('r');
+ $css = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css;
+ $etag = md5($css);
+
+ file_put_contents($out, $css);
+ file_put_contents(
+ $this->metadataName($out),
+ serialize(array(
+ 'etag' => $etag,
+ 'imports' => $this->scss->getParsedFiles(),
+ ))
+ );
+
+ return array($css, $etag);
+ }
+
+ /**
+ * Format error as a pseudo-element in CSS
+ *
+ * @param \Exception $error
+ *
+ * @return string
+ */
+ protected function createErrorCSS($error)
+ {
+ $message = str_replace(
+ array("'", "\n"),
+ array("\\'", "\\A"),
+ $error->getfile() . ":\n\n" . $error->getMessage()
+ );
+
+ return "body { display: none !important; }
+ html:after {
+ background: white;
+ color: black;
+ content: '$message';
+ display: block !important;
+ font-family: mono;
+ padding: 1em;
+ white-space: pre;
+ }";
+ }
+
+ /**
+ * Render errors as a pseudo-element within valid CSS, displaying the errors on any
+ * page that includes this CSS.
+ *
+ * @param boolean $show
+ */
+ public function showErrorsAsCSS($show = true)
+ {
+ $this->showErrorsAsCSS = $show;
+ }
+
+ /**
+ * Compile .scss file
+ *
+ * @param string $in Input file (.scss)
+ * @param string $out Output file (.css) optional
+ *
+ * @return string|bool
+ */
+ public function compileFile($in, $out = null)
+ {
+ if (! is_readable($in)) {
+ throw new \Exception('load error: failed to find ' . $in);
+ }
+
+ $pi = pathinfo($in);
+
+ $this->scss->addImportPath($pi['dirname'] . '/');
+
+ $compiled = $this->scss->compile(file_get_contents($in), $in);
+
+ if ($out !== null) {
+ return file_put_contents($out, $compiled);
+ }
+
+ return $compiled;
+ }
+
+ /**
+ * Check if file need compiling
+ *
+ * @param string $in Input file (.scss)
+ * @param string $out Output file (.css)
+ *
+ * @return bool
+ */
+ public function checkedCompile($in, $out)
+ {
+ if (! is_file($out) || filemtime($in) > filemtime($out)) {
+ $this->compileFile($in, $out);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Compile requested scss and serve css. Outputs HTTP response.
+ *
+ * @param string $salt Prefix a string to the filename for creating the cache name hash
+ */
+ public function serve($salt = '')
+ {
+ $protocol = isset($_SERVER['SERVER_PROTOCOL'])
+ ? $_SERVER['SERVER_PROTOCOL']
+ : 'HTTP/1.0';
+
+ if ($input = $this->findInput()) {
+ $output = $this->cacheName($salt . $input);
+ $etag = $noneMatch = trim($this->getIfNoneMatchHeader(), '"');
+
+ if ($this->needsCompile($input, $output, $etag)) {
+ try {
+ list($css, $etag) = $this->compile($input, $output);
+
+ $lastModified = gmdate('D, d M Y H:i:s', filemtime($output)) . ' GMT';
+
+ header('Last-Modified: ' . $lastModified);
+ header('Content-type: text/css');
+ header('ETag: "' . $etag . '"');
+
+ echo $css;
+
+ } catch (\Exception $e) {
+ if ($this->showErrorsAsCSS) {
+ header('Content-type: text/css');
+
+ echo $this->createErrorCSS($e);
+ } else {
+ header($protocol . ' 500 Internal Server Error');
+ header('Content-type: text/plain');
+
+ echo 'Parse error: ' . $e->getMessage() . "\n";
+ }
+
+ }
+
+ return;
+ }
+
+ header('X-SCSS-Cache: true');
+ header('Content-type: text/css');
+ header('ETag: "' . $etag . '"');
+
+ if ($etag === $noneMatch) {
+ header($protocol . ' 304 Not Modified');
+
+ return;
+ }
+
+ $modifiedSince = $this->getIfModifiedSinceHeader();
+ $mtime = filemtime($output);
+
+ if (@strtotime($modifiedSince) === $mtime) {
+ header($protocol . ' 304 Not Modified');
+
+ return;
+ }
+
+ $lastModified = gmdate('D, d M Y H:i:s', $mtime) . ' GMT';
+ header('Last-Modified: ' . $lastModified);
+
+ echo file_get_contents($output);
+
+ return;
+ }
+
+ header($protocol . ' 404 Not Found');
+ header('Content-type: text/plain');
+
+ $v = Version::VERSION;
+ echo "/* INPUT NOT FOUND scss $v */\n";
+ }
+
+ /**
+ * Based on explicit input/output files does a full change check on cache before compiling.
+ *
+ * @param string $in
+ * @param string $out
+ * @param boolean $force
+ *
+ * @return string Compiled CSS results
+ *
+ * @throws \Exception
+ */
+ public function checkedCachedCompile($in, $out, $force = false)
+ {
+ if (! is_file($in) || ! is_readable($in)) {
+ throw new \Exception('Invalid or unreadable input file specified.');
+ }
+
+ if (is_dir($out) || ! is_writable(file_exists($out) ? $out : dirname($out))) {
+ throw new \Exception('Invalid or unwritable output file specified.');
+ }
+
+ if ($force || $this->needsCompile($in, $out, $etag)) {
+ list($css, $etag) = $this->compile($in, $out);
+ } else {
+ $css = file_get_contents($out);
+ }
+
+ return $css;
+ }
+
+ /**
+ * Constructor
+ *
+ * @param string $dir Root directory to .scss files
+ * @param string $cacheDir Cache directory
+ * @param \Leafo\ScssPhp\Compiler|null $scss SCSS compiler instance
+ */
+ public function __construct($dir, $cacheDir = null, $scss = null)
+ {
+ $this->dir = $dir;
+
+ if (! isset($cacheDir)) {
+ $cacheDir = $this->join($dir, 'scss_cache');
+ }
+
+ $this->cacheDir = $cacheDir;
+
+ if (! is_dir($this->cacheDir)) {
+ mkdir($this->cacheDir, 0755, true);
+ }
+
+ if (! isset($scss)) {
+ $scss = new Compiler();
+ $scss->setImportPaths($this->dir);
+ }
+
+ $this->scss = $scss;
+ $this->showErrorsAsCSS = false;
+ }
+
+ /**
+ * Helper method to serve compiled scss
+ *
+ * @param string $path Root path
+ */
+ public static function serveFrom($path)
+ {
+ $server = new self($path);
+ $server->serve();
+ }
+}
--- /dev/null
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp;
+
+use Leafo\ScssPhp\Base\Range;
+
+/**
+ * SCSS utilties
+ *
+ * @author Anthon Pang <anthon.pang@gmail.com>
+ */
+class Util
+{
+ /**
+ * Asserts that `value` falls within `range` (inclusive), leaving
+ * room for slight floating-point errors.
+ *
+ * @param string $name The name of the value. Used in the error message.
+ * @param Range $range Range of values.
+ * @param array $value The value to check.
+ * @param string $unit The unit of the value. Used in error reporting.
+ *
+ * @return mixed `value` adjusted to fall within range, if it was outside by a floating-point margin.
+ *
+ * @throws \Exception
+ */
+ public static function checkRange($name, Range $range, $value, $unit = '')
+ {
+ $val = $value[1];
+ $grace = new Range(-0.00001, 0.00001);
+
+ if ($range->includes($val)) {
+ return $val;
+ }
+
+ if ($grace->includes($val - $range->first)) {
+ return $range->first;
+ }
+
+ if ($grace->includes($val - $range->last)) {
+ return $range->last;
+ }
+
+ throw new \Exception("$name {$val} must be between {$range->first} and {$range->last}$unit");
+ }
+}
--- /dev/null
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp;
+
+/**
+ * SCSSPHP version
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class Version
+{
+ const VERSION = 'v0.3.0';
+}
+++ /dev/null
-/* shortcuts */
-// clearing floats like a boss h5bp.com/q
-.clearfix {
- &::before,
- &::after {
- display: table;
- content: "";
- }
-
- &::after {
- clear: both;
- }
-}
-
-.square(@size) {
- height: @size;
- width: @size;
-}
-
-
-// sets default text shadows depending on background color
-.textShadow(@backgroundColor) when (lightness(@backgroundColor) >= 40%) {
- text-shadow: 0 1px 0 @wcfTextShadowLightColor;
-}
-.textShadow(@backgroundColor) when (lightness(@backgroundColor) < 60%) {
- text-shadow: 0 -1px 0 @wcfTextShadowDarkColor;
-}
-
-/* CSS 3 */
-.linearGradient(@backgroundColor, @gradientColor1, @gradientColor2) {
- background-color: @backgroundColor;
- background-image: -webkit-linear-gradient(@gradientColor1, @gradientColor2);
- background-image: linear-gradient(@gradientColor1, @gradientColor2);
-}
-
-.linearGradient(@backgroundColor, @gradientColor1, @gradientColor2, @gradientColor3, @direction: 180deg) {
- @oldDirection: @direction - 90deg;
- background-color: @backgroundColor;
- background-image: -webkit-linear-gradient(@oldDirection, @gradientColor1, @gradientColor2, @gradientColor3);
- background-image: linear-gradient(@direction, @gradientColor1, @gradientColor2, @gradientColor3);
-}
-
-.linearGradientNative(@parameters) {
- background-image: -webkit-linear-gradient(@parameters);
- background-image: linear-gradient(@parameters);
-}
-
-.transition(@property, @duration, @type: linear) {
- -webkit-transition-property: @property;
- transition-property: @property;
-
- -webkit-transition-duration: @duration;
- transition-duration: @duration;
-
- -webkit-transition-timing-function: @type;
- transition-timing-function: @type;
-}
-
-.boxShadow(@leftOffset, @topOffset, @color, @blurriness: 5px, @shadowHeight: 0) {
- box-shadow: @leftOffset @topOffset @blurriness @shadowHeight @color;
-}
-
-.boxShadowInset(@leftOffset, @topOffset, @color, @blurriness: 5px, @shadowHeight: 0) {
- box-shadow: inset @leftOffset @topOffset @blurriness @shadowHeight @color;
-}
-
-.boxShadowNative(@parameters) {
- box-shadow: @parameters;
-}
-
-.userSelectNone {
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
--- /dev/null
+/* shortcuts */
+// clearing floats like a boss h5bp.com/q
+.clearfix {
+ &::before,
+ &::after {
+ display: table;
+ content: "";
+ }
+
+ &::after {
+ clear: both;
+ }
+}
+
+.userSelectNone {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+@mixin square($size) {
+ height: $size;
+ width: $size;
+}
+
+// sets default text shadows depending on background color
+@mixin textShadow($backgroundColor) {
+ @if (lightness($backgroundColor) >= 40) {
+ text-shadow: 0 1px 0 $wcfTextShadowLightColor;
+ }
+ @else {
+ text-shadow: 0 -1px 0 $wcfTextShadowDarkColor;
+ }
+}
+
+/** @deprecated 2.2 - please use the native properties directly */
+@mixin linearGradient($backgroundColor, $gradientColor1, $gradientColor2) {
+ background-image: linear-gradient($gradientColor1, $gradientColor2);
+}
+@mixin linearGradient($backgroundColor, $gradientColor1, $gradientColor2, $gradientColor3) {
+ background-image: linear-gradient($direction, $gradientColor1, $gradientColor2, $gradientColor3);
+}
+@mixin linearGradientNative($parameters) {
+ background-image: linear-gradient($parameters);
+}
+@mixin transition($property, $duration, $type:linear) {
+ transition: $property $duration $type;
+}
+@mixin boxShadow($leftOffset, $topOffset, $color, $blurriness: 5px, $shadowHeight: 0) {
+ box-shadow: $leftOffset $topOffset $blurriness $shadowHeight $color;
+}
+@mixin boxShadowInset($leftOffset, $topOffset, $color, $blurriness: 5px, $shadowHeight: 0) {
+ box-shadow: inset $leftOffset $topOffset $blurriness $shadowHeight $color;
+}
+@mixin boxShadowNative($parameters) {
+ box-shadow: $parameters;
+}
+/** /deprecated */
+++ /dev/null
-/**
- * Parts taken from
- * http://meyerweb.com/eric/tools/css/reset/
- * v2.0 | 20110126
- * License: none (public domain)
- * modifyed to meet the needs of WoltLab
- */
-
-html, body, applet, object, iframe,
-h1, h2, h3, h4, h5, h6, p, blockquote, pre,
-address, big, cite, code, q,
-dl, dt, dd, ol, ul, li,
-fieldset, form, label, legend,
-table, caption, tbody, tfoot, thead, tr, th, td,
-canvas, embed,
-figure, figcaption,
-audio, video {
- margin: 0;
- padding: 0;
- border: 0;
-
-}
-
-img {
- border: 0;
-}
-
-h1, h2, h3, h4, h5, h6 {
- font-weight: normal;
- font-size: 100%;
-}
-
-ol, ul {
- list-style: none;
-}
-
-blockquote, q {
- quotes: none;
-}
-
-blockquote::before,
-blockquote::after,
-q::before,
-q::after {
- content: '';
- content: none;
-}
\ No newline at end of file
--- /dev/null
+/**
+ * Parts taken from
+ * http://meyerweb.com/eric/tools/css/reset/
+ * v2.0 | 20110126
+ * License: none (public domain)
+ * modifyed to meet the needs of WoltLab
+ */
+
+html, body, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+address, big, cite, code, q,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+canvas, embed,
+figure, figcaption,
+audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+
+}
+
+img {
+ border: 0;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-weight: normal;
+ font-size: 100%;
+}
+
+ol, ul {
+ list-style: none;
+}
+
+blockquote, q {
+ quotes: none;
+}
+
+blockquote::before,
+blockquote::after,
+q::before,
+q::after {
+ content: '';
+ content: none;
+}
\ No newline at end of file
+++ /dev/null
-.inlineList {
- display: flex;
- flex-wrap: wrap;
-
- > li {
- flex: 0 auto;
-
- &:not(:last-child) {
- margin-right: 5px;
- }
-
- > a {
- cursor: pointer;
- display: flex;
- }
- }
-}
-
-.nativeList {
- margin: 1em 0 1em 40px;
-
- li {
- margin: 7px 0;
- }
-}
-
-ol.dataList,
-ul.dataList {
- .inlineList;
-
- font-size: .85rem;
-
- > li {
- &:not(:last-child):after {
- content: ",";
- padding-left: 1px;
- }
- }
-}
-
-.framedIconList {
- display: flex;
- flex-wrap: wrap;
- padding-top: 10px;
-
- > li {
- flex: 0 0 25%;
- text-align: center;
-
- > a {
- display: block;
- }
- }
-
- & + .more {
- float: right;
- margin-top: 10px;
- }
-}
-
-.tagList {
- .inlineList;
-
- align-items: baseline;
-}
-
-.smileyList {
- align-items: center;
-}
--- /dev/null
+.inlineList {
+ display: flex;
+ flex-wrap: wrap;
+
+ > li {
+ flex: 0 auto;
+
+ &:not(:last-child) {
+ margin-right: 5px;
+ }
+
+ > a {
+ cursor: pointer;
+ display: flex;
+ }
+ }
+}
+
+.nativeList {
+ margin: 1em 0 1em 40px;
+
+ li {
+ margin: 7px 0;
+ }
+}
+
+ol.dataList,
+ul.dataList {
+ @extend .inlineList;
+
+ font-size: .85rem;
+
+ > li {
+ &:not(:last-child):after {
+ content: ",";
+ padding-left: 1px;
+ }
+ }
+}
+
+.framedIconList {
+ display: flex;
+ flex-wrap: wrap;
+ padding-top: 10px;
+
+ > li {
+ flex: 0 0 25%;
+ text-align: center;
+
+ > a {
+ display: block;
+ }
+ }
+
+ & + .more {
+ float: right;
+ margin-top: 10px;
+ }
+}
+
+.tagList {
+ @extend .inlineList;
+
+ align-items: baseline;
+}
+
+.smileyList {
+ align-items: center;
+}
+++ /dev/null
-.content {
- fieldset > legend {
- float: left;
- width: 100%;
-
- &+ * {
- clear: left;
- }
- }
-
- fieldset > legend,
- section > h1 {
- .wcfFontLarge;
-
- color: rgb(67, 67, 67);
- margin-bottom: 20px;
- }
-
- .container {
- fieldset {
- &:not(:first-child) {
- margin-top: 20px;
- }
-
- > legend {
- border-bottom: 1px solid rgb(238, 238, 238);
- padding-bottom: 5px;
- }
- }
-
- section {
- &:not(:first-child) {
- margin-top: 20px;
- }
-
- > h1 {
- border-bottom: 1px solid rgb(238, 238, 238);
- padding-bottom: 5px;
- }
- }
- }
-
- label {
- display: block;
-
- &:not(:first-child) {
- margin-top: 5px;
- }
- }
-}
--- /dev/null
+.content {
+ fieldset > legend {
+ float: left;
+ width: 100%;
+
+ &+ * {
+ clear: left;
+ }
+ }
+
+ fieldset > legend,
+ section > h1 {
+ @extend .wcfFontLarge;
+
+ color: $wcfContentHeadlineText;
+ margin-bottom: 20px;
+
+ a {
+ color: $wcfContentHeadlineLink;
+
+ &:hover {
+ color: $wcfContentHeadlineLinkActive;
+ }
+ }
+ }
+
+ .container {
+ fieldset {
+ &:not(:first-child) {
+ margin-top: 20px;
+ }
+
+ > legend {
+ border-bottom: 1px solid $wcfContentBorder;
+ padding-bottom: 5px;
+ }
+ }
+
+ section {
+ &:not(:first-child) {
+ margin-top: 20px;
+ }
+
+ > h1 {
+ border-bottom: 1px solid $wcfContentBorder;
+ padding-bottom: 5px;
+ }
+ }
+ }
+
+ label {
+ display: block;
+
+ &:not(:first-child) {
+ margin-top: 5px;
+ }
+ }
+}
+++ /dev/null
-.layoutFluid {
- margin-left: auto;
- margin-right: auto;
- min-width: @wcfLayoutMinWidth;
- max-width: @wcfLayoutMaxWidth;
-}
-
-.marginTop {
- margin-top: 20px;
-}
-
-.framed {
- > canvas,
- > img,
- > .icon {
- background-color: @wcfContentBackgroundColor;
- border: 1px solid @wcfContainerBorderColor;
- padding: 1px;
- }
-}
-
-.invisible {
- display: none;
-}
-
-/* boxes with an image */
-.box(@imageSize, @margin: 0) {
- display: flex;
-
- &:not(:last-child) {
- margin-bottom: (@margin + 3px);
- }
-
- > :first-child:not(:last-child) {
- flex: 0 auto;
- margin-right: @margin;
- }
-
- > :last-child {
- flex: 1;
- }
-}
-
-.box16 { .box(16px, 4px); }
-.box24 { .box(24px, 4px); }
-.box32 { .box(32px, 7px); }
-.box48 { .box(48px, 7px); }
-.box64 { .box(64px, 7px); }
-.box96 { .box(96px, 11px); }
-.box128 { .box(128px, 11px); }
-.box256 { .box(256px, 21px); }
-
-small {
- font-size: .85rem;
-}
-
-.wcfFontDefault {
- font-family: "Open Sans";
- font-weight: 400;
-}
-
-.wcfFontSmall {
- font-family: "Open Sans";
- font-size: .85rem;
- font-weight: 400;
-}
-
-.wcfFontBold {
- font-family: "Open Sans";
- font-weight: 600;
-}
-
-.wcfFontLarger {
- font-family: "Open Sans";
- font-size: 1.2rem;
- font-weight: 300;
-}
-
-.wcfFontLarge {
- font-family: "Open Sans";
- font-size: 1.4rem;
- font-weight: 300;
-}
-
-.elementPointer {
- pointer-events: none;
- position: absolute;
- top: 0;
- transform: translateY(-100%);
-
- &.center {
- left: 50%;
- transform: translateX(-50%) translateY(-100%);
- }
-
- &.left {
- left: 4px;
- }
-
- &.right {
- right: 4px;
- }
-
- &.flipVertical {
- bottom: 0;
- top: auto;
- transform: translateY(100%);
-
- &.center {
- transform: translateX(-50%) translateY(100%);
- }
- }
-}
--- /dev/null
+.layoutFluid {
+ margin-left: auto;
+ margin-right: auto;
+ min-width: $wcfLayoutMinWidth;
+ max-width: $wcfLayoutMaxWidth;
+}
+
+.marginTop {
+ margin-top: 20px;
+}
+
+.framed {
+ > canvas,
+ > img,
+ > .icon {
+ //background-color: @wcfContentBackgroundColor;
+ //border: 1px solid @wcfContainerBorderColor;
+ padding: 1px;
+ }
+}
+
+.invisible {
+ display: none;
+}
+
+.grayscale {
+ filter: gray;
+ -webkit-filter: grayscale(1);
+}
+
+/* boxes with an image */
+@mixin box($imageSize, $margin: 0) {
+ display: flex;
+
+ &:not(:last-child) {
+ margin-bottom: ($margin + 3px);
+ }
+
+ > :first-child:not(:last-child) {
+ flex: 0 auto;
+ margin-right: $margin;
+ }
+
+ > :last-child {
+ flex: 1;
+ }
+}
+
+.box16 { @include box(16px, 4px); }
+.box24 { @include box(24px, 4px); }
+.box32 { @include box(32px, 7px); }
+.box48 { @include box(48px, 7px); }
+.box64 { @include box(64px, 7px); }
+.box96 { @include box(96px, 11px); }
+.box128 { @include box(128px, 11px); }
+.box256 { @include box(256px, 21px); }
+
+small {
+ font-size: .85rem;
+}
+
+.wcfFontDefault {
+ font-family: "Open Sans";
+ font-weight: 400;
+}
+
+.wcfFontSmall {
+ font-family: "Open Sans";
+ font-size: .85rem;
+ font-weight: 400;
+}
+
+.wcfFontBold {
+ font-family: "Open Sans";
+ font-weight: 600;
+}
+
+.wcfFontLarger {
+ font-family: "Open Sans";
+ font-size: 1.2rem;
+ font-weight: 300;
+}
+
+.wcfFontLarge {
+ font-family: "Open Sans";
+ font-size: 1.4rem;
+ font-weight: 300;
+}
+
+.elementPointer {
+ pointer-events: none;
+ position: absolute;
+ top: 0;
+ transform: translateY(-100%);
+
+ &.center {
+ left: 50%;
+ transform: translateX(-50%) translateY(-100%);
+ }
+
+ &.left {
+ left: 4px;
+ }
+
+ &.right {
+ right: 4px;
+ }
+
+ &.flipVertical {
+ bottom: 0;
+ top: auto;
+ transform: translateY(100%);
+
+ &.center {
+ transform: translateX(-50%) translateY(100%);
+ }
+ }
+}
+++ /dev/null
-/* @TODO */
-.userAvatarImage { border-radius: 50%; }
-/* @TODO END */
-
-html, body {
- font-size: @wcfBaseFontSize;
- height: 100%;
-}
-
-body {
- background-color: @wcfPageBackgroundColor;
- color: @wcfPageColor;
- line-height: 1.428571429;
- position: relative;
- word-wrap: break-word;
-
- .wcfFontDefault;
-}
-
-a {
- color: @wcfPageLinkColor;
- cursor: pointer;
-
- &:hover {
- color: @wcfPageLinkHoverColor;
- }
-
- &:not(:hover) {
- text-decoration: none;
- }
-}
-
-strong {
- .wcfFontBold;
-}
-
-#pageContainer {
- display: flex;
- height: 100%;
- flex-direction: column;
-}
-
-/* COLUMN LAYOUT */
-#pageHeader {
- flex: 0 auto;
- z-index: 100;
-}
-
-.main {
- background-color: @wcfContentBackgroundColor;
- color: @wcfColor;
- flex: 1 auto;
- padding: 40px 0;
- z-index: 50;
-
- > div {
- display: flex;
- }
-
- a {
- color: @wcfLinkColor;
- text-decoration: none;
-
- &:hover {
- color: @wcfLinkHoverColor;
- }
- }
-}
-
-#content {
- flex: 1 auto;
-}
-
-.sidebar {
- flex: 0 0 310px;
-
- &:first-child {
- margin-right: 30px;
- }
-}
-
-#content + .sidebar {
- margin-left: 30px;
-}
-
-#pageFooter {
- flex: 0 auto;
-}
-
-/* CONTENT AREA */
-.boxHeadline {
- border-bottom: 1px solid rgba(238, 238, 238, 1);
- margin-bottom: 30px;
- padding-bottom: 10px;
-
- > h1 {
- .wcfFontLarge;
-
- color: rgba(67, 67, 67, 1);
- font-size: 2rem;
- }
-
- &.labeledHeadline {
- font-size: 0;
-
- > h1,
- > h2 {
- display: inline-block;
- margin-right: 10px;
- }
-
- > ul {
- display: inline-block;
- font-size: 1rem;
-
- &:not(:empty) {
- margin-right: 10px;
- }
- }
- }
-}
-
-.containerHeadline {
- > h3 {
- .wcfFontLarger;
-
- > .badge {
- margin-left: 5px;
- }
- }
-}
--- /dev/null
+/* @TODO */
+.userAvatarImage { border-radius: 50%; }
+/* @TODO END */
+
+html, body {
+ font-size: $wcfBaseFontSize;
+ height: 100%;
+}
+
+body {
+ line-height: 1.428571429;
+ position: relative;
+ word-wrap: break-word;
+
+ @extend .wcfFontDefault;
+}
+
+a {
+ cursor: pointer;
+
+ &:not(:hover) {
+ text-decoration: none;
+ }
+}
+
+strong {
+ @extend .wcfFontBold;
+}
+
+#pageContainer {
+ display: flex;
+ height: 100%;
+ flex-direction: column;
+}
+
+/* COLUMN LAYOUT */
+#pageHeader {
+ flex: 0 auto;
+ z-index: 100;
+}
+
+.main {
+ background-color: $wcfContentBackground;
+ color: $wcfContentText;
+ flex: 1 auto;
+ padding: 40px 0;
+ z-index: 50;
+
+ > div {
+ display: flex;
+ }
+
+ a {
+ color: $wcfContentLink;
+ text-decoration: none;
+
+ &:hover {
+ color: $wcfContentLinkActive;
+ }
+ }
+}
+
+#content {
+ flex: 1 auto;
+}
+
+.sidebar {
+ flex: 0 0 310px;
+
+ &:first-child {
+ margin-right: 30px;
+ }
+}
+
+#content + .sidebar {
+ margin-left: 30px;
+}
+
+#pageFooter {
+ flex: 0 auto;
+}
+
+/* CONTENT AREA */
+.boxHeadline {
+ border-bottom: 1px solid $wcfContentBorder;
+ color: $wcfContentHeadlineText;
+ margin-bottom: 30px;
+ padding-bottom: 10px;
+
+ > h1 {
+ @extend .wcfFontLarge;
+
+ font-size: 2rem;
+ }
+
+ a {
+ color: $wcfContentHeadlineLink;
+
+ &:hover {
+ color: $wcfContentHeadlineLinkActive;
+ }
+ }
+
+ &.labeledHeadline {
+ font-size: 0;
+
+ > h1,
+ > h2 {
+ display: inline-block;
+ margin-right: 10px;
+ }
+
+ > ul {
+ display: inline-block;
+ font-size: 1rem;
+
+ &:not(:empty) {
+ margin-right: 10px;
+ }
+ }
+ }
+}
+
+.containerHeadline {
+ > h3 {
+ @extend .wcfFontLarger;
+
+ > .badge {
+ margin-left: 5px;
+ }
+ }
+}
+++ /dev/null
-.navigation {
- background-color: rgba(44, 62, 80, 1);
- flex: 0 auto;
- padding: 5px 0;
- z-index: 25;
-
- > div {
- align-items: center;
- display: flex;
- justify-content: flex-end;
- height: 30px;
- }
-}
-
-.navigationIcons {
- display: flex;
- flex: 0 auto;
- flex-direction: row-reverse;
-
- > li {
- flex: 0 auto;
-
- &:not(:last-child) {
- margin-left: 10px;
- }
-
- > a {
- opacity: .8;
- transition: opacity .2s linear;
-
- &:hover {
- opacity: 1;
- }
-
- > .icon {
- color: rgba(255, 255, 255, 1);
- }
- }
- }
-}
-
-.contentNavigation {
- align-items: center;
- display: flex;
-
- &:not(:first-child) {
- margin-top: 20px;
- }
-
- &:not(:last-child) {
- margin-bottom: 20px;
- }
-
- > nav {
- flex: 1 auto;
-
- & + nav {
- flex: 0 0 auto;
- }
-
- &:not(.pageNavigation) {
- text-align: right;
- }
- }
-
- ul {
- display: inline-flex;
-
- > li {
- flex: 0 0 auto;
-
- &:not(:last-child) {
- margin-right: 5px;
- }
- }
- }
-}
-
-.pageNavigation > ul {
- background-color: rgb(79, 129, 189);
- border-radius: 3px;
- display: inline-flex;
- overflow: hidden;
-
- > li {
- background-color: transparent;
- border-radius: 0;
- border-width: 0;
- flex: 0 0 auto;
- padding: 0;
- transition: background-color .2s linear;
-
- &:not(.active):not(.disabled):hover {
- background-color: #3498db;
- }
-
- &:not(:last-child) {
- border-right: 1px solid rgb(112, 152, 200);
- margin-right: 0;
- }
-
- &.active {
- background-color: rgb(255, 255, 255);
-
- > span:not(.invisible) {
- color: rgb(79, 129, 189);
- cursor: default;
-
- &:hover {
- color: rgb(79, 129, 189);
- }
- }
- }
-
- &.jumpTo {
- cursor: pointer;
- }
-
- &.skip {
- align-items: center;
- display: flex;
- }
-
- > a,
- > span:not(.invisible) {
- color: rgb(255, 255, 255);
- display: block;
- padding: 3px 7px;
-
- &:hover {
- color: rgb(255, 255, 255);
- }
- }
- }
-}
-
-.pageNavigation.small > ul > li {
- > a,
- > span:not(.invisible) {
- padding: 1px 5px;
- }
-}
--- /dev/null
+.navigation {
+ background-color: $wcfHeaderBackground;
+ flex: 0 auto;
+ padding: 5px 0;
+ z-index: 25;
+
+ > div {
+ align-items: center;
+ display: flex;
+ justify-content: flex-end;
+ height: 30px;
+ }
+}
+
+.navigationIcons {
+ display: flex;
+ flex: 0 auto;
+ flex-direction: row-reverse;
+
+ > li {
+ flex: 0 auto;
+
+ &:not(:last-child) {
+ margin-left: 10px;
+ }
+
+ > a {
+ opacity: .8;
+ transition: opacity .2s linear;
+
+ &:hover {
+ opacity: 1;
+ }
+
+ > .icon {
+ color: $wcfHeaderLink;
+ }
+ }
+ }
+}
+
+.contentNavigation {
+ align-items: center;
+ display: flex;
+
+ &:not(:first-child) {
+ margin-top: 20px;
+ }
+
+ &:not(:last-child) {
+ margin-bottom: 20px;
+ }
+
+ > nav {
+ flex: 1 auto;
+
+ & + nav {
+ flex: 0 0 auto;
+ }
+
+ &:not(.pageNavigation) {
+ text-align: right;
+ }
+ }
+
+ ul {
+ display: inline-flex;
+
+ > li {
+ flex: 0 0 auto;
+
+ &:not(:last-child) {
+ margin-right: 5px;
+ }
+ }
+ }
+}
+
+.pageNavigation > ul {
+ background-color: rgb(79, 129, 189);
+ border-radius: 3px;
+ display: inline-flex;
+ overflow: hidden;
+
+ > li {
+ background-color: transparent;
+ border-radius: 0;
+ border-width: 0;
+ flex: 0 0 auto;
+ padding: 0;
+ transition: background-color .2s linear;
+
+ &:not(.active):not(.disabled):hover {
+ background-color: #3498db;
+ }
+
+ &:not(:last-child) {
+ border-right: 1px solid rgb(112, 152, 200);
+ margin-right: 0;
+ }
+
+ &.active {
+ background-color: rgb(255, 255, 255);
+
+ > span:not(.invisible) {
+ color: rgb(79, 129, 189);
+ cursor: default;
+
+ &:hover {
+ color: rgb(79, 129, 189);
+ }
+ }
+ }
+
+ &.jumpTo {
+ cursor: pointer;
+ }
+
+ &.skip {
+ align-items: center;
+ display: flex;
+ }
+
+ > a,
+ > span:not(.invisible) {
+ color: rgb(255, 255, 255);
+ display: block;
+ padding: 3px 7px;
+
+ &:hover {
+ color: rgb(255, 255, 255);
+ }
+ }
+ }
+}
+
+.pageNavigation.small > ul > li {
+ > a,
+ > span:not(.invisible) {
+ padding: 1px 5px;
+ }
+}
+++ /dev/null
-#pageFooter {
- background-color: rgba(52, 73, 94, 1);
- padding: 20px 0;
- z-index: 20;
-
- address {
- color: rgba(255, 255, 255, 1);
- font-style: normal;
- opacity: .8;
- text-align: center;
- transition: opacity .2s linear;
-
- &:hover {
- opacity: 1;
- }
-
- a {
- color: rgba(255, 255, 255, 1);
- }
- }
-}
-
-#pageFooterBoxes {
- background-color: rgba(44, 62, 80, 1);
- padding: 30px;
- z-index: 40;
-
- > div > ul {
- display: flex;
- flex-wrap: wrap;
-
- > li {
- flex: 0 0 50%;
-
- > .icon:first-child {
- display: none;
- }
-
- > div > .containerHeadline > h3 {
- font-family: "Segoe UI Light";
- font-size: 1.4rem;
- margin-bottom: 10px;
- }
- }
- }
-}
--- /dev/null
+#pageFooterBoxes {
+ background-color: $wcfFooterBoxBackground;
+ color: $wcfFooterBoxText;
+ padding: 30px;
+ z-index: 40;
+
+ a {
+ color: $wcfFooterBoxLink;
+
+ &:hover {
+ color: $wcfFooterBoxLinkActive;
+ }
+ }
+
+ > div > ul {
+ display: flex;
+ flex-wrap: wrap;
+
+ > li {
+ flex: 0 0 50%;
+
+ > .icon:first-child {
+ display: none;
+ }
+
+ > div > .containerHeadline > h3 {
+ font-family: "Segoe UI Light";
+ font-size: 1.4rem;
+ margin-bottom: 10px;
+ }
+ }
+ }
+}
+
+#pageFooter {
+ background-color: $wcfFooterBackground;
+ color: $wcfFooterText;
+ padding: 20px 0;
+ z-index: 20;
+
+ a {
+ color: $wcfFooterLink;
+
+ &:hover {
+ color: $wcfFooterLinkActive;
+ }
+ }
+
+ address {
+ font-style: normal;
+ opacity: .8;
+ text-align: center;
+ transition: opacity .2s linear;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+}
+++ /dev/null
-/* @TODO */
-#logo {
- //width: 75px;
- overflow: hidden;
- > a > img {
- //height: 60px;
- }
-}
-.interactiveDropdown { display: none; }
-/* @TODO END */
-
-#pageHeader {
- background-color: rgba(52, 73, 94, 1); /* @TODO */
-}
-
-#pageHeader > div > div {
- align-items: center;
- display: flex;
- flex-wrap: wrap;
- padding: 20px 0;
-}
-
-/* LOGO */
-#logo {
- flex: 0 0 50%;
- //margin-right: @wcfGapMedium;
-
- > a > img.small {
- display: none;
- }
-}
-
-/* MAIN MENU */
-#mainMenu {
- flex: 0 50%;
-
- > ul {
- display: flex;
-
- > li {
- .wcfFontLarger;
-
- flex: 0 auto;
-
- &:not(:last-child) {
- margin-right: @wcfGapMedium;
- }
-
- &.active > a {
- .wcfFontBold;
-
- color: rgba(255, 255, 255, 1);
- }
-
- > a {
- color: rgba(255, 255, 255, .8);
- transition: color .2s linear;
- text-decoration: none;
- text-transform: uppercase;
-
- &:hover {
- color: rgba(255, 255, 255, 1);
- }
- }
- }
- }
-}
-
-.subMenuItems {
- position: relative;
-
- &:hover > .subMenu {
- opacity: 1;
- transition-delay: 0s;
- visibility: visible;
- }
-
- > a {
- padding-right: 15px;
-
- &:after {
- content: @fa-var-caret-down;
- display: block;
- font-family: FontAwesome;
- position: absolute;
- right: 0;
- top: 0;
- }
- }
-}
-
-.subMenu {
- background-color: rgba(52, 73, 94, 1);
- border-radius: 3px;
- opacity: 0;
- padding: 5px 0;
- position: absolute;
- transition: visibility .2s linear .2s, opacity .2s linear;
- visibility: hidden;
-
- > li {
- &:not(:first-child) {
- margin-top: 5px;
- }
-
- &.active > a {
- background-color: rgb(79, 129, 189);
- cursor: default;
- }
-
- > a {
- .wcfFontDefault;
-
- color: rgb(255, 255, 255);
- display: block;
- font-size: 1rem;
- padding: 5px 10px;
- transition: background-color .2s linear;
- white-space: nowrap;
-
- &:hover {
- background-color: rgb(79, 129, 189);
- }
- }
- }
-}
-
-
-
-/* USER PANEL */
-#topMenu {
- flex: 0 50%;
-
- > ul {
- display: flex;
- justify-content: flex-end;
-
- > li {
- align-items: center;
- display: flex;
- flex: 0 auto;
-
- &:not(:last-child) > a {
- margin: 0 5px;
- }
-
- &.active > a {
- color: rgba(255, 255, 255, 1);
- font-family: "Segoe UI Semibold";
- font-weight: bold;
- }
-
- > a {
- color: rgba(255, 255, 255, .8);
- flex: 0 auto;
- transition: color .2s linear;
- text-decoration: none;
- text-transform: uppercase;
-
- &:hover {
- color: rgba(255, 255, 255, 1);
-
- > .icon {
- color: rgba(255, 255, 255, 1);
- }
- }
-
- > .icon {
- color: rgba(255, 255, 255, .8);
- transition: color .2s linear;
-
- .icon24;
- }
-
- > span:not(.icon) {
- display: none;
- }
- }
- }
- }
-}
-
-/* SEARCH AREA */
-#search {
- flex: 0 50%;
- text-align: right;
-}
-
--- /dev/null
+/* @TODO */
+#logo {
+ //width: 75px;
+ overflow: hidden;
+ > a > img {
+ //height: 60px;
+ }
+}
+.interactiveDropdown { display: none; }
+/* @TODO END */
+
+#pageHeader {
+ background-color: $wcfUserPanelBackground;
+}
+
+#pageHeader > div > div {
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ padding: 20px 0;
+}
+
+/* LOGO */
+#logo {
+ flex: 0 0 50%;
+ //margin-right: @wcfGapMedium;
+
+ > a > img.small {
+ display: none;
+ }
+}
+
+/* MAIN MENU */
+#mainMenu {
+ flex: 0 50%;
+
+ > ul {
+ display: flex;
+
+ > li {
+ @extend .wcfFontLarger;
+
+ flex: 0 auto;
+
+ &:not(:last-child) {
+ margin-right: $wcfGapMedium;
+ }
+
+ &.active > a {
+ @extend .wcfFontBold;
+
+ color: rgba(255, 255, 255, 1);
+ }
+
+ > a {
+ color: rgba(255, 255, 255, .8);
+ transition: color .2s linear;
+ text-decoration: none;
+ text-transform: uppercase;
+
+ &:hover {
+ color: rgba(255, 255, 255, 1);
+ }
+ }
+ }
+ }
+}
+
+.subMenuItems {
+ position: relative;
+
+ &:hover > .subMenu {
+ opacity: 1;
+ transition-delay: 0s;
+ visibility: visible;
+ }
+
+ > a {
+ padding-right: 15px;
+
+ &:after {
+ // @TODO
+ //content: @fa-var-caret-down;
+ display: block;
+ font-family: FontAwesome;
+ position: absolute;
+ right: 0;
+ top: 0;
+ }
+ }
+}
+
+.subMenu {
+ background-color: rgba(52, 73, 94, 1);
+ border-radius: 3px;
+ opacity: 0;
+ padding: 5px 0;
+ position: absolute;
+ transition: visibility .2s linear .2s, opacity .2s linear;
+ visibility: hidden;
+
+ > li {
+ &:not(:first-child) {
+ margin-top: 5px;
+ }
+
+ &.active > a {
+ background-color: rgb(79, 129, 189);
+ cursor: default;
+ }
+
+ > a {
+ @extend .wcfFontDefault;
+
+ color: rgb(255, 255, 255);
+ display: block;
+ font-size: 1rem;
+ padding: 5px 10px;
+ transition: background-color .2s linear;
+ white-space: nowrap;
+
+ &:hover {
+ background-color: rgb(79, 129, 189);
+ }
+ }
+ }
+}
+
+
+
+/* USER PANEL */
+#topMenu {
+ flex: 0 50%;
+
+ > ul {
+ display: flex;
+ justify-content: flex-end;
+
+ > li {
+ align-items: center;
+ display: flex;
+ flex: 0 auto;
+
+ &:not(:last-child) > a {
+ margin: 0 5px;
+ }
+
+ &.active > a {
+ // @TODO
+ color: rgba(255, 255, 255, 1);
+ font-family: "Segoe UI Semibold";
+ font-weight: bold;
+ }
+
+ > a {
+ color: rgba(255, 255, 255, .8);
+ flex: 0 auto;
+ transition: color .2s linear;
+ text-decoration: none;
+ text-transform: uppercase;
+
+ &:hover {
+ color: rgba(255, 255, 255, 1);
+
+ > .icon {
+ color: rgba(255, 255, 255, 1);
+ }
+ }
+
+ > .icon {
+ color: rgba(255, 255, 255, .8);
+ transition: color .2s linear;
+
+ @extend .icon24;
+ }
+
+ > span:not(.icon) {
+ display: none;
+ }
+ }
+ }
+ }
+}
+
+/* SEARCH AREA */
+#search {
+ flex: 0 50%;
+ text-align: right;
+}
+
+++ /dev/null
-.sidebar {
- > div,
- > fieldset,
- > section {
- background-color: rgb(240, 240, 240);
- border-radius: 3px;
- padding: 20px;
-
- &:after {
- clear: both;
- content: "";
- display: block;
- height: 0;
- }
- }
-
- > div + *,
- > fieldset + *,
- > section + * {
- margin-top: 30px;
- }
-
- > div {
- fieldset + fieldset,
- section + section {
- margin-top: 20px;
- }
- }
-
- fieldset > legend {
- float: left;
- width: 100%;
-
- & + * {
- clear: left;
- }
- }
-
- section > h1,
- fieldset > legend {
- .wcfFontLarge;
-
- color: rgba(67, 67, 67, 1);
- margin-bottom: 10px;
- }
-
- dl.dataList {
- font-size: .85rem;
- overflow: hidden;
- }
-
- dl:not(.dataList) {
- &:not(:first-child) {
- margin-top: 10px;
- }
- }
-
- dl:not(.plain) {
- display: block;
-
- > dt,
- > dd {
- display: block;
- margin: 0;
- text-align: left;
- width: 100%;
- }
- }
-}
-
-.sidebarNavigation > li {
- &.active {
- background-color: rgb(255, 255, 255);
- margin: 0 -20px;
- padding: 0 20px;
- }
-
- > a {
- display: block;
- padding: 5px 0;
- }
-}
-
-.sidebarBoxHeadline > h3 {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
--- /dev/null
+.sidebar {
+ > div,
+ > fieldset,
+ > section {
+ background-color: $wcfContentBackgroundAlternate;
+ border-radius: 3px;
+ padding: 20px;
+
+ &:after {
+ clear: both;
+ content: "";
+ display: block;
+ height: 0;
+ }
+ }
+
+ > div + *,
+ > fieldset + *,
+ > section + * {
+ margin-top: 30px;
+ }
+
+ > div {
+ fieldset + fieldset,
+ section + section {
+ margin-top: 20px;
+ }
+ }
+
+ fieldset > legend {
+ float: left;
+ width: 100%;
+
+ & + * {
+ clear: left;
+ }
+ }
+
+ section > h1,
+ fieldset > legend {
+ .wcfFontLarge;
+
+ color: rgba(67, 67, 67, 1);
+ margin-bottom: 10px;
+ }
+
+ dl.dataList {
+ font-size: .85rem;
+ overflow: hidden;
+ }
+
+ dl:not(.dataList) {
+ &:not(:first-child) {
+ margin-top: 10px;
+ }
+ }
+
+ dl:not(.plain) {
+ display: block;
+
+ > dt,
+ > dd {
+ display: block;
+ margin: 0;
+ text-align: left;
+ width: 100%;
+ }
+ }
+}
+
+.sidebarNavigation > li {
+ &.active {
+ background-color: rgb(255, 255, 255);
+ margin: 0 -20px;
+ padding: 0 20px;
+ }
+
+ > a {
+ display: block;
+ padding: 5px 0;
+ }
+}
+
+.sidebarBoxHeadline > h3 {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+++ /dev/null
-.error,
-.info,
-.success {
- border-radius: 3px;
- padding: 10px 20px;
-}
-
-.error {
- background-color: rgb(242, 222, 222);
- color: rgb(169, 68, 66);
-}
-
-.info {
- background-color: rgb(217, 237, 247);
- color: rgb(49, 112, 143);
-}
-
-.success {
- background-color: rgb(223, 240, 216);
- color: rgb(60, 118, 61);
-}
-
-/* inline errors */
-.innerError {
- background-color: rgb(242, 222, 222);
- color: rgb(169, 68, 66);
- display: table;
- line-height: 1.5;
- margin-top: 8px;
- padding: 5px 10px;
- position: relative;
-
- /* pointer */
- &::before {
- border: 6px solid transparent;
- border-bottom-color: rgb(242, 222, 222);
- border-top-width: 0;
- content: "";
- display: inline-block;
- left: 10px;
- position: absolute;
- top: -6px;
- z-index: 101;
- }
-}
--- /dev/null
+.error,
+.info,
+.success {
+ border-radius: 3px;
+ padding: 10px 20px;
+}
+
+.error {
+ background-color: rgb(242, 222, 222);
+ color: rgb(169, 68, 66);
+}
+
+.info {
+ background-color: rgb(217, 237, 247);
+ color: rgb(49, 112, 143);
+}
+
+.success {
+ background-color: rgb(223, 240, 216);
+ color: rgb(60, 118, 61);
+}
+
+/* inline errors */
+.innerError {
+ background-color: rgb(242, 222, 222);
+ color: rgb(169, 68, 66);
+ display: table;
+ line-height: 1.5;
+ margin-top: 8px;
+ padding: 5px 10px;
+ position: relative;
+
+ /* pointer */
+ &::before {
+ border: 6px solid transparent;
+ border-bottom-color: rgb(242, 222, 222);
+ border-top-width: 0;
+ content: "";
+ display: inline-block;
+ left: 10px;
+ position: absolute;
+ top: -6px;
+ z-index: 101;
+ }
+}
+++ /dev/null
-.breadcrumbs {
- flex: 1;
-
- > ul {
- display: flex;
-
- > li {
- flex: 0 auto;
- font-size: .85rem;
-
- &:not(:last-child) {
- margin-right: 10px;
-
- &:after {
- content: "/";
- }
-
- > a {
- margin-right: 10px;
- }
- }
-
- > a {
- color: rgba(255, 255, 255, 1);
- opacity: .8;
- text-decoration: none;
- transition: opacity .2s linear;
-
- &:hover {
- opacity: 1;
- }
- }
- }
- }
-}
\ No newline at end of file
--- /dev/null
+.breadcrumbs {
+ flex: 1;
+
+ > ul {
+ display: flex;
+
+ > li {
+ flex: 0 auto;
+ font-size: .85rem;
+
+ &:not(:last-child) {
+ margin-right: 10px;
+
+ &:after {
+ color: $wcfHeaderText;
+ content: "/";
+ }
+
+ > a {
+ margin-right: 10px;
+ }
+ }
+
+ > a {
+ color: $wcfHeaderLink;
+ opacity: .8;
+ text-decoration: none;
+ transition: opacity .2s linear;
+
+ &:hover {
+ color: $wcfHeaderLinkActive;
+ opacity: 1;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
+++ /dev/null
-button,
-input[type="button"],
-input[type="reset"],
-input[type="submit"],
-.button {
- background-color: @wcfButtonBackgroundColor;
- border: 1px solid @wcfButtonBorderColor;
- border-radius: 3px;
- color: @wcfButtonColor !important;
- cursor: pointer;
- display: inline-block;
- padding: 5px 10px;
- transition-duration: .2s;
- transition-property: background-color, border-color, color;
- transition-timing-function: linear;
-
- .icon {
- color: @wcfButtonColor;
- transition: color .2s linear;
- }
-
- &:hover {
- background-color: @wcfButtonHoverBackgroundColor;
- border-color: @wcfButtonHoverBorderColor;
- color: @wcfButtonHoverColor !important;
-
- .icon {
- color: @wcfButtonHoverColor;
- }
- }
-
- &.small {
- font-size: .85rem;
- padding: 4px 7px;
- }
-}
-
-button.buttonPrimary,
-input[type="button"].buttonPrimary,
-input[type="submit"],
-.button.buttonPrimary {
- background-color: @wcfButtonPrimaryBackgroundColor;
- border-color: @wcfButtonPrimaryBorderColor;
- color: @wcfButtonPrimaryColor !important;
-
- .icon {
- color: @wcfButtonPrimaryColor;
- }
-
- /* @TODO */
- &:hover {
- background-color: @wcfButtonPrimaryBackgroundColor;
- border-color: @wcfButtonHoverBorderColor;
- color: @wcfButtonHoverColor;
-
- .icon {
- color: @wcfButtonHoverColor;
- }
- }
- /* @TODO END */
-}
-
-.buttonList {
- .inlineList;
-
- &.smallButtons .button {
- font-size: .85rem;
- padding: 4px 7px;
- }
-
- /* members list */
- &.letters {
- margin-bottom: -10px;
-
- > li {
- flex: 0 0 10%;
- margin-bottom: 10px;
-
- > a {
- display: block;
- text-align: center;
- }
- }
- }
-}
-
-.buttonGroupNavigation > ul {
- .inlineList;
-}
-
-.buttonGroup {
- .inlineList;
-
- > li {
- &:not(:last-child) {
- border-right: 1px solid @wcfButtonHoverBackgroundColor;
- margin-right: 0;
- }
-
- &:first-child .button {
- border-bottom-left-radius: 3px;
- border-top-left-radius: 3px;
- }
-
- &:last-child .button {
- border-bottom-right-radius: 3px;
- border-top-right-radius: 3px;
- }
-
- .button {
- border-radius: 0;
- }
- }
-}
--- /dev/null
+button,
+input[type="button"],
+input[type="reset"],
+input[type="submit"],
+.button {
+ background-color: $wcfButtonBackground;
+ border: 1px solid $wcfButtonBorder;
+ border-radius: 3px;
+ color: $wcfButtonText !important;
+ cursor: pointer;
+ display: inline-block;
+ padding: 5px 10px;
+ transition-duration: .2s;
+ transition-property: background-color, border-color, color;
+ transition-timing-function: linear;
+
+ .icon {
+ color: $wcfButtonText;
+ transition: color .2s linear;
+ }
+
+ &:hover {
+ background-color: $wcfButtonBackgroundActive;
+ border-color: $wcfButtonBorderActive;
+ color: $wcfButtonTextActive !important;
+
+ .icon {
+ color: $wcfButtonTextActive;
+ }
+ }
+
+ &.small {
+ font-size: .85rem;
+ padding: 4px 7px;
+ }
+}
+
+button.buttonPrimary,
+input[type="button"].buttonPrimary,
+input[type="submit"],
+.button.buttonPrimary {
+ background-color: $wcfButtonBackgroundAccent;
+ border-color: $wcfButtonBorderAccent;
+ color: $wcfButtonTextAccent !important;
+
+ .icon {
+ color: $wcfButtonTextAccent;
+ }
+
+ &:hover {
+ background-color: $wcfButtonBackgroundAccentActive;
+ border-color: $wcfButtonBorderAccentActive;
+ color: $wcfButtonTextAccentActive;
+
+ .icon {
+ color: $wcfButtonTextAccentActive;
+ }
+ }
+}
+
+.buttonList {
+ @extend .inlineList;
+
+ &.smallButtons .button {
+ font-size: .85rem;
+ padding: 4px 7px;
+ }
+
+ /* members list */
+ &.letters {
+ margin-bottom: -10px;
+
+ > li {
+ flex: 0 0 10%;
+ margin-bottom: 10px;
+
+ > a {
+ display: block;
+ text-align: center;
+ }
+ }
+ }
+}
+
+.buttonGroupNavigation > ul {
+ @extend .inlineList;
+}
+
+.buttonGroup {
+ @extend .inlineList;
+
+ > li {
+ &:not(:last-child) {
+ border-right: 1px solid $wcfButtonBackgroundAccentActive;
+ margin-right: 0;
+ }
+
+ &:first-child .button {
+ border-bottom-left-radius: 3px;
+ border-top-left-radius: 3px;
+ }
+
+ &:last-child .button {
+ border-bottom-right-radius: 3px;
+ border-top-right-radius: 3px;
+ }
+
+ .button {
+ border-radius: 0;
+ }
+ }
+}
+++ /dev/null
-.messageList {
- border: 1px solid rgb(79, 129, 189);
- border-width: 1px 0;
- margin: 30px 0;
-
- > li {
- margin-top: 0;
- padding: 40px 0;
-
- &:not(:first-child) {
- border-top: 1px solid rgb(79, 129, 189);
- }
- }
-}
-
-.message {
- display: flex;
-}
-
-/* sidebar */
-.messageSidebar {
- align-self: flex-start;
- background-color: rgb(79, 129, 189);
- border-radius: 3px;
- color: rgb(255, 255, 255);
- flex: 0 0 170px;
- padding: 20px;
- position: relative;
- text-align: center;
-
- a {
- color: rgb(255, 255, 255);
- }
-
- .dataList {
- font-size: .85rem;
- }
-}
-
-.messageAuthor + * {
- margin-top: 25px;
-
- &:before {
- border-top: 2px solid rgb(112, 152, 200);
- content: "";
- left: 0;
- margin-top: -10px;
- position: absolute;
- right: 0;
- }
-}
-
-.messageAuthorContainer:not(:last-child) {
- margin-bottom: 5px;
-}
-
-/* content */
-.messageContent {
- display: flex;
- flex: 1 auto;
- flex-direction: column;
- margin-left: 30px;
-}
-
-/* content - header */
-.messageHeader {
- display: flex;
- flex: 0 0 auto;
- margin-bottom: 20px;
-}
-
-.messageQuickOptions {
- .inlineList;
-
- flex: 0 0 auto;
- opacity: .3;
- order: 2;
- transition: opacity .2s linear;
-
- .badge {
- color: rgb(255, 255, 255);
- font-size: .85rem;
- }
-}
-
-.message:hover .messageQuickOptions {
- opacity: .6;
-}
-
-.message .messageHeader .messageQuickOptions:hover {
- opacity: 1;
-}
-
-.messageHeadline {
- flex: 1 auto;
- order: 1;
-
- > h1 {
- font-family: "Segoe UI Light";
- font-size: 1.4rem;
- margin-bottom: 5px;
- }
-
- > p {
- font-size: .85rem;
- }
-}
-
-/* content - body */
-.messageBody {
- flex: 1 auto;
-
- &.editor {
- align-items: center;
- display: flex;
- justify-content: center;
-
- > .icon {
- flex: 0 0 auto;
- }
-
- > .editorContainer {
- flex: 1 auto;
- }
- }
-}
-
-/* content - footer */
-.messageFooter {
- flex: 0 0 auto;
- margin-top: 20px;
-}
-
-.messageFooterNote {
- border-left: 2px solid rgb(238, 238, 238);
- padding-left: 5px;
-
- &:not(:first-child) {
- margin-top: 5px;
- }
-}
-
-.messageSignature {
- border-top: 1px solid rgb(238, 238, 238);
- opacity: .6;
- padding-top: 10px;
- transition: opacity .2s linear;
-}
-
-.message:hover .messageSignature {
- opacity: 1;
-}
-
-.messageFooterButtons {
- .inlineList;
-
- justify-content: flex-end;
- margin-top: 10px;
- opacity: .3;
- transition: opacity .2s linear;
-
- &.forceVisible {
- opacity: 1 !important;
- }
-
- > li:not(:last-child) {
- margin-right: 5px;
- }
-}
-
-.message:hover {
- .messageFooterButtons {
- opacity: .6;
-
- &:hover {
- opacity: 1;
- }
- }
-}
-
-/* ### message groups ### */
-.messageGroupList {
- .columnSubject {
- > .labelList {
- float: right;
- padding-left: 7px;
- }
-
- > h3 {
- > .messageGroupLink {
- font-size: @wcfTitleFontSize;
- }
-
- > .badge.label {
- top: -2px;
- }
- }
-
- > small {
- display: block;
- }
-
- > nav {
- font-size: @wcfSmallFontSize;
-
- > ul > li {
- display: inline;
- }
- }
- }
-
- tr {
- &.new .columnSubject > h3 > .messageGroupLink {
- font-weight: bold;
- }
-
- &.new .columnAvatar div > p > img,
- &:hover .columnAvatar div > p > img {
- opacity: 1;
- }
-
- &.messageDisabled {
- color: @wcfDisabledColor;
-
- > td {
- background-color: @wcfDisabledBackgroundColor !important;
- }
-
- a:not(.badge) {
- color: @wcfDisabledColor;
- }
- }
-
- &.messageDeleted {
- color: @wcfDeletedColor;
-
- > td {
- background-color: @wcfDeletedBackgroundColor !important;
- }
-
- a:not(.badge) {
- color: @wcfDeletedColor;
- }
- }
-
- .columnSubject .statusDisplay .pageNavigation {
- opacity: 0;
-
- .transition(opacity, .2s);
- }
-
- &:hover .columnSubject .statusDisplay .pageNavigation {
- opacity: 1;
- }
-
- &.new .columnAvatar > div {
- &::after {
- color: @wcfLinkColor;
- content: "\f069";
- font-family: FontAwesome;
- font-weight: normal !important;
- font-style: normal !important;
- font-size: 14px;
- position: absolute;
- text-decoration: none !important;
- top: -4px;
- right: -2px;
-
- .textShadow(@wcfContainerBackgroundColor);
- }
- }
- }
-
- .columnAvatar {
- div {
- position: relative;
- width: 40px;
- height: 38px;
-
- > p > img {
- opacity: .6;
-
- .transition(opacity, .2s);
- }
- }
-
- .myAvatar {
- position: absolute;
- width: 16px;
- height: 16px;
- bottom: -2px;
- left: 24px;
- opacity: 1;
-
- > img {
- .boxShadow(0, 0, rgba(0, 0, 0, .3), 3px);
- }
- }
- }
-
- .columnLastPost {
- white-space: nowrap;
- word-wrap: normal;
-
- > div > div > small {
- color: @wcfDimmedColor;
- }
- }
-
- .messageGroupInfo {
- .inlineList;
-
- font-size: @wcfSmallFontSize;
-
- > li:not(:last-child) {
- margin-right: 5px;
-
- &:after {
- content: " - ";
- }
- }
- }
-}
--- /dev/null
+.messageList {
+ border: 1px solid $wcfContentBackgroundAccent;
+ border-width: 1px 0;
+ margin: 30px 0;
+
+ > li {
+ margin-top: 0;
+ padding: 40px 0;
+
+ &:not(:first-child) {
+ border-top: 1px solid $wcfContentBackgroundAccent;
+ }
+ }
+}
+
+.message {
+ display: flex;
+}
+
+/* sidebar */
+.messageSidebar {
+ align-self: flex-start;
+ background-color: $wcfContentBackgroundAccent;
+ border-radius: 3px;
+ color: $wcfContentTextAccent;
+ flex: 0 0 170px;
+ padding: 20px;
+ position: relative;
+ text-align: center;
+
+ a {
+ color: $wcfContentLinkAccent;
+
+ &:hover {
+ color: $wcfContentLinkAccentActive;
+ }
+ }
+
+ .dataList {
+ font-size: .85rem;
+ }
+}
+
+.messageAuthor + * {
+ margin-top: 25px;
+
+ &:before {
+ border-top: 2px solid $wcfContentBorderAccent;
+ content: "";
+ left: 0;
+ margin-top: -10px;
+ position: absolute;
+ right: 0;
+ }
+}
+
+.messageAuthorContainer:not(:last-child) {
+ margin-bottom: 5px;
+}
+
+/* content */
+.messageContent {
+ display: flex;
+ flex: 1 auto;
+ flex-direction: column;
+ margin-left: 30px;
+}
+
+/* content - header */
+.messageHeader {
+ display: flex;
+ flex: 0 0 auto;
+ margin-bottom: 20px;
+}
+
+.messageQuickOptions {
+ @extend .inlineList;
+
+ flex: 0 0 auto;
+ opacity: .3;
+ order: 2;
+ transition: opacity .2s linear;
+
+ .badge {
+ color: rgb(255, 255, 255);
+ font-size: .85rem;
+ }
+}
+
+.message:hover .messageQuickOptions {
+ opacity: .6;
+}
+
+.message .messageHeader .messageQuickOptions:hover {
+ opacity: 1;
+}
+
+.messageHeadline {
+ flex: 1 auto;
+ order: 1;
+
+ > h1 {
+ font-family: "Segoe UI Light";
+ font-size: 1.4rem;
+ margin-bottom: 5px;
+ }
+
+ > p {
+ font-size: .85rem;
+ }
+}
+
+/* content - body */
+.messageBody {
+ flex: 1 auto;
+
+ &.editor {
+ align-items: center;
+ display: flex;
+ justify-content: center;
+
+ > .icon {
+ flex: 0 0 auto;
+ }
+
+ > .editorContainer {
+ flex: 1 auto;
+ }
+ }
+}
+
+/* content - footer */
+.messageFooter {
+ flex: 0 0 auto;
+ margin-top: 20px;
+}
+
+.messageFooterNote {
+ border-left: 2px solid rgb(238, 238, 238);
+ padding-left: 5px;
+
+ &:not(:first-child) {
+ margin-top: 5px;
+ }
+}
+
+.messageSignature {
+ border-top: 1px solid rgb(238, 238, 238);
+ opacity: .6;
+ padding-top: 10px;
+ transition: opacity .2s linear;
+}
+
+.message:hover .messageSignature {
+ opacity: 1;
+}
+
+.messageFooterButtons {
+ @extend .inlineList;
+
+ justify-content: flex-end;
+ margin-top: 10px;
+ opacity: .3;
+ transition: opacity .2s linear;
+
+ &.forceVisible {
+ opacity: 1 !important;
+ }
+
+ > li:not(:last-child) {
+ margin-right: 5px;
+ }
+}
+
+.message:hover {
+ .messageFooterButtons {
+ opacity: .6;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+}
+
+/* ### message groups ### */
+.messageGroupList {
+ .columnSubject {
+ > .labelList {
+ float: right;
+ padding-left: 7px;
+ }
+
+ > h3 {
+ > .messageGroupLink {
+ font-size: $wcfTitleFontSize;
+ }
+
+ > .badge.label {
+ top: -2px;
+ }
+ }
+
+ > small {
+ display: block;
+ }
+
+ > nav {
+ font-size: $wcfSmallFontSize;
+
+ > ul > li {
+ display: inline;
+ }
+ }
+ }
+
+ tr {
+ &.new .columnSubject > h3 > .messageGroupLink {
+ font-weight: bold;
+ }
+
+ &.new .columnAvatar div > p > img,
+ &:hover .columnAvatar div > p > img {
+ opacity: 1;
+ }
+
+ &.messageDisabled {
+ color: $wcfDisabledColor;
+
+ > td {
+ background-color: $wcfDisabledBackgroundColor !important;
+ }
+
+ a:not(.badge) {
+ color: $wcfDisabledColor;
+ }
+ }
+
+ &.messageDeleted {
+ color: $wcfDeletedColor;
+
+ > td {
+ background-color: $wcfDeletedBackgroundColor !important;
+ }
+
+ a:not(.badge) {
+ color: $wcfDeletedColor;
+ }
+ }
+
+ .columnSubject .statusDisplay .pageNavigation {
+ opacity: 0;
+ transition: opacity .2s linear;
+ }
+
+ &:hover .columnSubject .statusDisplay .pageNavigation {
+ opacity: 1;
+ }
+
+ &.new .columnAvatar > div {
+ &::after {
+ color: $wcfLinkColor;
+ content: "\f069";
+ font-family: FontAwesome;
+ font-weight: normal !important;
+ font-style: normal !important;
+ font-size: 14px;
+ position: absolute;
+ text-decoration: none !important;
+ top: -4px;
+ right: -2px;
+
+ @include textShadow($wcfContainerBackgroundColor);
+ }
+ }
+ }
+
+ .columnAvatar {
+ div {
+ position: relative;
+ width: 40px;
+ height: 38px;
+
+ > p > img {
+ opacity: .6;
+ transition: opacity .2s linear;
+ }
+ }
+
+ .myAvatar {
+ position: absolute;
+ width: 16px;
+ height: 16px;
+ bottom: -2px;
+ left: 24px;
+ opacity: 1;
+
+ > img {
+ @include boxShadow(0, 0, rgba(0, 0, 0, .3), 3px);
+ }
+ }
+ }
+
+ .columnLastPost {
+ white-space: nowrap;
+ word-wrap: normal;
+
+ > div > div > small {
+ color: $wcfDimmedColor;
+ }
+ }
+
+ .messageGroupInfo {
+ @extend .inlineList;
+
+ font-size: $wcfSmallFontSize;
+
+ > li:not(:last-child) {
+ margin-right: 5px;
+
+ &:after {
+ content: " - ";
+ }
+ }
+ }
+}