View source
<?php
class lessc {
private $buffer;
private $out;
private $env = array();
private $count = 0;
private $level = 0;
private $line = 1;
private $precedence = array(
'+' => '0',
'-' => '0',
'*' => '1',
'/' => '1',
);
private $dtypes = array(
'expression',
'variable',
);
private $units = array(
'px',
'%',
'in',
'cm',
'mm',
'em',
'ex',
'pt',
'pc',
's',
);
public $importDisabled = false;
public $importDir = '';
public function __construct($fname = null) {
if ($fname) {
$this
->load($fname);
}
$this->matchString = '(' . implode('|', array_map(array(
$this,
'preg_quote',
), array_keys($this->precedence))) . ')';
}
public function load($fname) {
if (!is_file($fname)) {
throw new Exception('load error: failed to find ' . $fname);
}
$pi = pathinfo($fname);
$this->file = $fname;
$this->importDir = $pi['dirname'] . '/';
$this->buffer = file_get_contents($fname);
}
public function parse($text = null) {
if ($text) {
$this->buffer = $text;
}
$this
->reset();
$this
->push();
$this
->set('__tags', array(
'',
));
$this->buffer = $this
->removeComments($this->buffer);
if (preg_match('/^\\s+/', $this->buffer, $m)) {
$this->line += substr_count($m[0], "\n");
$this->buffer = ltrim($this->buffer);
}
while (false !== ($dat = $this
->readChunk())) {
if (is_string($dat)) {
$this->out .= $dat;
}
}
if ($count = count($this->env) > 1) {
throw new exception('Failed to parse ' . (count($this->env) - 1) . ' unclosed block' . ($count > 1 ? 's' : ''));
}
return $this->out;
}
private function readChunk() {
if ($this->buffer == '') {
return false;
}
try {
$this
->keyword($name)
->literal(':')
->propertyValue($value)
->end()
->advance();
$this
->append($name, $value);
if ($this->level > 1) {
return true;
}
else {
return $this
->compileProperty($name, array(
$this
->getVal($name),
)) . "\n";
}
} catch (exception $ex) {
$this
->undo();
}
try {
$this
->tags($tags);
if (count($tags) == 1) {
try {
$save = $this->count;
$this
->argumentDef($args);
} catch (exception $ex) {
$this->count = $save;
}
}
$this
->literal('{')
->advance();
$this
->push();
foreach ($tags as &$tag) {
if ($tag[0] == "@") {
$tag[0] = "%";
}
}
$this
->set('__tags', $tags);
if (isset($args)) {
$this
->set('__args', $args);
}
return true;
} catch (exception $ex) {
$this
->undo();
}
try {
$this
->literal('}')
->advance();
$tags = $this
->multiplyTags();
$env = end($this->env);
$ctags = $env['__tags'];
unset($env['__tags']);
if (isset($env['__args'])) {
foreach ($env['__args'] as $arg) {
if (isset($arg[1])) {
$this
->prepend('@' . $arg[0], $arg[1]);
}
}
}
if (!empty($tags)) {
$out = $this
->compileBlock($tags, $env);
}
$this
->pop();
foreach ($ctags as $t) {
if ($this
->get($t, array(
end($this->env),
))) {
$this
->merge($t, $env);
}
else {
$this
->set($t, $env);
}
}
return isset($out) ? $out : true;
} catch (exception $ex) {
$this
->undo();
}
try {
$this
->import($url, $media)
->advance();
if ($this->importDisabled) {
return "/* import is disabled */\n";
}
$full = $this->importDir . $url;
if (file_exists($file = $full) || file_exists($file = $full . '.less')) {
$this->buffer = $this
->removeComments(file_get_contents($file) . ";\n" . $this->buffer);
return true;
}
return '@import url("' . $url . '")' . ($media ? ' ' . $media : '') . ";\n";
} catch (exception $ex) {
$this
->undo();
}
try {
$this
->variable($name)
->literal(':')
->propertyValue($value)
->end()
->advance();
$this
->append('@' . $name, $value);
return true;
} catch (exception $ex) {
$this
->undo();
}
try {
$this
->tags($tags, true, '>');
foreach ($tags as &$tag) {
if ($tag[0] == "@") {
$tag[0] = "%";
}
}
$save = $this->count;
try {
$this
->argumentValues($argv);
} catch (exception $ex) {
$this->count = $save;
}
$this
->end()
->advance();
$env = $this
->get(array_shift($tags));
while ($sub = array_shift($tags)) {
if (isset($env[$sub])) {
$env = $env[$sub];
}
else {
$env = null;
break;
}
}
if ($env == null) {
return true;
}
if (!empty($env['__args'])) {
foreach ($env['__args'] as $arg) {
$name = $arg[0];
$value = is_array($argv) ? array_shift($argv) : null;
if ($value == null && isset($arg[1])) {
$value = $arg[1];
}
if (isset($env['@' . $name])) {
array_unshift($env['@' . $name], $value);
}
else {
$env['@' . $name] = array(
$value,
);
}
}
}
ob_start();
foreach ($env as $name => $value) {
if (!isset($value[0])) {
$rtags = $this
->multiplyTags(array(
$name,
));
echo $this
->compileBlock($rtags, $value);
}
if (!preg_match('/^__/', $name)) {
if ($this
->get($name, array(
end($this->env),
))) {
while ($tval = array_shift($value)) {
$this
->append($name, $tval);
}
}
else {
$this
->set($name, $value);
}
}
}
return ob_get_clean();
} catch (exception $ex) {
$this
->undo();
}
try {
$this
->literal(';')
->advance();
return true;
} catch (exception $ex) {
$this
->undo();
}
$this
->match("(.*?)(\n|\$)", $m);
throw new exception('Failed to parse line ' . $this->line . "\nOffending line: " . $m[1]);
}
private function import(&$url, &$media) {
$this
->literal('@import');
$save = $this->count;
try {
$this
->literal('url(')
->string($url)
->literal(')');
} catch (exception $ex) {
$this->count = $save;
$this
->string($url);
}
$this
->to(';', $media);
return $this;
}
private function string(&$string, &$d = null) {
try {
$this
->literal('"', true);
$delim = '"';
} catch (exception $ex) {
$this
->literal("'", true);
$delim = "'";
}
$this
->to($delim, $string);
if (!isset($d)) {
$d = $delim;
}
return $this;
}
private function end() {
try {
$this
->literal(';');
} catch (exception $ex) {
if (strlen($this->buffer) <= $this->count || $this->buffer[$this->count] != '}') {
throw new exception('parse error: failed to find end');
}
}
return $this;
}
private function argumentValues(&$args, $delim = ';') {
$this
->literal('(');
$values = array();
while (true) {
try {
$this
->propertyValue($values[])
->literal(';');
} catch (exception $ex) {
break;
}
}
$this
->literal(')');
$args = $values;
return $this;
}
private function argumentDef(&$args, $delim = ';') {
$this
->literal('(');
$values = array();
while (true) {
try {
$arg = array();
$this
->variable($arg[]);
try {
$this
->literal(':')
->propertyValue($value);
$arg[] = $value;
} catch (exception $ax) {
}
$values[] = $arg;
$this
->literal($delim);
} catch (exception $ex) {
break;
}
}
$this
->literal(')');
$args = $values;
return $this;
}
private function tags(&$tags, $simple = false, $delim = ',') {
$tags = array();
while (1) {
$this
->tag($tmp, $simple);
$tags[] = trim($tmp);
try {
$this
->literal($delim);
} catch (Exception $ex) {
break;
}
}
return $this;
}
private function tag(&$tag, $simple = false) {
if ($simple) {
$chars = '^,:;{}\\][>\\(\\)';
}
else {
$chars = '^,;{}\\(\\)';
}
if (!$this
->match('([' . $chars . '0-9][' . $chars . ']*)', $m)) {
throw new exception('parse error: failed to parse tag');
}
$tag = trim($m[1]);
return $this;
}
private function literal($what) {
if (strlen($what) == 1 && $this->count < strlen($this->buffer) && $what != $this->buffer[$this->count] || !$this
->match($this
->preg_quote($what), $m)) {
throw new Exception('parse error: failed to prase literal ' . $what);
}
return $this;
}
private function propertyValue(&$value) {
$out = array();
while (1) {
try {
$this
->expressionList($out[]);
$this
->literal(',');
} catch (exception $ex) {
break;
}
}
if (!empty($out)) {
$out = array_map(array(
$this,
'compressValues',
), $out);
$value = $this
->compressValues($out, ', ');
}
return $this;
}
private function expressionList(&$vals) {
$vals = array();
$this
->expression($vals[]);
while (1) {
try {
$this
->expression($tmp);
} catch (Exception $ex) {
break;
}
$vals[] = $tmp;
}
return $this;
}
private function expression(&$result) {
try {
$this
->literal('(')
->expression($exp)
->literal(')');
$lhs = $exp;
} catch (exception $ex) {
$this
->value($lhs);
}
$result = $this
->expHelper($lhs, 0);
return $this;
}
private function expHelper($lhs, $minP) {
while ($this
->match($this->matchString, $m) && $this->precedence[$m[1]] >= $minP) {
try {
$this
->literal('(')
->expression($exp)
->literal(')');
$rhs = $exp;
} catch (exception $ex) {
$this
->value($rhs);
}
if ($this
->peek($this->matchString, $mi) && $this->precedence[$mi[1]] > $minP) {
$rhs = $this
->expHelper($rhs, $this->precedence[$mi[1]]);
}
if (in_array($rhs[0], $this->dtypes) || in_array($lhs[0], $this->dtypes)) {
$lhs = array(
'expression',
$m[1],
$lhs,
$rhs,
);
}
else {
$lhs = $this
->evaluate($m[1], $lhs, $rhs);
}
}
return $lhs;
}
private function value(&$val) {
try {
return $this
->unit($val);
} catch (exception $ex) {
}
try {
$save = $this->count;
$this
->accessor($a);
$tmp = $this
->get($a[0]);
$val = end($tmp[$a[1]]);
return $this;
} catch (exception $ex) {
$this->count = $save;
}
try {
return $this
->color($val);
} catch (exception $ex) {
}
try {
$save = $this->count;
$this
->func($f);
$val = array(
'string',
$f,
);
return $this;
} catch (exception $ex) {
$this->count = $save;
}
try {
$save = $this->count;
$this
->string($tmp, $d);
$val = array(
'string',
$d . $tmp . $d,
);
return $this;
} catch (exception $ex) {
$this->count = $save;
}
try {
$this
->keyword($k);
$val = array(
'keyword',
$k,
);
return $this;
} catch (exception $ex) {
}
try {
$this
->variable($name);
$val = array(
'variable',
'@' . $name,
);
return $this;
} catch (exception $ex) {
}
throw new exception('parse error: failed to find value');
}
private function unit(&$unit, $units = null) {
if (!$units) {
$units = $this->units;
}
if (!$this
->match('(-?[0-9]*(\\.)?[0-9]+)(' . implode('|', $units) . ')?', $m)) {
throw new exception('parse error: failed to consume unit');
}
if (!isset($m[3])) {
$m[3] = 'number';
}
$unit = array(
$m[3],
$m[1],
);
return $this;
}
private 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);
}
}
else {
$save = $this->count;
try {
$this
->literal('rgb');
try {
$this
->literal('a');
$count = 4;
} catch (exception $ex) {
$count = 3;
}
$this
->literal('(');
foreach (range(1, $count) as $i) {
$this
->unit($color[], array(
'%',
));
if ($i != $count) {
$this
->literal(',');
}
if ($color[$i][0] == '%') {
$color[$i] = 255 * ($color[$i][1] / 100);
}
else {
$color[$i] = $color[$i][1];
}
}
$this
->literal(')');
$color = $this
->fixColor($color);
} catch (exception $ex) {
$this->count = $save;
throw new exception('failed to find color');
}
}
$out = $color;
return $this;
}
private function variable(&$var) {
$this
->literal('@')
->keyword($var);
return $this;
}
private function accessor(&$var) {
$this
->tag($scope, true)
->literal('[');
try {
$this
->variable($name);
$name = '@' . $name;
} catch (exception $ex) {
try {
$this
->literal("'")
->keyword($name)
->literal("'");
} catch (exception $ex) {
throw new exception('parse error: failed to parse accessor');
}
}
$this
->literal(']');
$var = array(
$scope,
$name,
);
return $this;
}
private function func(&$func) {
$this
->keyword($fname)
->literal('(')
->to(')', $args);
$func = $fname . '(' . $args . ')';
return $this;
}
private function keyword(&$word) {
if (!$this
->match('([\\w_\\-!"][\\w\\-_"]*)', $m)) {
throw new Exception('parse error: failed to find keyword');
}
$word = $m[1];
return $this;
}
private function to($what, &$out) {
if (!$this
->match('(.*?)' . $this
->preg_quote($what), $m)) {
throw new exception('parse error: failed to consume to ' . $what);
}
$out = $m[1];
return $this;
}
private function compileBlock($rtags, $env) {
foreach ($rtags as $i => $tag) {
if (preg_match('/( |^)%/', $tag)) {
unset($rtags[$i]);
}
}
if (empty($rtags)) {
return '';
}
$props = 0;
ob_start();
foreach ($env as $name => $value) {
if (isset($value[0]) && $name[0] != '@' && $name != '__args') {
echo $this
->compileProperty($name, $value, 1) . "\n";
$props++;
}
}
$list = ob_get_clean();
if ($props == 0) {
return true;
}
if ($props == 1) {
$list = ' ' . trim($list) . ' ';
}
return implode(", ", $rtags) . ' {' . ($props > 1 ? "\n" : '') . $list . "}\n";
}
private function compileProperty($name, $value, $level = 0) {
foreach ($value as $v) {
$props[] = str_repeat(' ', $level) . $name . ':' . $this
->compileValue($v) . ';';
}
return implode("\n", $props);
}
private function compileValue($value) {
switch ($value[0]) {
case 'list':
return implode($value[1], array_map(array(
$this,
'compileValue',
), $value[2]));
case 'expression':
return $this
->compileValue($this
->evaluate($value[1], $value[2], $value[3]));
case 'variable':
$tmp = $this
->compileValue($this
->getVal($value[1], $this
->pushName($value[1])));
$this
->popName();
return $tmp;
case 'string':
$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) {
$value[1] = str_replace('{' . $var . '}', $val, $value[1]);
}
return $value[1];
case 'color':
return $this
->compileColor($value);
case 'keyword':
return $value[1];
case 'number':
return $value[1];
default:
return $value[1] . $value[0];
}
}
private function compileColor($c) {
if (count($c) == 5) {
return 'rgba(' . $c[1] . ',' . $c[2] . ',' . $c[3] . ',' . $c[4] . ')';
}
$out = '#';
foreach (range(1, 3) as $i) {
$out .= ($c[$i] < 16 ? '0' : '') . dechex($c[$i]);
}
return $out;
}
private function evaluate($op, $lft, $rgt) {
$pushed = 0;
while (in_array($lft[0], $this->dtypes)) {
if ($lft[0] == 'expression') {
$lft = $this
->evaluate($lft[1], $lft[2], $lft[3]);
}
else {
if ($lft[0] == 'variable') {
$lft = $this
->getVal($lft[1], $this
->pushName($lft[1]), array(
'number',
0,
));
$pushed++;
}
}
}
while ($pushed != 0) {
$this
->popName();
$pushed--;
}
while (in_array($rgt[0], $this->dtypes)) {
if ($rgt[0] == 'expression') {
$rgt = $this
->evaluate($rgt[1], $rgt[2], $rgt[3]);
}
else {
if ($rgt[0] == 'variable') {
$rgt = $this
->getVal($rgt[1], $this
->pushName($rgt[1]), array(
'number',
0,
));
$pushed++;
}
}
}
while ($pushed != 0) {
$this
->popName();
$pushed--;
}
if ($lft[0] == 'color' && $rgt[0] == 'color') {
return $this
->op_color_color($op, $lft, $rgt);
}
if ($lft[0] == 'color') {
return $this
->op_color_number($op, $lft, $rgt);
}
if ($rgt[0] == 'color') {
return $this
->op_number_color($op, $lft, $rgt);
}
return $this
->op_number_number($op, $lft, $rgt);
}
private function op_number_number($op, $lft, $rgt) {
if ($rgt[0] == '%') {
$rgt[1] /= 100;
}
if ($rgt[0] == 'number' || $rgt[0] == '%') {
$type = $lft[0];
}
else {
$type = $rgt[0];
}
$num = array(
$type,
);
switch ($op) {
case '+':
$num[] = $lft[1] + $rgt[1];
break;
case '*':
$num[] = $lft[1] * $rgt[1];
break;
case '-':
$num[] = $lft[1] - $rgt[1];
break;
case '/':
if ($rgt[1] == 0) {
throw new exception("parse error: can't divide by zero");
}
$num[] = $lft[1] / $rgt[1];
break;
default:
throw new exception('parse error: number op number failed on op ' . $op);
}
return $num;
}
private function op_number_color($op, $lft, $rgt) {
if ($op == '+' || ($op = '*')) {
return $this
->op_color_number($op, $rgt, $lft);
}
}
private function op_color_number($op, $lft, $rgt) {
if ($rgt[0] == '%') {
$rgt[1] /= 100;
}
return $this
->op_color_color($op, $lft, array(
'color',
$rgt[1],
$rgt[1],
$rgt[1],
));
}
private function op_color_color($op, $lft, $rgt) {
$newc = array(
'color',
);
switch ($op) {
case '+':
$newc[] = $lft[1] + $rgt[1];
$newc[] = $lft[2] + $rgt[2];
$newc[] = $lft[3] + $rgt[3];
break;
case '*':
$newc[] = $lft[1] * $rgt[1];
$newc[] = $lft[2] * $rgt[2];
$newc[] = $lft[3] * $rgt[3];
break;
case '-':
$newc[] = $lft[1] - $rgt[1];
$newc[] = $lft[2] - $rgt[2];
$newc[] = $lft[3] - $rgt[3];
break;
case '/':
if ($rgt[1] == 0 || $rgt[2] == 0 || $rgt[3] == 0) {
throw new exception("parse error: can't divide by zero");
}
$newc[] = $lft[1] / $rgt[1];
$newc[] = $lft[2] / $rgt[2];
$newc[] = $lft[3] / $rgt[3];
break;
default:
throw new exception('parse error: color op number failed on op ' . $op);
}
return $this
->fixColor($newc);
}
private 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;
}
private 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);
}
private function merge($name, $value) {
$top =& $this->env[count($this->env) - 1];
if (!isset($top[$name])) {
return $this
->set($name, $value);
}
foreach ($value as $k => $v) {
$top[$name][$k] = $v;
}
}
private function set($name, $value) {
$this->env[count($this->env) - 1][$name] = $value;
}
private function append($name, $value) {
$this->env[count($this->env) - 1][$name][] = $value;
}
private 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);
}
}
private function push() {
$this->level++;
$this->env[] = array();
}
private function pop() {
if ($this->level == 1) {
throw new exception('parse error: unexpected end of block');
}
$this->level--;
return array_pop($this->env);
}
private $expandStack = array();
private function pushName($name) {
$count = array_count_values($this->expandStack);
$count = isset($count[$name]) ? $count[$name] : 0;
$this->expandStack[] = $name;
return $count;
}
private function popName() {
return array_pop($this->expandStack);
}
private 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);
$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;
}
}
$this->count = 0;
return $out;
}
private function match($regex, &$out, $eatWhitespace = true) {
$r = '/^.{' . $this->count . '}' . $regex . ($eatWhitespace ? '\\s*' : '') . '/is';
if (preg_match($r, $this->buffer, $out)) {
$this->count = strlen($out[0]);
return true;
}
return false;
}
private function peek($regex, &$out = null) {
return preg_match('/^.{' . $this->count . '}' . $regex . '/is', $this->buffer, $out);
}
private function compressValues($values, $delim = ' ') {
if (count($values) == 1) {
return $values[0];
}
return array(
'list',
$delim,
$values,
);
}
private function fixColor($c) {
for ($i = 1; $i < 4; $i++) {
if ($c[$i] < 0) {
$c[$i] = 0;
}
if ($c[$i] > 255) {
$c[$i] = 255;
}
$c[$i] = floor($c[$i]);
}
return $c;
}
private function preg_quote($what) {
return preg_quote($what, '/');
}
private function reset() {
$this->out = '';
$this->env = array();
$this->line = 1;
$this->count = 0;
}
private function advance() {
$tmp = substr($this->buffer, 0, $this->count);
$this->line += substr_count($tmp, "\n");
$this->buffer = substr($this->buffer, $this->count);
$this->count = 0;
}
private function undo() {
$this->count = 0;
}
private 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] == '@') {
continue;
}
$rtags[] = trim($p . ($t[0] == ':' ? '' : ' ') . $t);
}
}
return $rtags;
}
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;
}
}