You are here

lessc.inc.php in Less CSS Preprocessor 6.3

File

lessphp/lessc.inc.php
View source
<?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 = '&';
  private static $precedence = array(
    '+' => 0,
    '-' => 0,
    '*' => 1,
    '/' => 1,
    '%' => 1,
  );
  private static $operatorString;

  // regex string to match any of the operators
  private static $dtypes = array(
    'expression',
    'variable',
    'function',
    'negative',
  );

  // types with delayed computation
  private static $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);
      }
    }

    // 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);
    }

    // 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";
    }

    // 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 bracketed value (contained within in a tag definition)
  function tagBracket(&$value) {
    $s = $this
      ->seek();
    if ($this
      ->literal('[') && $this
      ->to(']', $c, true) && $this
      ->literal(']', false)) {
      $value .= '[' . $c . ']';

      // whitespace?
      if ($this
        ->match('', $_)) {
        $value .= $_[0];
      }
      return true;
    }
    $this
      ->seek($s);
    return false;
  }

  // a single tag
  function tag(&$tag, $simple = false) {
    if ($simple) {
      $chars = '^,:;{}\\][>\\(\\) ';
    }
    else {
      $chars = '^,;{}[';
    }
    $tag = '';
    if ($this
      ->tagBracket($first)) {
      $tag .= $first;
    }
    while ($this
      ->match('([' . $chars . '0-9][' . $chars . ']*)', $m)) {
      $tag .= $m[1];
      if ($simple) {
        break;
      }
      if ($this
        ->tagBracket($brack)) {
        $tag .= $brack;
      }
    }
    $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('/{(' . $this
          ->preg_quote($this->vPrefix) . '[\\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;
  }

}

Classes

Namesort descending Description
lessc