1
0
Fork 0
mirror of https://github.com/Oreolek/kohana-less.git synced 2024-07-08 17:34:26 +03:00
kohana-less/vendor/lessphp/lessc.inc.php
2010-05-05 21:33:28 +08:00

1313 lines
32 KiB
PHP

<?php
/**
* lessphp v0.2.0
* http://leafo.net/lessphp
*
* LESS Css compiler, adapted from http://lesscss.org/docs.html
*
* Copyright 2010, Leaf Corcoran <leafot@gmail.com>
* Licensed under MIT or GPLv3, see LICENSE
*/
//
// investigate trouble with ^M
// fix the alpha value with color when using a percent
//
class lessc {
private $buffer;
private $count;
private $line;
private $expandStack;
private $env = array();
public $vPrefix = '@';
public $mPrefix = '$';
public $imPrefix = '!';
public $selfSelector = '&';
static private $precedence = array(
'+' => 0,
'-' => 0,
'*' => 1,
'/' => 1,
'%' => 1,
);
static private $operatorString; // regex string to match any of the operators
static private $dtypes = array('expression', 'variable', 'function', 'negative'); // types with delayed computation
static private $units = array(
'px', '%', 'in', 'cm', 'mm', 'em', 'ex', 'pt', 'pc', 'ms', 's', 'deg');
public $importDisabled = false;
public $importDir = '';
// compile chunk off the head of buffer
function chunk() {
if (empty($this->buffer)) return false;
$s = $this->seek();
// a property
if ($this->keyword($key) && $this->assign() && $this->propertyValue($value) && $this->end()) {
// look for important prefix
if ($key{0} == $this->imPrefix && strlen($key) > 1) {
$key = substr($key, 1);
if ($value[0] == 'list' && $value[1] == ' ') {
$value[2][] = array('keyword', '!important');
} else {
$value = array('list', ' ', array($value, array('keyword', '!important')));
}
}
$this->append($key, $value);
if (count($this->env) == 1)
return $this->compileProperty($key, array($value))."\n";
else
return true;
} else {
$this->seek($s);
}
// look for special css @ directives
if (count($this->env) == 1 && $this->count < strlen($this->buffer) && $this->buffer[$this->count] == '@') {
// a font-face block
if ($this->literal('@font-face') && $this->literal('{')) {
$this->push();
$this->set('__tags', array('@font-face'));
$this->set('__dontsave', true);
return true;
} else {
$this->seek($s);
}
// charset
if ($this->literal('@charset') && $this->propertyValue($value) && $this->end()) {
return "@charset ".$this->compileValue($value).";\n";
} else {
$this->seek($s);
}
}
// opening abstract block
if ($this->tag($tag, true) && $this->argumentDef($args) && $this->literal('{')) {
$this->push();
// move out of variable scope
if ($tag{0} == $this->vPrefix) $tag[0] = $this->mPrefix;
$this->set('__tags', array($tag));
if (isset($args)) $this->set('__args', $args);
return true;
} else {
$this->seek($s);
}
// opening css block
if ($this->tags($tags) && $this->literal('{')) {
// move @ tags out of variable namespace!
foreach($tags as &$tag) {
if ($tag{0} == $this->vPrefix) $tag[0] = $this->mPrefix;
}
$this->push();
$this->set('__tags', $tags);
return true;
} else {
$this->seek($s);
}
// closing block
if ($this->literal('}')) {
$tags = $this->multiplyTags();
$env = end($this->env);
$ctags = $env['__tags'];
unset($env['__tags']);
// insert the default arguments
if (isset($env['__args'])) {
foreach ($env['__args'] as $arg) {
if (isset($arg[1])) {
$this->prepend($this->vPrefix.$arg[0], $arg[1]);
}
}
}
if (!empty($tags))
$out = $this->compileBlock($tags, $env);
$this->pop();
// make the block(s) available in the new current scope
if (!isset($env['__dontsave'])) {
foreach ($ctags as $t) {
// if the block already exists then merge
if ($this->get($t, array(end($this->env)))) {
$this->merge($t, $env);
} else {
$this->set($t, $env);
}
}
}
return isset($out) ? $out : true;
}
// import statement
if ($this->import($url, $media)) {
if ($this->importDisabled) return "/* import is disabled */\n";
$full = $this->importDir.$url;
if (file_exists($file = $full) || file_exists($file = $full.'.less')) {
$loaded = $this->removeComments(ltrim(file_get_contents($file).";"));
$this->buffer = substr($this->buffer, 0, $this->count).$loaded.substr($this->buffer, $this->count);
return true;
}
return '@import url("'.$url.'")'.($media ? ' '.$media : '').";\n";
}
// setting variable
if ($this->variable($name) && $this->assign() && $this->propertyValue($value) && $this->end()) {
$this->append($this->vPrefix.$name, $value);
return true;
} else {
$this->seek($s);
}
// mixin/function expand
if ($this->tags($tags, true, '>') && ($this->argumentValues($argv) || true) && $this->end()) {
$env = $this->getEnv($tags);
if ($env == null) return true;
// if we have arguments then insert them
if (!empty($env['__args'])) {
foreach($env['__args'] as $arg) {
$vname = $this->vPrefix.$arg[0];
$value = is_array($argv) ? array_shift($argv) : null;
// copy default value if there isn't one supplied
if ($value == null && isset($arg[1]))
$value = $arg[1];
// if ($value == null) continue; // don't define so it can search up
// create new entry if var doesn't exist in scope
if (isset($env[$vname])) {
array_unshift($env[$vname], $value);
} else {
// new element
$env[$vname] = array($value);
}
}
}
// set all properties
ob_start();
$blocks = array();
foreach ($env as $name => $value) {
// skip the metatdata
if (preg_match('/^__/', $name)) continue;
// if it is a block, remember it to compile after everything
// is mixed in
if (!isset($value[0]))
$blocks[] = array($name, $value);
// copy the data
// don't overwrite previous value, look in current env for name
if ($this->get($name, array(end($this->env)))) {
while ($tval = array_shift($value))
$this->append($name, $tval);
} else
$this->set($name, $value);
}
// render sub blocks
foreach ($blocks as $b) {
$rtags = $this->multiplyTags(array($b[0]));
echo $this->compileBlock($rtags, $b[1]);
}
return ob_get_clean();
} else {
$this->seek($s);
}
// spare ;
if ($this->literal(';')) return true;
return false; // couldn't match anything, throw error
}
// recursively find the cartesian product of all tags in stack
function multiplyTags($tags = array(' '), $d = null) {
if ($d === null) $d = count($this->env) - 1;
$parents = $d == 0 ? $this->env[$d]['__tags']
: $this->multiplyTags($this->env[$d]['__tags'], $d - 1);
$rtags = array();
foreach ($parents as $p) {
foreach ($tags as $t) {
if ($t{0} == $this->mPrefix) continue; // skip functions
$d = ' ';
if ($t{0} == ':' || $t{0} == $this->selfSelector) {
$t = ltrim($t, $this->selfSelector);
$d = '';
}
$rtags[] = trim($p.$d.$t);
}
}
return $rtags;
}
// a list of expressions
function expressionList(&$exps) {
$values = array();
while ($this->expression($exp)) {
$values[] = $exp;
}
if (count($values) == 0) return false;
$exps = $this->compressList($values, ' ');
return true;
}
// a single expression
function expression(&$out) {
$s = $this->seek();
$needWhite = true;
if ($this->literal('(') && $this->expression($exp) && $this->literal(')')) {
$lhs = $exp;
$needWhite = false;
} elseif ($this->seek($s) && $this->value($val)) {
$lhs = $val;
} else {
return false;
}
$out = $this->expHelper($lhs, 0, $needWhite);
return true;
}
// resursively parse infix equation with $lhs at precedence $minP
function expHelper($lhs, $minP, $needWhite = true) {
$ss = $this->seek();
// try to find a valid operator
while ($this->match(self::$operatorString.($needWhite ? '\s+' : ''), $m) && self::$precedence[$m[1]] >= $minP) {
$needWhite = true;
// get rhs
$s = $this->seek();
if ($this->literal('(') && $this->expression($exp) && $this->literal(')')) {
$needWhite = false;
$rhs = $exp;
} elseif ($this->seek($s) && $this->value($val)) {
$rhs = $val;
} else break;
// peek for next operator to see what to do with rhs
if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > $minP) {
$rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
}
// don't evaluate yet if it is dynamic
if (in_array($rhs[0], self::$dtypes) || in_array($lhs[0], self::$dtypes))
$lhs = array('expression', $m[1], $lhs, $rhs);
else
$lhs = $this->evaluate($m[1], $lhs, $rhs);
$ss = $this->seek();
}
$this->seek($ss);
return $lhs;
}
// consume a list of values for a property
function propertyValue(&$value) {
$values = array();
$s = null;
while ($this->expressionList($v)) {
$values[] = $v;
$s = $this->seek();
if (!$this->literal(',')) break;
}
if ($s) $this->seek($s);
if (count($values) == 0) return false;
$value = $this->compressList($values, ', ');
return true;
}
// a single value
function value(&$value) {
// try a unit
if ($this->unit($value)) return true;
// see if there is a negation
$s = $this->seek();
if ($this->literal('-', false) && $this->variable($vname)) {
$value = array('negative', array('variable', $this->vPrefix.$vname));
return true;
} else {
$this->seek($s);
}
// accessor
// must be done before color
// this needs negation too
if ($this->accessor($a)) {
$tmp = $this->getEnv($a[0]);
if ($tmp && isset($tmp[$a[1]]))
$value = end($tmp[$a[1]]);
return true;
}
// color
if ($this->color($value)) return true;
// css function
// must be done after color
if ($this->func($value)) return true;
// string
if ($this->string($tmp, $d)) {
$value = array('string', $d.$tmp.$d);
return true;
}
// try a keyword
if ($this->keyword($word)) {
$value = array('keyword', $word);
return true;
}
// try a variable
if ($this->variable($vname)) {
$value = array('variable', $this->vPrefix.$vname);
return true;
}
return false;
}
// an import statement
function import(&$url, &$media) {
$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->literal('url(')) $parens = true; else $parens = false;
if (!$this->string($url)) {
if ($parens && $this->to(')', $url)) {
$parens = false; // got em
} else {
$this->seek($s);
return false;
}
}
if ($parens && !$this->literal(')')) {
$this->seek($s);
return false;
}
// now the rest is media
return $this->to(';', $media, false, true);
}
// a scoped value accessor
// .hello > @scope1 > @scope2['value'];
function accessor(&$var) {
$s = $this->seek();
if (!$this->tags($scope, true, '>') || !$this->literal('[')) {
$this->seek($s);
return false;
}
// either it is a variable or a property
// why is a property wrapped in quotes, who knows!
if ($this->variable($name)) {
$name = $this->vPrefix.$name;
} elseif($this->literal("'") && $this->keyword($name) && $this->literal("'")) {
// .. $this->count is messed up if we wanted to test another access type
} else {
$this->seek($s);
return false;
}
if (!$this->literal(']')) {
$this->seek($s);
return false;
}
$var = array($scope, $name);
return true;
}
// a string
function string(&$string, &$d = null) {
$s = $this->seek();
if ($this->literal('"', false)) {
$delim = '"';
} else if($this->literal("'", false)) {
$delim = "'";
} else {
return false;
}
if (!$this->to($delim, $string)) {
$this->seek($s);
return false;
}
$d = $delim;
return true;
}
// a numerical unit
function unit(&$unit, $allowed = null) {
$simpleCase = $allowed == null;
if (!$allowed) $allowed = self::$units;
if ($this->match('(-?[0-9]*(\.)?[0-9]+)('.implode('|', $allowed).')?', $m, !$simpleCase)) {
if (!isset($m[3])) $m[3] = 'number';
$unit = array($m[3], $m[1]);
// check for size/height font unit.. should this even be here?
if ($simpleCase) {
$s = $this->seek();
if ($this->literal('/', false) && $this->unit($right, self::$units)) {
$unit = array('keyword', $this->compileValue($unit).'/'.$this->compileValue($right));
} else {
// get rid of whitespace
$this->seek($s);
$this->match('', $_);
}
}
return true;
}
return false;
}
// a # color
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;
}
// consume a list of property values delimited by ; and wrapped in ()
function argumentValues(&$args, $delim = ';') {
$s = $this->seek();
if (!$this->literal('(')) return false;
$values = array();
while ($this->propertyValue($value)) {
$values[] = $value;
if (!$this->literal($delim)) break;
}
if (!$this->literal(')')) {
$this->seek($s);
return false;
}
$args = $values;
return true;
}
// consume an argument definition list surrounded by (), each argument is a variable name with optional value
function argumentDef(&$args, $delim = ';') {
$s = $this->seek();
if (!$this->literal('(')) return false;
$values = array();
while ($this->variable($vname)) {
$arg = array($vname);
if ($this->assign() && $this->propertyValue($value)) {
$arg[] = $value;
// let the : slide if there is no value
}
$values[] = $arg;
if (!$this->literal($delim)) break;
}
if (!$this->literal(')')) {
$this->seek($s);
return false;
}
$args = $values;
return true;
}
// consume a list of tags
// this accepts a hanging delimiter
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;
}
// a single tag
function tag(&$tag, $simple = false) {
if ($simple)
$chars = '^,:;{}\][>\(\) ';
else
$chars = '^,;{}[';
$tag = '';
while ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) {
$tag.= $m[1];
if ($simple) break;
$s = $this->seek();
if ($this->literal('[') && $this->to(']', $c, true) && $this->literal(']')) {
$tag .= '['.$c.'] ';
} else {
$this->seek($s);
break;
}
}
$tag = trim($tag);
if ($tag == '') return false;
return true;
}
// a css function
function func(&$func) {
$s = $this->seek();
if ($this->match('([\w\-_][\w\-_:\.]*)', $m) && $this->literal('(')) {
$fname = $m[1];
if ($fname == 'url') {
$this->to(')', $content, true);
$args = array('string', $content);
} else {
$args = array();
while (true) {
$ss = $this->seek();
if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
$args[] = array('list', '=', array(array('keyword', $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;
}
}
$this->seek($s);
return false;
}
// consume a less variable
function variable(&$name) {
$s = $this->seek();
if ($this->literal($this->vPrefix, false) && $this->keyword($name)) {
return true;
}
return false;
}
// consume an assignment operator
function assign() {
return $this->literal(':') || $this->literal('=');
}
// consume a keyword
function keyword(&$word) {
if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
$word = $m[1];
return true;
}
return false;
}
// consume an end of statement delimiter
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;
}
function compressList($items, $delim) {
if (count($items) == 1) return $items[0];
else return array('list', $delim, $items);
}
function compileBlock($rtags, $env) {
// don't render functions
// todo: this shouldn't need to happen because multiplyTags prunes them, verify
/*
foreach ($rtags as $i => $tag) {
if (preg_match('/( |^)%/', $tag))
unset($rtags[$i]);
}
*/
if (empty($rtags)) return '';
$props = 0;
// print all the visible properties
ob_start();
foreach ($env as $name => $value) {
// todo: change this, poor hack
// make a better name storage system!!! (value types are fine)
// but.. don't render special properties (blocks, vars, metadata)
if (isset($value[0]) && $name{0} != $this->vPrefix && $name != '__args') {
echo $this->compileProperty($name, $value, 1)."\n";
$props++;
}
}
$list = ob_get_clean();
if ($props == 0) return '';
// do some formatting
if ($props == 1) $list = ' '.trim($list).' ';
return implode(", ", $rtags).' {'.($props > 1 ? "\n" : '').
$list."}\n";
}
function compileProperty($name, $value, $level = 0) {
// output all repeated properties
foreach ($value as $v)
$props[] = str_repeat(' ', $level).
$name.':'.$this->compileValue($v).';';
return implode("\n", $props);
}
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 'keyword':
// [1] - the keyword
case 'number':
// [1] - the number
return $value[1];
case 'expression':
// [1] - operator
// [2] - value of left hand side
// [3] - value of right
return $this->compileValue($this->evaluate($value[1], $value[2], $value[3]));
case 'string':
// [1] - contents of string (includes quotes)
// search for inline variables to replace
$replace = array();
if (preg_match_all('/{(@[\w-_][0-9\w-_]*)}/', $value[1], $m)) {
foreach($m[1] as $name) {
if (!isset($replace[$name]))
$replace[$name] = $this->compileValue(array('variable', $name));
}
}
foreach ($replace as $var=>$val) {
// strip quotes
if (preg_match('/^(["\']).*?(\1)$/', $val)) {
$val = substr($val, 1, -1);
}
$value[1] = str_replace('{'.$var.'}', $val, $value[1]);
}
return $value[1];
case 'color':
// [1] - red component (either number for a %)
// [2] - green component
// [3] - blue component
// [4] - optional alpha component
if (count($value) == 5) { // rgba
return 'rgba('.$value[1].','.$value[2].','.$value[3].','.$value[4].')';
}
$out = '#';
foreach (range(1,3) as $i)
$out .= ($value[$i] < 16 ? '0' : '').dechex($value[$i]);
return $out;
case 'variable':
// [1] - the name of the variable including @
$tmp = $this->compileValue(
$this->getVal($value[1], $this->pushName($value[1]))
);
$this->popName();
return $tmp;
case 'negative':
// [1] - some value that needs to become negative
return $this->compileValue($this->reduce($value));
case 'function':
// [1] - function name
// [2] - some value representing arguments
// see if there is a library function for this func
$f = array($this, 'lib_'.$value[1]);
if (is_callable($f)) {
return call_user_func($f, $value[2]);
}
return $value[1].'('.$this->compileValue($value[2]).')';
default: // assumed to be unit
return $value[1].$value[0];
}
}
function lib_quote($arg) {
return '"'.$this->compileValue($arg).'"';
}
function lib_unquote($arg) {
$out = $this->compileValue($arg);
if ($this->quoted($out)) $out = substr($out, 1, -1);
return $out;
}
// is a string surrounded in quotes? returns the quoting char if true
function quoted($s) {
if (preg_match('/^("|\').*?\1$/', $s, $m))
return $m[1];
else return false;
}
// convert rgb, rgba into color type suitable for math
// todo: add hsl
function funcToColor($func) {
$fname = $func[1];
if (!preg_match('/^(rgb|rgba)$/', $fname)) return false;
if ($func[2][0] != 'list') return false; // need a list of arguments
$components = array();
$i = 1;
foreach ($func[2][2] as $c) {
$c = $this->reduce($c);
if ($i < 4) {
if ($c[0] == '%') $components[] = 255 * ($c[1] / 100);
else $components[] = floatval($c[1]);
} elseif ($i == 4) {
if ($c[0] == '%') $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);
}
// reduce a delayed type to its final value
// dereference variables and solve equations
function reduce($var, $defaultValue = array('number', 0)) {
$pushed = 0; // number of variable names pushed
while (in_array($var[0], self::$dtypes)) {
if ($var[0] == 'expression') {
$var = $this->evaluate($var[1], $var[2], $var[3]);
} else if ($var[0] == 'variable') {
$var = $this->getVal($var[1], $this->pushName($var[1]), $defaultValue);
$pushed++;
} else if ($var[0] == 'function') {
$color = $this->funcToColor($var);
if ($color) $var = $color;
break; // no where to go after a function
} else if ($var[0] == 'negative') {
$value = $this->reduce($var[1]);
if (is_numeric($value[1])) {
$value[1] = -1*$value[1];
}
$var = $value;
}
}
while ($pushed != 0) { $this->popName(); $pushed--; }
return $var;
}
// evaluate an expression
function evaluate($op, $left, $right) {
$left = $this->reduce($left);
$right = $this->reduce($right);
if ($left[0] == 'color' && $right[0] == 'color') {
$out = $this->op_color_color($op, $left, $right);
return $out;
}
if ($left[0] == 'color') {
return $this->op_color_number($op, $left, $right);
}
if ($right[0] == 'color') {
return $this->op_number_color($op, $left, $right);
}
// concatenate strings
if ($op == '+' && $left[0] == 'string') {
$append = $this->compileValue($right);
if ($this->quoted($append)) $append = substr($append, 1, -1);
$lhs = $this->compileValue($left);
if ($q = $this->quoted($lhs)) $lhs = substr($lhs, 1, -1);
if (!$q) $q = '';
return array('string', $q.$lhs.$append.$q);
}
if ($left[0] == 'keyword' || $right[0] == 'keyword' ||
$left[0] == 'string' || $right[0] == 'string')
{
// look for negative op
if ($op == '-') $right[1] = '-'.$right[1];
return array('keyword', $this->compileValue($left) .' '. $this->compileValue($right));
}
// default to number operation
return $this->op_number_number($op, $left, $right);
}
// make sure a color's components don't go out of bounds
function fixColor($c) {
foreach (range(1, 3) as $i) {
if ($c[$i] < 0) $c[$i] = 0;
if ($c[$i] > 255) $c[$i] = 255;
$c[$i] = floor($c[$i]);
}
return $c;
}
function op_number_color($op, $lft, $rgt) {
if ($op == '+' || $op = '*') {
return $this->op_color_number($op, $rgt, $lft);
}
}
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]));
}
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) throw new exception("evaluate error: can't divide by zero");
$out[] = $lval / $rval;
break;
default:
throw new exception('evaluate error: color op number failed on op '.$op);
}
}
return $this->fixColor($out);
}
// operator on two numbers
function op_number_number($op, $left, $right) {
if ($right[0] == '%') $right[1] /= 100;
// figure out type
if ($right[0] == 'number' || $right[0] == '%') $type = $left[0];
else $type = $right[0];
$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) throw new exception('parse error: divide by zero');
$value = $left[1] / $right[1];
break;
default:
throw new exception('parse error: unknown number operator: '.$op);
}
return array($type, $value);
}
/* environment functions */
// push name on expand stack, and return its
// count before being pushed
function pushName($name) {
$count = array_count_values($this->expandStack);
$count = isset($count[$name]) ? $count[$name] : 0;
$this->expandStack[] = $name;
return $count;
}
// pop name off expand stack and return it
function popName() {
return array_pop($this->expandStack);
}
// push a new environment
function push() {
$this->level++;
$this->env[] = array();
}
// pop environment off the stack
function pop() {
if ($this->level == 1)
throw new exception('parse error: unexpected end of block');
$this->level--;
return array_pop($this->env);
}
// set something in the current env
function set($name, $value) {
$this->env[count($this->env) - 1][$name] = $value;
}
// append to array in the current env
function append($name, $value) {
$this->env[count($this->env) - 1][$name][] = $value;
}
// put on the front of the value
function prepend($name, $value) {
if (isset($this->env[count($this->env) - 1][$name]))
array_unshift($this->env[count($this->env) - 1][$name], $value);
else $this->append($name, $value);
}
// get the highest occurrence of value
function get($name, $env = null) {
if (empty($env)) $env = $this->env;
for ($i = count($env) - 1; $i >= 0; $i--)
if (isset($env[$i][$name])) return $env[$i][$name];
return null;
}
// get the most recent value of a variable
// return default if it isn't found
// $skip is number of vars to skip
function getVal($name, $skip = 0, $default = array('keyword', '')) {
$val = $this->get($name);
if ($val == null) return $default;
$tmp = $this->env;
while (!isset($tmp[count($tmp) - 1][$name])) array_pop($tmp);
while ($skip > 0) {
$skip--;
if (!empty($val)) {
array_pop($val);
}
if (empty($val)) {
array_pop($tmp);
$val = $this->get($name, $tmp);
}
if (empty($val)) return $default;
}
return end($val);
}
// get the environment described by path, an array of env names
function getEnv($path) {
if (!is_array($path)) $path = array($path);
// move @ tags out of variable namespace
foreach($path as &$tag)
if ($tag{0} == $this->vPrefix) $tag[0] = $this->mPrefix;
$env = $this->get(array_shift($path));
while ($sub = array_shift($path)) {
if (isset($env[$sub])) // todo add a type check for environment
$env = $env[$sub];
else {
$env = null;
break;
}
}
return $env;
}
// merge a block into the current env
function merge($name, $value) {
// if the current block isn't there then just set
$top =& $this->env[count($this->env) - 1];
if (!isset($top[$name])) return $this->set($name, $value);
// copy the block into the old one, including meta data
foreach ($value as $k=>$v) {
// todo: merge property values instead of replacing
// have to check type for this
$top[$name][$k] = $v;
}
}
function literal($what, $eatWhitespace = true) {
// this is here mainly prevent notice from { } string accessor
if ($this->count >= strlen($this->buffer)) return false;
// shortcut on single letter
if (!$eatWhitespace and strlen($what) == 1) {
if ($this->buffer{$this->count} == $what) {
$this->count++;
return true;
}
else return false;
}
return $this->match($this->preg_quote($what), $m, $eatWhitespace);
}
function preg_quote($what) {
return preg_quote($what, '/');
}
// advance counter to next occurrence of $what
// $until - don't include $what in advance
function to($what, &$out, $until = false, $allowNewline = false) {
$validChars = $allowNewline ? "[^\n]" : '.';
if (!$this->match('('.$validChars.'*?)'.$this->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
function match($regex, &$out, $eatWhitespace = true) {
$r = '/'.$regex.($eatWhitespace ? '\s*' : '').'/Ais';
if (preg_match($r, $this->buffer, $out, null, $this->count)) {
$this->count += strlen($out[0]);
return true;
}
return false;
}
// match something without consuming it
function peek($regex, &$out = null) {
$r = '/'.$regex.'/Ais';
$result = preg_match($r, $this->buffer, $out, null, $this->count);
return $result;
}
// seek to a spot in the buffer or return where we are on no argument
function seek($where = null) {
if ($where === null) return $this->count;
else $this->count = $where;
return true;
}
// parse and compile buffer
function parse($str = null) {
if ($str) $this->buffer = $str;
$this->env = array();
$this->expandStack = array();
$this->count = 0;
$this->line = 1;
$this->buffer = $this->removeComments($this->buffer);
$this->push(); // set up global scope
$this->set('__tags', array('')); // equivalent to 1 in tag multiplication
// trim whitespace on head
if (preg_match('/^\s+/', $this->buffer, $m)) {
$this->line += substr_count($m[0], "\n");
$this->buffer = ltrim($this->buffer);
}
$out = '';
while (false !== ($compiled = $this->chunk())) {
if (is_string($compiled)) $out .= $compiled;
}
if ($this->count != strlen($this->buffer)) $this->throwParseError();
if (count($this->env) > 1)
throw new exception('parse error: unclosed block');
// print_r($this->env);
return $out;
}
function throwParseError($msg = 'parse error') {
$line = $this->line + substr_count(substr($this->buffer, 0, $this->count), "\n");
if ($this->peek("(.*?)\n", $m))
throw new exception($msg.': failed at `'.$m[1].'` line: '.$line);
}
function __construct($fname = null) {
if (!self::$operatorString) {
self::$operatorString =
'('.implode('|', array_map(array($this, 'preg_quote'), array_keys(self::$precedence))).')';
}
if ($fname) {
if (!is_file($fname)) {
throw new Exception('load error: failed to find '.$fname);
}
$pi = pathinfo($fname);
$this->fileName = $fname;
$this->importDir = $pi['dirname'].'/';
$this->buffer = file_get_contents($fname);
}
}
// remove comments from $text
// todo: make it work for all functions, not just url
// todo: make it not mess up line counter with block comments
function removeComments($text) {
$out = '';
while (!empty($text) &&
preg_match('/^(.*?)("|\'|\/\/|\/\*|url\(|$)/is', $text, $m))
{
if (!trim($text)) break;
$out .= $m[1];
$text = substr($text, strlen($m[0]));
switch ($m[2]) {
case 'url(':
preg_match('/^(.*?)(\)|$)/is', $text, $inner);
$text = substr($text, strlen($inner[0]));
$out .= $m[2].$inner[1].$inner[2];
break;
case '//':
preg_match("/^(.*?)(\n|$)/is", $text, $inner);
// give back the newline
$text = substr($text, strlen($inner[0]) - 1);
break;
case '/*';
preg_match("/^(.*?)(\*\/|$)/is", $text, $inner);
$text = substr($text, strlen($inner[0]));
break;
case '"':
case "'":
preg_match("/^(.*?)(".$m[2]."|$)/is", $text, $inner);
$text = substr($text, strlen($inner[0]));
$out .= $m[2].$inner[1].$inner[2];
break;
}
}
return $out;
}
// compile to $in to $out if $in is newer than $out
// returns true when it compiles, false otherwise
public static function ccompile($in, $out) {
if (!is_file($out) || filemtime($in) > filemtime($out)) {
$less = new lessc($in);
file_put_contents($out, $less->parse());
return true;
}
return false;
}
}
?>