View source
<?php
require_once 'SassFile.php';
require_once 'SassException.php';
require_once 'tree/SassNode.php';
class SassParser {
const CACHE = true;
const CACHE_LOCATION = './sass-cache';
const CSS_LOCATION = './css';
const TEMPLATE_LOCATION = './sass-templates';
const BEGIN_COMMENT = '/';
const BEGIN_CSS_COMMENT = '/*';
const END_CSS_COMMENT = '*/';
const BEGIN_SASS_COMMENT = '//';
const BEGIN_INTERPOLATION = '#';
const BEGIN_INTERPOLATION_BLOCK = '#{';
const BEGIN_BLOCK = '{';
const END_BLOCK = '}';
const END_STATEMENT = ';';
const DOUBLE_QUOTE = '"';
const SINGLE_QUOTE = "'";
public static $instance;
private $indentChar;
private $indentChars = array(
' ',
"\t",
);
private $indentSpaces = 2;
private $source;
private $basepath;
private $cache;
private $cache_location;
private $css_location;
private $debug_info;
protected $extensions;
protected $filename;
public static $functions;
private $line;
private $line_numbers;
private $load_paths;
private $load_path_functions;
private $property_syntax;
private $quiet;
private $style;
private $syntax;
private $template_location;
private $vendor_properties = array();
private $debug = FALSE;
private $_vendorProperties = array(
'border-radius' => array(
'-moz-border-radius',
'-webkit-border-radius',
'-khtml-border-radius',
),
'border-top-right-radius' => array(
'-moz-border-radius-topright',
'-webkit-border-top-right-radius',
'-khtml-border-top-right-radius',
),
'border-bottom-right-radius' => array(
'-moz-border-radius-bottomright',
'-webkit-border-bottom-right-radius',
'-khtml-border-bottom-right-radius',
),
'border-bottom-left-radius' => array(
'-moz-border-radius-bottomleft',
'-webkit-border-bottom-left-radius',
'-khtml-border-bottom-left-radius',
),
'border-top-left-radius' => array(
'-moz-border-radius-topleft',
'-webkit-border-top-left-radius',
'-khtml-border-top-left-radius',
),
'box-shadow' => array(
'-moz-box-shadow',
'-webkit-box-shadow',
),
'box-sizing' => array(
'-moz-box-sizing',
'-webkit-box-sizing',
),
'opacity' => array(
'-moz-opacity',
'-webkit-opacity',
'-khtml-opacity',
),
);
public function __construct($options = array()) {
if (!is_array($options)) {
if (isset($options['debug']) && $options['debug']) {
throw new SassException('Options must be an array');
}
$options = count((array) $options) ? (array) $options : array();
}
if (!empty($options['vendor_properties'])) {
if ($options['vendor_properties'] === true) {
$this->vendor_properties = $this->_vendorProperties;
}
elseif (is_array($options['vendor_properties'])) {
$this->vendor_properties = array_merge($this->_vendorProperties, $options['vendor_properties']);
}
}
unset($options['language'], $options['vendor_properties']);
$basepath = $_SERVER['PHP_SELF'];
$basepath = substr($basepath, 0, strrpos($basepath, '/') + 1);
$defaultOptions = array(
'basepath' => $basepath,
'cache' => self::CACHE,
'cache_location' => dirname(__FILE__) . DIRECTORY_SEPARATOR . self::CACHE_LOCATION,
'css_location' => dirname(__FILE__) . DIRECTORY_SEPARATOR . self::CSS_LOCATION,
'debug_info' => FALSE,
'filename' => array(
'dirname' => '',
'basename' => '',
),
'functions' => array(),
'load_paths' => array(),
'load_path_functions' => array(),
'line' => 1,
'line_numbers' => FALSE,
'style' => SassRenderer::STYLE_NESTED,
'syntax' => SassFile::SASS,
'debug' => FALSE,
);
$options = array_merge($defaultOptions, $options);
self::$functions = $options['functions'];
unset($options['functions']);
foreach ($options as $name => $value) {
if (property_exists($this, $name)) {
$this->{$name} = $value;
}
}
self::$instance = $this;
$GLOBALS['SassParser_debug'] = $this->debug;
}
public function __get($name) {
$getter = 'get' . ucfirst($name);
if (method_exists($this, $getter)) {
return $this
->{$getter}();
}
if (property_exists($this, $name)) {
return $this->{$name};
}
if ($this->debug) {
throw new SassException('No getter function for ' . $name);
}
}
public function getBasepath() {
return $this->basepath;
}
public function getCache() {
return $this->cache;
}
public function getCache_location() {
return $this->cache_location;
}
public function getCss_location() {
return $this->css_location;
}
public function getDebug_info() {
return $this->debug_info;
}
public function getFilename() {
return $this->filename;
}
public function getLine() {
return $this->line;
}
public function getSource() {
return $this->source;
}
public function getLine_numbers() {
return $this->line_numbers;
}
public function getFunctions() {
return self::$functions;
}
public function getLoad_paths() {
return $this->load_paths;
}
public function getLoad_path_functions() {
return $this->load_path_functions;
}
public function getProperty_syntax() {
return $this->property_syntax;
}
public function getQuiet() {
return $this->quiet;
}
public function getStyle() {
return $this->style;
}
public function getSyntax() {
return $this->syntax;
}
public function getTemplate_location() {
return $this->template_location;
}
public function getVendor_properties() {
return $this->vendor_properties;
}
public function getDebug() {
return $this->debug;
}
public function getOptions() {
return array(
'cache' => $this->cache,
'cache_location' => $this->cache_location,
'css_location' => $this->css_location,
'filename' => $this->filename,
'functions' => $this->functions,
'line' => $this->line,
'line_numbers' => $this->line_numbers,
'load_paths' => $this->load_paths,
'load_path_functions' => $this->load_path_functions,
'property_syntax' => $this->property_syntax,
'quiet' => $this->quiet,
'style' => $this->style,
'syntax' => $this->syntax,
'template_location' => $this->template_location,
'vendor_properties' => $this->vendor_properties,
'debug' => $this->debug,
);
}
public function toCss($source, $isFile = true) {
return $this
->parse($source, $isFile)
->render();
}
public function parse($source, $isFile = true) {
if (!$source) {
return $this
->toTree($source);
}
if (is_array($source)) {
$return = array();
foreach ($source as $source_file) {
$return = array_merge($return, $this
->parse($source_file, TRUE));
}
return $return;
}
if ($isFile && ($files = SassFile::get_file($source, $this))) {
$files_source = '';
foreach ($files as $file) {
$this->filename = $file;
$this->syntax = substr($this->filename, -4);
if ($this->syntax !== SassFile::SASS && $this->syntax !== SassFile::SCSS) {
if ($this->debug) {
throw new SassException('Invalid {what}', array(
'{what}' => 'syntax option',
));
}
return FALSE;
}
if ($this->cache) {
$cached = SassFile::get_cached_file($this->filename, $this->cache_location);
if ($cached !== false) {
return $cached;
}
}
$contents = file_get_contents($this->filename);
SassFile::$parser = $this;
SassFile::$path = $this->filename;
$contents = preg_replace_callback('/url\\(\\s*[\'"]?(?![a-z]+:|\\/+)([^\'")]+)[\'"]?\\s*\\)/i', 'SassFile::resolve_paths', $contents);
$files_source .= $contents;
if ($this->cache) {
SassFile::set_cached_file($tree, $filename, $this->cache_location);
}
}
return $this
->toTree($files_source);
}
else {
return $this
->toTree($source);
}
}
private function toTree($source) {
if ($this->syntax === SassFile::SASS) {
$source = str_replace(array(
"\r\n",
"\n\r",
"\r",
), "\n", $source);
$this->source = explode("\n", $source);
$this
->setIndentChar();
}
else {
$this->source = $source;
}
unset($source);
$root = new SassRootNode($this);
$this
->buildTree($root);
return $root;
}
private function buildTree($parent) {
$node = $this
->getNode($parent);
while (is_object($node) && $node
->isChildOf($parent)) {
$parent
->addChild($node);
$node = $this
->buildTree($node);
}
return $node;
}
private function getNode($node) {
$token = $this
->getToken();
if (empty($token)) {
return null;
}
switch (true) {
case SassDirectiveNode::isa($token):
return $this
->parseDirective($token, $node);
case SassCommentNode::isa($token):
return new SassCommentNode($token);
case SassVariableNode::isa($token):
return new SassVariableNode($token);
case SassPropertyNode::isa(array(
'token' => $token,
'syntax' => $this->property_syntax,
)):
return new SassPropertyNode($token, $this->property_syntax);
case SassFunctionDefinitionNode::isa($token):
return new SassFunctionDefinitionNode($token);
case SassMixinDefinitionNode::isa($token):
if ($this->syntax === SassFile::SCSS) {
if ($this->debug) {
throw new SassException('Mixin definition shortcut not allowed in SCSS', $this);
}
return;
}
else {
return new SassMixinDefinitionNode($token);
}
case SassMixinNode::isa($token):
if ($this->syntax === SassFile::SCSS) {
if ($this->debug) {
throw new SassException('Mixin include shortcut not allowed in SCSS', $this);
}
return;
}
else {
return new SassMixinNode($token);
}
default:
return new SassRuleNode($token);
break;
}
}
private function getToken() {
return $this->syntax === SassFile::SASS ? $this
->sass2Token() : $this
->scss2Token();
}
private function sass2Token() {
$statement = '';
$token = null;
while (is_null($token) && !empty($this->source)) {
while (empty($statement) && !empty($this->source)) {
$source = array_shift($this->source);
$statement = trim($source);
$this->line++;
}
if (empty($statement)) {
break;
}
$level = $this
->getLevel($source);
if ($statement[0] === self::BEGIN_COMMENT) {
if (substr($statement, 0, strlen(self::BEGIN_SASS_COMMENT)) === self::BEGIN_SASS_COMMENT) {
unset($statement);
while ($this
->getLevel($this->source[0]) > $level) {
array_shift($this->source);
$this->line++;
}
continue;
}
elseif (substr($statement, 0, strlen(self::BEGIN_CSS_COMMENT)) === self::BEGIN_CSS_COMMENT) {
while ($this
->getLevel($this->source[0]) > $level) {
$statement .= "\n" . ltrim(array_shift($this->source));
$this->line++;
}
}
else {
$this->source = $statement;
if ($this->debug) {
throw new SassException('Illegal comment type', $this);
}
}
}
elseif (substr($statement, -1) === SassRuleNode::CONTINUED) {
while ($this
->getLevel($this->source[0]) === $level) {
$statement .= ltrim(array_shift($this->source));
$this->line++;
}
}
$token = (object) array(
'source' => $statement,
'level' => $level,
'filename' => $this->filename,
'line' => $this->line - 1,
);
}
return $token;
}
private function getLevel($source) {
$indent = strlen($source) - strlen(ltrim($source));
$level = $indent / $this->indentSpaces;
if (is_float($level)) {
$level = (int) ceil($level);
}
if (!is_int($level) || preg_match("/[^{$this->indentChar}]/", substr($source, 0, $indent))) {
$this->source = $source;
if ($this->debug) {
throw new SassException('Invalid indentation', $this);
}
else {
return 0;
}
}
return $level;
}
private function scss2Token() {
static $srcpos = 0;
static $srclen;
$statement = '';
$token = null;
if (empty($srclen)) {
$srclen = strlen($this->source);
}
while (is_null($token) && $srcpos < strlen($this->source)) {
$c = $this->source[$srcpos++];
switch ($c) {
case self::BEGIN_COMMENT:
if (substr($this->source, $srcpos - 1, strlen(self::BEGIN_SASS_COMMENT)) === self::BEGIN_SASS_COMMENT) {
while ($this->source[$srcpos++] !== "\n") {
}
$statement .= "\n";
}
elseif (substr($this->source, $srcpos - 1, strlen(self::BEGIN_CSS_COMMENT)) === self::BEGIN_CSS_COMMENT) {
if (ltrim($statement)) {
if ($this->debug) {
throw new SassException('Invalid comment', (object) array(
'source' => $statement,
'filename' => $this->filename,
'line' => $this->line,
));
}
}
$statement .= $c . $this->source[$srcpos++];
while (substr($this->source, $srcpos, strlen(self::END_CSS_COMMENT)) !== self::END_CSS_COMMENT) {
$statement .= $this->source[$srcpos++];
}
$srcpos += strlen(self::END_CSS_COMMENT);
$token = $this
->createToken($statement . self::END_CSS_COMMENT);
}
else {
$statement .= $c;
}
break;
case self::DOUBLE_QUOTE:
case self::SINGLE_QUOTE:
$statement .= $c;
while ($this->source[$srcpos] !== $c) {
$statement .= $this->source[$srcpos++];
}
$statement .= $this->source[$srcpos++];
break;
case self::BEGIN_INTERPOLATION:
$statement .= $c;
if (substr($this->source, $srcpos - 1, strlen(self::BEGIN_INTERPOLATION_BLOCK)) === self::BEGIN_INTERPOLATION_BLOCK) {
while ($this->source[$srcpos] !== self::END_BLOCK) {
$statement .= $this->source[$srcpos++];
}
$statement .= $this->source[$srcpos++];
}
break;
case self::BEGIN_BLOCK:
case self::END_BLOCK:
case self::END_STATEMENT:
$token = $this
->createToken($statement . $c);
if (is_null($token)) {
$statement = '';
}
break;
default:
$statement .= $c;
break;
}
}
if (is_null($token)) {
$srclen = $srcpos = 0;
}
return $token;
}
private function createToken($statement) {
static $level = 0;
$this->line += substr_count($statement, "\n");
$statement = trim($statement);
if (substr($statement, 0, strlen(self::BEGIN_CSS_COMMENT)) !== self::BEGIN_CSS_COMMENT) {
$statement = str_replace(array(
"\n",
"\r",
), '', $statement);
}
$last = substr($statement, -1);
$statement = rtrim($statement, ' ' . self::BEGIN_BLOCK . self::END_STATEMENT);
$statement = preg_match('/#\\{.+?\\}$/i', $statement) ? $statement : rtrim($statement, self::END_BLOCK);
$token = $statement ? (object) array(
'source' => $statement,
'level' => $level,
'filename' => $this->filename,
'line' => $this->line,
) : null;
$level += $last === self::BEGIN_BLOCK ? 1 : ($last === self::END_BLOCK ? -1 : 0);
return $token;
}
private function parseDirective($token, $parent) {
switch (SassDirectiveNode::extractDirective($token)) {
case '@extend':
return new SassExtendNode($token);
break;
case '@function':
return new SassFunctionDefinitionNode($token);
break;
case '@return':
return new SassReturnNode($token);
break;
case '@mixin':
return new SassMixinDefinitionNode($token);
break;
case '@include':
return new SassMixinNode($token);
break;
case '@import':
if ($this->syntax == SassFile::SASS) {
$i = 0;
$source = '';
while (!empty($this->source) && empty($source)) {
$source = $this->source[$i++];
}
if (!empty($source) && $this
->getLevel($source) > $token->level) {
if ($this->debug) {
throw new SassException('Nesting not allowed beneath @import directive', $token);
}
}
}
return new SassImportNode($token);
break;
case '@each':
return new SassEachNode($token);
break;
case '@for':
return new SassForNode($token);
break;
case '@if':
return new SassIfNode($token);
break;
case '@else':
return new SassElseNode($token);
break;
case '@do':
case '@while':
return new SassWhileNode($token);
break;
case '@debug':
return new SassDebugNode($token);
break;
case '@warn':
return new SassDebugNode($token, true);
break;
default:
return new SassDirectiveNode($token);
break;
}
}
private function setIndentChar() {
foreach ($this->source as $l => $source) {
if (!empty($source) && in_array($source[0], $this->indentChars)) {
$this->indentChar = $source[0];
for ($i = 0, $len = strlen($source); $i < $len && $source[$i] == $this->indentChar; $i++) {
}
if ($i < $len && in_array($source[$i], $this->indentChars)) {
$this->line = ++$l;
$this->source = $source;
if ($this->debug) {
throw new SassException('Mixed indentation not allowed', $this);
}
}
$this->indentSpaces = $this->indentChar == ' ' ? $i : 1;
return;
}
}
$this->indentChar = ' ';
$this->indentSpaces = 2;
}
}