Blame |
Last modification |
View Log
| RSS feed
1
<?php
namespace de\pointedears\css\least
;
require_once __DIR__
. '/../../string/parser/Lexer.php';
require_once __DIR__
. '/../../string/parser/Parser.php';
/**
* Parses a LEAST stylesheet into a CSS stylesheet
*
* @author Thomas 'PointedEars' Lahn
* @property-read string $compiled
*/
class Parser
extends \de\pointedears\string\parser\Parser
{
/**
* CSS stylesheet code compiled from the LEAST stylesheet
*
* @var string
*/
protected $_compiled;
/**
* Array of arrays of template variables, one inner array for
* each scope level.
* @var array[array]
*/
protected $_vars = array(array());
/**
* The nesting level of the current variable scope.
*
* Used to hold variable definitions per nesting level.
*
* @var int
*/
protected $_scope_level = 0;
/**
* Cache for lookup results.
*
* The lookup cache improves parser performance with
* nested scopes. By contrast to <code>$_vars</code>, it is
* a _shallow_ storage for template variable definitions and
* represents the current _certain_ variable knowledge of the
* parser, i.e. variables that have been defined in outer scopes
* and the same scope, and whose values have been looked up in
* <code>$_vars</code> before.
*
* The lookup cache is replaced by the variables of the outer
* scope when a scope is exited because definitions in the
* outer scope may have been shadowed by the inner scope.
* Note that definitions from the second-next, third-next
* aso. scopes need to be looked up again then.
*
* @var array
*/
protected $_lookup_cache = array();
/**
* Mixins
* @var array
*/
protected $_mixins = array();
/**
* <code>true</code> if the parser is parsing a mix-in
* @var boolean
*/
protected $_in_mixin = false;
/**
* The currently parsed mix-in
* @var Mixin
*/
protected $_current_mixin;
/**
* The nesting level in the currently parsed mix-in, 0 again when
* it just ended
* @var int
*/
protected $_mixin_level = 0;
public function __construct
($code)
{
$lexer = new \de\pointedears\string\parser\Lexer
($code);
$nl = 'n';
$escape = '\\[0-9a-f]{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-f]';
$lexer->addTokens(array(
'(?P<PHP><\?php.*?\?>)',
"/\*[^*]*\*+([^/*][^*]*\*+)*/",
"\"([^\n\r\f\\\"]|\\{$nl}|{$escape})*\"",
"'([^\n\r\f\\']|\\{$nl}|{$escape})*'",
// '\.(?P<mixin_def>[\w-]+)\s*\((?P<params>.*?)\)\s*(?!;)',
// '\.(?P<mixin_call>[\w-]+)\s*\((?P<args>.*)?\)\s*;\\s*',
'[{}]',
// '\$(?P<name>\w+)\s*=\s*(?P<value>\'.+?\'|".+"|.+?)\s*;\s*',
// '\$(?P<ref>\w+)'
));
$lexer->ignoreCase = true;
$lexer->dotAll = true;
parent
::__construct
($lexer);
$this->_compiled
= $code;
}
protected function defineVar
($name, $value)
{
$scope_level = $this->_scope_level
;
if (defined('DEBUG') && DEBUG
> 0)
{
echo "<?php // Least.php: Found definition \${$name} = `{$value}'"
. " at scope level {$scope_level} ?>\n";
}
$this->_vars
[$scope_level][$name] = $value;
$this->_lookup_cache
[$name] = $value;
}
/**
* Resolves a template variable reference against the template scope chain
* @param string $ref
* @return mixed
*/
protected function resolveVar
($name)
{
$scope_level = $this->_scope_level
+ 1;
$orig_level = $scope_level - 1;
if (defined('DEBUG') && DEBUG
> 0)
{
echo "<?php // Least.php: Found reference \${$name} at scope level "
. ($scope_level - 1) . " ?>\n";
}
/* Try the lookup cache for this scope level first */
if (array_key_exists($name, $this->_lookup_cache
))
{
$value = $this->_lookup_cache
[$name];
if (defined('DEBUG') && DEBUG
> 1)
{
echo "<?php // Least.php: Resolved reference \${$name}"
. " at scope level {$orig_level}"
. " from lookup cache, value `{$value}' ?>\n";
}
return $value;
}
while ($scope_level--)
{
if (array_key_exists($name, $this->_vars
[$scope_level]))
{
$value = $this->_vars
[$scope_level][$name];
$this->_lookup_cache
[$name] = $value;
if (defined('DEBUG') && DEBUG
> 1)
{
echo "<?php // Least.php: Resolved reference \${$name}"
. " from scope level {$orig_level}"
. " at scope level {$scope_level}, value `{$value}' ?>\n";
}
return $value;
}
}
if (defined('DEBUG') && DEBUG
> 0)
{
echo "<?php // Least.php: WARNING: Unresolved reference \${$name}"
. " at scope level {$orig_level}. Variable not in scope? ?>\n";
}
}
protected function defineMixin
($name, $params)
{
$scope_level = $this->_scope_level
;
if (defined('DEBUG') && DEBUG
> 0)
{
echo "<?php // Least.php: Found mixin definition {$name}($params)"
. " at scope level {$scope_level}. ?>\n";
}
$mixin = new Mixin
($params);
$this->_mixins
[$name] = $mixin;
return $mixin;
}
protected function callMixin
($name, $args)
{
return $this->$_mixins[$name]->apply($args);
}
public function parseToken
($matches)
{
ini_set('html_errors', 0);
// var_dump($matches);
// /* Get match offset and match length */
// $match = $matches[0];
// $match_start = $match[1];
// $match_length = mb_strlen($match[0]);
// /* Transform $matches to the format it is usually set as (without PREG_OFFSET_CAPTURE set) */
// $matches = array_map(function ($m) {
// return $m[0];
// }, $matches);
// $match = $matches[0];
// if (defined('DEBUG') && DEBUG > 1)
// {
// echo print_r(array(
// 'match_start' => $match_start,
// 'match_length' => $match_length,
// 'matches' => $matches,
// 'in_mixin' => $in_mixin,
// 'scope_level' => $this->_scope_level,
// ), true) . "\n";
// }
// if (isset($matches['mixin_def']) && $matches['mixin_def'])
// {
// $this->_in_mixin = true;
// $this->_current_mixin = $this->defineMixin($matches['mixin_def'], $matches['params']);
// // $code = self::replace(mb_strlen($match), '', $code, $match_start);
// // $match_length = 0;
// }
// else if (isset($matches['mixin_call']) && $matches['mixin_call'])
// {
// //return
// $this->callMixin($matches['mixin_call'], $matches['args']);
// //$code = self::replace(mb_strlen($match), '', $code, $match_start);
// //$match_length = 0;
// $mixin_start = $match_start;
// }
// else if ($match === '{')
// {
// ++$this->_scope_level;
// $this->_vars[$this->_scope_level] = array();
// if ($this->_in_mixin)
// {
// if ($mixin_body_start === 0)
// {
// $mixin_body_start = $match_start;
// }
// ++$this->_mixin_level;
// }
// }
// else if ($match === '}')
// {
// --$this->_scope_level;
// $this->_lookup_cache = $this->_vars[$this->_scope_level];
// if ($this->_in_mixin)
// {
// --$this->_mixin_level;
// if ($this->_mixin_level === 0)
// {
// $this->_in_mixin = false;
// $this->_current_mixin->setBody(substr($code, $mixin_start + 1, $match_start - $mixin_start - 1));
// echo '"'.print_r(substr($code, $mixin_start, $match_start - $mixin_start + 1), true) . "\"\n";
// // $code = self::replace($match_start - $mixin_start + 1, '', $code, $mixin_start);
// // $match_length = 0;
// }
// }
// }
// else if (isset($matches['name']) && $matches['name'])
// {
// $this->defineVar($matches['name'], $matches['value']);
// $code = self::replace(mb_strlen($match), '', $code, $match_start);
// $match_length = 0;
// }
// else if (isset($matches['ref']) && $matches['ref'])
// {
// $name = $matches['ref'];
// if (!$in_mixin || !in_array($name, $mixin->params, true))
// {
// $value = $this->resolveVar($matches['ref']);
// $this->_lexer->text = self::replace(mb_strlen($match), $value, $code, $match_start);
// $this->_lexer->offset -= $match_length + mb_strlen($value);
// }
// }
}
public function getCompiled
()
{
return $this->_compiled
;
}
}
class Mixin
extends \de\pointedears\Base
{
/**
* Parameters of the mixin
* @var array
*/
protected $_params;
/**
* Body of the mixin
* @var string
*/
protected $_body = '';
/**
* @param string $params
*/
public function __construct
($params, $body = '')
{
$this->_params
= is_array($params)
?
$params
: array_map(
function ($e) {
return preg_replace('/^\$/', '', $e);
},
preg_split('/\s*,\s*/', $params));
if ($body !== '')
{
$this->setBody($body);
}
}
public function setBody
($body)
{
$this->body = (string
) $body;
}
/**
* @param string $arguments
*/
public function apply
($arguments)
{
return '';
}
}