<?php
/**
- * lessphp v0.4.0@6e8e724fc7
+ * lessphp v0.4.0@2cc77e3c7b
* http://leafo.net/lessphp
*
- * LESS css compiler, adapted from http://lesscss.org
+ * LESS CSS compiler, adapted from http://lesscss.org
*
* For ease of distribution, lessphp 0.4.0 is under a dual license.
* You are free to pick which one suits your needs.
/**
- * The less compiler and parser.
+ * 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
*/
class lessc {
static public $VERSION = "v0.4.0";
- static protected $TRUE = array("keyword", "true");
- static protected $FALSE = array("keyword", "false");
+
+ static public $TRUE = array("keyword", "true");
+ static public $FALSE = array("keyword", "false");
protected $libFunctions = array();
protected $registeredVars = array();
foreach ($this->sortProps($block->props) as $prop) {
$this->compileProp($prop, $block, $out);
}
+ $out->lines = $this->deduplicate($out->lines);
+ }
+
+ /**
+ * Deduplicate lines in a block. Comments are not deduplicated. If a
+ * duplicate rule is detected, the comments immediately preceding each
+ * occurence are consolidated.
+ */
+ protected function deduplicate($lines) {
+ $unique = array();
+ $comments = array();
- $out->lines = array_values(array_unique($out->lines));
+ foreach($lines as $line) {
+ if (strpos($line, '/*') === 0) {
+ $comments[] = $line;
+ continue;
+ }
+ if (!in_array($line, $unique)) {
+ $unique[] = $line;
+ }
+ array_splice($unique, array_search($line, $unique), 0, $comments);
+ $comments = array();
+ }
+ return array_merge($unique, $comments);
}
protected function sortProps($props, $split = false) {
$vars = array();
$imports = array();
$other = array();
+ $stack = array();
foreach ($props as $prop) {
switch ($prop[0]) {
+ case "comment":
+ $stack[] = $prop;
+ break;
case "assign":
+ $stack[] = $prop;
if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) {
- $vars[] = $prop;
+ $vars = array_merge($vars, $stack);
} else {
- $other[] = $prop;
+ $other = array_merge($other, $stack);
}
+ $stack = array();
break;
case "import":
$id = self::$nextImportId++;
$prop[] = $id;
- $imports[] = $prop;
+ $stack[] = $prop;
+ $imports = array_merge($imports, $stack);
$other[] = array("import_mixin", $id);
+ $stack = array();
break;
default:
- $other[] = $prop;
+ $stack[] = $prop;
+ $other = array_merge($other, $stack);
+ $stack = array();
+ break;
}
}
+ $other = array_merge($other, $stack);
if ($split) {
return array(array_merge($vars, $imports), $other);
* The input is expected to be reduced. This function will not work on
* things like expressions and variables.
*/
- protected function compileValue($value) {
+ public function compileValue($value) {
switch ($value[0]) {
case 'list':
// [1] - delimiter
return $this->lib_rgbahex($color);
}
+ /**
+ * Given an url, decide whether to output a regular link or the base64-encoded contents of the file
+ *
+ * @param array $value either an argument list (two strings) or a single string
+ * @return string formatted url(), either as a link or base64-encoded
+ */
+ protected function lib_data_uri($value) {
+ $mime = ($value[0] === 'list') ? $value[2][0][2] : null;
+ $url = ($value[0] === 'list') ? $value[2][1][2][0] : $value[2][0];
+
+ $fullpath = $this->findImport($url);
+
+ if($fullpath && ($fsize = filesize($fullpath)) !== false) {
+ // IE8 can't handle data uris larger than 32KB
+ if($fsize/1024 < 32) {
+ if(is_null($mime)) {
+ if(class_exists('finfo')) { // php 5.3+
+ $finfo = new finfo(FILEINFO_MIME);
+ $mime = explode('; ', $finfo->file($fullpath));
+ $mime = $mime[0];
+ } elseif(function_exists('mime_content_type')) { // PHP 5.2
+ $mime = mime_content_type($fullpath);
+ }
+ }
+
+ if(!is_null($mime)) // fallback if the mime type is still unknown
+ $url = sprintf('data:%s;base64,%s', $mime, base64_encode(file_get_contents($fullpath)));
+ }
+ }
+
+ return 'url("'.$url.'")';
+ }
+
// utility func to unquote a string
protected function lib_e($arg) {
switch ($arg[0]) {
* 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) {
+ public function colorArgs($args) {
if ($args[0] != 'list' || count($args[2]) < 2) {
return array(array('color', 0, 0, 0), 0);
}
}
protected function lib_contrast($args) {
- if ($args[0] != 'list' || count($args[2]) < 3) {
- return array(array('color', 0, 0, 0), 0);
- }
+ $darkColor = array('color', 0, 0, 0);
+ $lightColor = array('color', 255, 255, 255);
+ $threshold = 0.43;
- list($inputColor, $darkColor, $lightColor) = $args[2];
+ if ( $args[0] == 'list' ) {
+ $inputColor = ( isset($args[2][0]) ) ? $this->assertColor($args[2][0]) : $lightColor;
+ $darkColor = ( isset($args[2][1]) ) ? $this->assertColor($args[2][1]) : $darkColor;
+ $lightColor = ( isset($args[2][2]) ) ? $this->assertColor($args[2][2]) : $lightColor;
+ $threshold = ( isset($args[2][3]) ) ? $this->assertNumber($args[2][3]) : $threshold;
+ }
+ else {
+ $inputColor = $this->assertColor($args);
+ }
- $inputColor = $this->assertColor($inputColor);
- $darkColor = $this->assertColor($darkColor);
- $lightColor = $this->assertColor($lightColor);
- $hsl = $this->toHSL($inputColor);
+ $inputColor = $this->coerceColor($inputColor);
+ $darkColor = $this->coerceColor($darkColor);
+ $lightColor = $this->coerceColor($lightColor);
- if ($hsl[3] > 50) {
- return $darkColor;
- }
+ //Figure out which is actually light and dark!
+ if ( $this->lib_luma($darkColor) > $this->lib_luma($lightColor) ) {
+ $t = $lightColor;
+ $lightColor = $darkColor;
+ $darkColor = $t;
+ }
- return $lightColor;
+ $inputColor_alpha = $this->lib_alpha($inputColor);
+ if ( ( $this->lib_luma($inputColor) * $inputColor_alpha) < $threshold) {
+ return $lightColor;
+ }
+ return $darkColor;
}
- protected function assertColor($value, $error = "expected color value") {
+ protected function lib_luma($color) {
+ $color = $this->coerceColor($color);
+ return (0.2126 * $color[0] / 255) + (0.7152 * $color[1] / 255) + (0.0722 * $color[2] / 255);
+ }
+
+
+ public 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") {
+ public function assertNumber($value, $error = "expecting number") {
if ($value[0] == "number") return $value[1];
$this->throwError($error);
}
- protected function assertArgs($value, $expectedArgs, $name="") {
+ public function assertArgs($value, $expectedArgs, $name="") {
if ($expectedArgs == 1) {
return $value;
} else {
list(, $name, $args) = $value;
if ($name == "%") $name = "_sprintf";
+
$f = isset($this->libFunctions[$name]) ?
- $this->libFunctions[$name] : array($this, 'lib_'.$name);
+ $this->libFunctions[$name] : array($this, 'lib_'.str_replace('-', '_', $name));
if (is_callable($f)) {
if ($args[0] == 'list')
return $value;
}
- protected function toBool($a) {
+ public function toBool($a) {
if ($a) return self::$TRUE;
else return self::$FALSE;
}
/**
* Uses the current value of $this->count to show line and line number
*/
- protected function throwError($msg = null) {
+ public function throwError($msg = null) {
if ($this->sourceLoc >= 0) {
$this->sourceParser->throwError($msg, $this->sourceLoc);
}
$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 (!is_null($this->env->parent))
+ if ( !property_exists($this->env, 'parent') || !is_null($this->env->parent) )
throw new exception('parse error: unclosed block');
return $this->env;
if (empty($this->buffer)) return false;
$s = $this->seek();
+ if ($this->whitespace()) {
+ return true;
+ }
+
// setting a property
if ($this->keyword($key) && $this->assign() &&
$this->propertyValue($value, $key) && $this->end())
}
// opening a simple block
- if ($this->tags($tags) && $this->literal('{')) {
+ if ($this->tags($tags) && $this->literal('{', false)) {
$tags = $this->fixTags($tags);
$this->pushBlock($tags);
return true;
// an import statement
protected function import(&$out) {
- $s = $this->seek();
if (!$this->literal('@import')) return false;
// @import "something.css" media;
// 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;
// consume an end of statement delimiter
protected function end() {
- if ($this->literal(';')) {
+ if ($this->literal(';', false)) {
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 ;
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])) {
+ if (isset($m[1]) && empty($this->seenComments[$this->count])) {
$this->append(array("comment", $m[1]));
- $this->commentsSeen[$this->count] = true;
+ $this->seenComments[$this->count] = true;
}
$this->count += strlen($m[0]);
$gotWhite = true;