You are here

class lessc in Less CSS Preprocessor 6

Same name and namespace in other branches
  1. 6.3 lessphp/lessc.inc.php \lessc

Hierarchy

Expanded class hierarchy of lessc

File

./lessc.inc.php, line 27

View source
class lessc {
  private $buffer;
  private $out;
  private $env = array();

  // environment stack
  private $count = 0;

  // temporary advance
  private $level = 0;
  private $line = 1;

  // the current line
  private $precedence = array(
    '+' => '0',
    '-' => '0',
    '*' => '1',
    '/' => '1',
  );

  // delayed types
  private $dtypes = array(
    'expression',
    'variable',
  );

  // the default set of units
  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))) . ')';
  }

  // load a css from file
  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();

    // set up global scope
    $this
      ->set('__tags', array(
      '',
    ));

    // equivalent to 1 in tag multiplication
    $this->buffer = $this
      ->removeComments($this->buffer);

    // trim whitespace on head
    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' : ''));
    }

    // print_r($this->env);
    return $this->out;
  }

  // read a chunk off the head of the buffer
  // chunks are separated by ; (in most cases)
  private function readChunk() {
    if ($this->buffer == '') {
      return false;
    }

    // todo: media directive
    // media screen {
    // 	blocks
    // }
    // a property
    try {
      $this
        ->keyword($name)
        ->literal(':')
        ->propertyValue($value)
        ->end()
        ->advance();
      $this
        ->append($name, $value);

      // we can print it right away if we are in global scope (makes no sense, but w/e)
      if ($this->level > 1) {
        return true;
      }
      else {
        return $this
          ->compileProperty($name, array(
          $this
            ->getVal($name),
        )) . "\n";
      }
    } catch (exception $ex) {
      $this
        ->undo();
    }

    // entering a block
    try {
      $this
        ->tags($tags);

      // it can only be a function if there is one tag
      if (count($tags) == 1) {
        try {
          $save = $this->count;
          $this
            ->argumentDef($args);
        } catch (exception $ex) {
          $this->count = $save;
        }
      }
      $this
        ->literal('{')
        ->advance();
      $this
        ->push();

      //  move @ tags out of variable namespace!
      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();
    }

    // leaving a block
    try {
      $this
        ->literal('}')
        ->advance();
      $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('@' . $arg[0], $arg[1]);
          }
        }
      }
      if (!empty($tags)) {
        $out = $this
          ->compileBlock($tags, $env);
      }
      $this
        ->pop();

      // make the block(s) available in the new current scope
      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;
    } catch (exception $ex) {
      $this
        ->undo();
    }

    // look for import
    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();
    }

    // setting a variable
    try {
      $this
        ->variable($name)
        ->literal(':')
        ->propertyValue($value)
        ->end()
        ->advance();
      $this
        ->append('@' . $name, $value);
      return true;
    } catch (exception $ex) {
      $this
        ->undo();
    }

    // look for a namespace/function to expand
    // todo: this catches a lot of invalid syntax because tag
    // consumer is liberal. This causes errors to be hidden
    try {
      $this
        ->tags($tags, true, '>');

      //  move @ tags out of variable namespace
      foreach ($tags as &$tag) {
        if ($tag[0] == "@") {
          $tag[0] = "%";
        }
      }

      // look for arguments
      $save = $this->count;
      try {
        $this
          ->argumentValues($argv);
      } catch (exception $ex) {
        $this->count = $save;
      }
      $this
        ->end()
        ->advance();

      // find the final environment
      $env = $this
        ->get(array_shift($tags));
      while ($sub = array_shift($tags)) {
        if (isset($env[$sub])) {

          // todo add a type check for environment
          $env = $env[$sub];
        }
        else {
          $env = null;
          break;
        }
      }
      if ($env == null) {
        return true;
      }

      // if we have arguments then insert them
      if (!empty($env['__args'])) {
        foreach ($env['__args'] as $arg) {
          $name = $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['@' . $name])) {
            array_unshift($env['@' . $name], $value);
          }
          else {

            // new element
            $env['@' . $name] = array(
              $value,
            );
          }
        }
      }

      // set all properties
      ob_start();
      foreach ($env as $name => $value) {

        // if it is a block then render it
        if (!isset($value[0])) {
          $rtags = $this
            ->multiplyTags(array(
            $name,
          ));
          echo $this
            ->compileBlock($rtags, $value);
        }

        // copy everything except metadata
        if (!preg_match('/^__/', $name)) {

          // 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);
          }
        }
      }
      return ob_get_clean();
    } catch (exception $ex) {
      $this
        ->undo();
    }

    // ignore spare ;
    try {
      $this
        ->literal(';')
        ->advance();
      return true;
    } catch (exception $ex) {
      $this
        ->undo();
    }

    // something failed
    // print_r($this->env);
    $this
      ->match("(.*?)(\n|\$)", $m);
    throw new exception('Failed to parse line ' . $this->line . "\nOffending line: " . $m[1]);
  }

  /**
   * consume functions
   *
   * they return instance of class so they can be chained
   * any return vals are put into referenced arguments
   */

  // look for an import statement on the head of the buffer
  private function import(&$url, &$media) {
    $this
      ->literal('@import');
    $save = $this->count;
    try {

      // todo: merge this with the keyword url('')
      $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) {

      // there is an end of block next, then no problem
      if (strlen($this->buffer) <= $this->count || $this->buffer[$this->count] != '}') {
        throw new exception('parse error: failed to find end');
      }
    }
    return $this;
  }

  // gets a list of property values separated by ; between ( and )
  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;
  }

  // consume agument definition, variable names with optional value
  private function argumentDef(&$args, $delim = ';') {
    $this
      ->literal('(');
    $values = array();
    while (true) {
      try {
        $arg = array();
        $this
          ->variable($arg[]);

        // look for a default value
        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;
  }

  // get a list of tags separated by commas
  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;
  }

  // match a single tag, aka the block identifier
  // $simple only match simple tags, no funny selectors allowed
  // this accepts spaces so it can mis comments...
  private function tag(&$tag, $simple = false) {
    if ($simple) {
      $chars = '^,:;{}\\][>\\(\\)';
    }
    else {
      $chars = '^,;{}\\(\\)';
    }

    // can't start with a number
    if (!$this
      ->match('([' . $chars . '0-9][' . $chars . ']*)', $m)) {
      throw new exception('parse error: failed to parse tag');
    }
    $tag = trim($m[1]);
    return $this;
  }

  // consume $what and following whitespace from the head of buffer
  private function literal($what) {

    // if $what is one char we can speed things up
    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;
  }

  // consume list of values for property
  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;
  }

  // evaluate a list of expressions separated by spaces
  private function expressionList(&$vals) {
    $vals = array();
    $this
      ->expression($vals[]);

    // there should be at least one
    while (1) {
      try {
        $this
          ->expression($tmp);
      } catch (Exception $ex) {
        break;
      }
      $vals[] = $tmp;
    }
    return $this;
  }

  // evaluate a group of values separated by operators
  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;
  }

  // used to recursively love infix equation with proper operator order
  private function expHelper($lhs, $minP) {

    // while there is an operator and the precedence is greater or equal to min
    while ($this
      ->match($this->matchString, $m) && $this->precedence[$m[1]] >= $minP) {

      // check for subexp
      try {
        $this
          ->literal('(')
          ->expression($exp)
          ->literal(')');
        $rhs = $exp;
      } catch (exception $ex) {
        $this
          ->value($rhs);
      }

      // find out if next up needs rhs
      if ($this
        ->peek($this->matchString, $mi) && $this->precedence[$mi[1]] > $minP) {
        $rhs = $this
          ->expHelper($rhs, $this->precedence[$mi[1]]);
      }

      // todo: find a way to precalculate non-delayed types
      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;
  }

  // consume a css value:
  // a keyword
  // a variable (includes accessor);
  // a color
  // a unit (em, px, pt, %, mm), can also have no unit 4px + 3;
  // a string
  private function value(&$val) {
    try {
      return $this
        ->unit($val);
    } catch (exception $ex) {

      /* $this->undo(); */
    }

    // look for accessor
    // must be done before color
    try {
      $save = $this->count;

      // todo: replace with counter stack
      $this
        ->accessor($a);
      $tmp = $this
        ->get($a[0]);

      // get env
      $val = end($tmp[$a[1]]);

      // get latest var
      return $this;
    } catch (exception $ex) {
      $this->count = $save;

      /* $this->undo(); */
    }
    try {
      return $this
        ->color($val);
    } catch (exception $ex) {

      /* $this->undo(); */
    }
    try {
      $save = $this->count;
      $this
        ->func($f);
      $val = array(
        'string',
        $f,
      );
      return $this;
    } catch (exception $ex) {
      $this->count = $save;
    }

    // a string
    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) {

      /* $this->undo(); */
    }

    // try to get a variable
    try {
      $this
        ->variable($name);
      $val = array(
        'variable',
        '@' . $name,
      );
      return $this;
    } catch (exception $ex) {

      /* $this->undo(); */
    }
    throw new exception('parse error: failed to find value');
  }

  // $units the allowed units
  // number is always allowed (is this okay?)
  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');
    }

    // throw on a default unit
    if (!isset($m[3])) {
      $m[3] = 'number';
    }
    $unit = array(
      $m[3],
      $m[1],
    );
    return $this;
  }

  // todo: hue saturation lightness support (hsl)
  // need a function to convert hsl to rgb
  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;

        // todo: this is retarded
        $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('(');

        // grab the numbers and format
        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;

    // don't put things on out unless everything works out
    return $this;
  }
  private function variable(&$var) {
    $this
      ->literal('@')
      ->keyword($var);
    return $this;
  }
  private function accessor(&$var) {
    $this
      ->tag($scope, true)
      ->literal('[');

    // see if it is a variable
    try {
      $this
        ->variable($name);
      $name = '@' . $name;
    } catch (exception $ex) {

      // try to see if it is a property
      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;
  }

  // read a css function off the head of the buffer
  private function func(&$func) {
    $this
      ->keyword($fname)
      ->literal('(')
      ->to(')', $args);
    $func = $fname . '(' . $args . ')';
    return $this;
  }

  // read a keyword off the head of the buffer
  private function keyword(&$word) {
    if (!$this
      ->match('([\\w_\\-!"][\\w\\-_"]*)', $m)) {
      throw new Exception('parse error: failed to find keyword');
    }
    $word = $m[1];
    return $this;
  }

  // this ignores comments because it doesn't grab by token
  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;
  }

  /**
   * compile functions turn data into css code
   */
  private function compileBlock($rtags, $env) {

    // don't render functions
    foreach ($rtags as $i => $tag) {
      if (preg_match('/( |^)%/', $tag)) {
        unset($rtags[$i]);
      }
    }
    if (empty($rtags)) {
      return '';
    }
    $props = 0;

    // print all the 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] != '@' && $name != '__args') {
        echo $this
          ->compileProperty($name, $value, 1) . "\n";
        $props++;
      }
    }
    $list = ob_get_clean();
    if ($props == 0) {
      return true;
    }

    // do some formatting
    if ($props == 1) {
      $list = ' ' . trim($list) . ' ';
    }
    return implode(", ", $rtags) . ' {' . ($props > 1 ? "\n" : '') . $list . "}\n";
  }
  private function compileProperty($name, $value, $level = 0) {

    // compile all repeated properties
    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':

        // search for values inside the 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:

        // assumed to be a unit
        return $value[1] . $value[0];
    }
  }
  private function compileColor($c) {
    if (count($c) == 5) {

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

  /**
   * arithmetic evaluator and operators
   */

  // evalue an operator
  // this is a messy function, probably a better way to do it
  private function evaluate($op, $lft, $rgt) {
    $pushed = 0;

    // figure out what expressions and variables are equal to
    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);
    }

    // default number number
    return $this
      ->op_number_number($op, $lft, $rgt);
  }
  private function op_number_number($op, $lft, $rgt) {
    if ($rgt[0] == '%') {
      $rgt[1] /= 100;
    }

    // figure out the type
    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);
  }

  /**
   * functions for controlling the environment
   */

  // get something out of the env
  // search from the head of the stack down
  // $env what environment to search in
  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;
  }

  // get the most recent value of a variable
  // return default if it isn't found
  // $skip is number of vars to skip
  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);
  }

  // merge a block into the current env
  private 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;
    }
  }

  // set something in the current env
  private function set($name, $value) {
    $this->env[count($this->env) - 1][$name] = $value;
  }

  // append to array in the current env
  private function append($name, $value) {
    $this->env[count($this->env) - 1][$name][] = $value;
  }

  // put on the front of the 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);
    }
  }

  // push a new environment stack
  private function push() {
    $this->level++;
    $this->env[] = array();
  }

  // pop environment off the stack
  private function pop() {
    if ($this->level == 1) {
      throw new exception('parse error: unexpected end of block');
    }
    $this->level--;
    return array_pop($this->env);
  }

  /**
   * misc functions
   */

  // functions for manipulating the expand stack
  private $expandStack = array();

  // push name on expand stack and return its count
  // before being pushed
  private function pushName($name) {
    $count = array_count_values($this->expandStack);
    $count = isset($count[$name]) ? $count[$name] : 0;
    $this->expandStack[] = $name;
    return $count;
  }

  // pop name of expand stack and return it
  private function popName() {
    return array_pop($this->expandStack);
  }

  // remove comments from $text
  // todo: make it work for all functions, not just url
  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);

          // 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;
      }
    }
    $this->count = 0;
    return $out;
  }

  // match text from the head while skipping $count characters
  // advances the temp counter if it succeeds
  private function match($regex, &$out, $eatWhitespace = true) {

    // if ($this->count > 100) echo '-- '.$this->count."\n";
    $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);
  }

  // compress a list of values into a single type
  // if the list contains one thing, then return that thing
  private function compressValues($values, $delim = ' ') {
    if (count($values) == 1) {
      return $values[0];
    }
    return array(
      'list',
      $delim,
      $values,
    );
  }

  // make sure a color's components don't go out of bounds
  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) {

    // I don't know why it doesn't include it by default
    return preg_quote($what, '/');
  }

  // reset all internal state to default
  private function reset() {
    $this->out = '';
    $this->env = array();
    $this->line = 1;
    $this->count = 0;
  }

  // advance the buffer n places
  private function advance() {

    // this is probably slow
    $tmp = substr($this->buffer, 0, $this->count);
    $this->line += substr_count($tmp, "\n");
    $this->buffer = substr($this->buffer, $this->count);
    $this->count = 0;
  }

  //  reset the temporary advance
  private function undo() {
    $this->count = 0;
  }

  // find the cartesian product of all tags in stack
  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;
        }

        // skip functions
        $rtags[] = trim($p . ($t[0] == ':' ? '' : ' ') . $t);
      }
    }
    return $rtags;
  }

  /**
   * static utility functions
   */

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

}

Members

Namesort descending Modifiers Type Description Overrides
lessc::$buffer private property
lessc::$count private property
lessc::$dtypes private property
lessc::$env private property
lessc::$expandStack private property
lessc::$importDir public property
lessc::$importDisabled public property
lessc::$level private property
lessc::$line private property
lessc::$out private property
lessc::$precedence private property
lessc::$units private property
lessc::accessor private function
lessc::advance private function
lessc::append private function
lessc::argumentDef private function
lessc::argumentValues private function
lessc::ccompile public static function
lessc::color private function
lessc::compileBlock private function * compile functions turn data into css code
lessc::compileColor private function
lessc::compileProperty private function
lessc::compileValue private function
lessc::compressValues private function
lessc::end private function
lessc::evaluate private function
lessc::expHelper private function
lessc::expression private function
lessc::expressionList private function
lessc::fixColor private function
lessc::func private function
lessc::get private function
lessc::getVal private function
lessc::import private function
lessc::keyword private function
lessc::literal private function
lessc::load public function
lessc::match private function
lessc::merge private function
lessc::multiplyTags private function
lessc::op_color_color private function
lessc::op_color_number private function
lessc::op_number_color private function
lessc::op_number_number private function
lessc::parse public function
lessc::peek private function
lessc::pop private function
lessc::popName private function
lessc::preg_quote private function
lessc::prepend private function
lessc::propertyValue private function
lessc::push private function
lessc::pushName private function
lessc::readChunk private function
lessc::removeComments private function
lessc::reset private function
lessc::set private function
lessc::string private function
lessc::tag private function
lessc::tags private function
lessc::to private function
lessc::undo private function
lessc::unit private function
lessc::value private function
lessc::variable private function
lessc::__construct public function