Subversion Repositories PHPX

Rev

Blame | Last modification | View Log | RSS feed

1
<?php

namespace de\pointedears\css\least;

require_once __DIR__ . '/../../string/parser/Lexer.php';
require_once __DIR__ . '/../../string/parser/Parser.php';

/**
 * Parses a LEAST stylesheet into a CSS stylesheet
 *
 * @author Thomas 'PointedEars' Lahn
 * @property-read string $compiled
 */

class Parser extends \de\pointedears\string\parser\Parser
{
  /**
   * CSS stylesheet code compiled from the LEAST stylesheet
   *
   * @var string
   */

  protected $_compiled;
 
  /**
   * Array of arrays of template variables, one inner array for
   * each scope level.
   * @var array[array]
   */

  protected $_vars = array(array());

  /**
   * The nesting level of the current variable scope.
   *
   * Used to hold variable definitions per nesting level.
   *
   * @var int
   */

  protected $_scope_level = 0;

  /**
   * Cache for lookup results.
   *
   * The lookup cache improves parser performance with
   * nested scopes.  By contrast to <code>$_vars</code>, it is
   * a _shallow_ storage for template variable definitions and
   * represents the current _certain_ variable knowledge of the
   * parser, i.e. variables that have been defined in outer scopes
   * and the same scope, and whose values have been looked up in
   * <code>$_vars</code> before.
   *
   * The lookup cache is replaced by the variables of the outer
   * scope when a scope is exited because definitions in the
   * outer scope may have been shadowed by the inner scope.
   * Note that definitions from the second-next, third-next
   * aso. scopes need to be looked up again then.
   *
   * @var array
   */

  protected $_lookup_cache = array();

  /**
   * Mixins
   * @var array
   */

  protected $_mixins = array();
 
  /**
   * <code>true</code> if the parser is parsing a mix-in
   * @var boolean
   */

  protected $_in_mixin = false;

  /**
   * The currently parsed mix-in
   * @var Mixin
   */

  protected $_current_mixin;

  /**
   * The nesting level in the currently parsed mix-in, 0 again when
   * it just ended
   * @var int
   */

  protected $_mixin_level = 0;
 
  public function __construct ($code)
  {
    $lexer = new \de\pointedears\string\parser\Lexer($code);

    $nl = 'n';
    $escape = '\\[0-9a-f]{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-f]';
    $lexer->addTokens(array(
      '(?P<PHP><\?php.*?\?>)',
      "/\*[^*]*\*+([^/*][^*]*\*+)*/",
      "\"([^\n\r\f\\\"]|\\{$nl}|{$escape})*\"",
      "'([^\n\r\f\\']|\\{$nl}|{$escape})*'",
//       '\.(?P<mixin_def>[\w-]+)\s*\((?P<params>.*?)\)\s*(?!;)',
//       '\.(?P<mixin_call>[\w-]+)\s*\((?P<args>.*)?\)\s*;\\s*',
      '[{}]',
//       '\$(?P<name>\w+)\s*=\s*(?P<value>\'.+?\'|".+"|.+?)\s*;\s*',
//       '\$(?P<ref>\w+)'
    ));

    $lexer->ignoreCase = true;
    $lexer->dotAll = true;

    parent::__construct($lexer);
    $this->_compiled = $code;
  }
 
  protected function defineVar($name, $value)
  {
    $scope_level = $this->_scope_level;

    if (defined('DEBUG') && DEBUG > 0)
    {
      echo "<?php // Least.php: Found definition \${$name} = `{$value}'"
      . " at scope level {$scope_level} ?>\n";
    }

    $this->_vars[$scope_level][$name] = $value;
    $this->_lookup_cache[$name] = $value;
  }

  /**
   * Resolves a template variable reference against the template scope chain
   * @param string $ref
   * @return mixed
   */

  protected function resolveVar ($name)
  {
    $scope_level = $this->_scope_level + 1;
    $orig_level = $scope_level - 1;

    if (defined('DEBUG') && DEBUG > 0)
    {
      echo "<?php // Least.php: Found reference \${$name} at scope level "
      . ($scope_level - 1) . " ?>\n";
    }

    /* Try the lookup cache for this scope level first */
    if (array_key_exists($name, $this->_lookup_cache))
    {
      $value = $this->_lookup_cache[$name];

      if (defined('DEBUG') && DEBUG > 1)
      {
        echo "<?php // Least.php: Resolved reference \${$name}"
        . " at scope level {$orig_level}"
        . " from lookup cache, value `{$value}' ?>\n";
      }

      return $value;
    }

    while ($scope_level--)
    {
      if (array_key_exists($name, $this->_vars[$scope_level]))
      {
        $value = $this->_vars[$scope_level][$name];
        $this->_lookup_cache[$name] = $value;

        if (defined('DEBUG') && DEBUG > 1)
        {
          echo "<?php // Least.php: Resolved reference \${$name}"
          . " from scope level {$orig_level}"
          . " at scope level {$scope_level}, value `{$value}' ?>\n";
        }

        return $value;
      }
    }

    if (defined('DEBUG') && DEBUG > 0)
    {
      echo "<?php // Least.php: WARNING: Unresolved reference \${$name}"
      . " at scope level {$orig_level}. Variable not in scope? ?>\n";
    }
  }

  protected function defineMixin($name, $params)
  {
    $scope_level = $this->_scope_level;

    if (defined('DEBUG') && DEBUG > 0)
    {
      echo "<?php // Least.php: Found mixin definition {$name}($params)"
      . " at scope level {$scope_level}. ?>\n";
    }

    $mixin = new Mixin($params);
    $this->_mixins[$name] = $mixin;

    return $mixin;
  }

  protected function callMixin($name, $args)
  {
    return $this->$_mixins[$name]->apply($args);
  }

  public function parseToken ($matches)
  {
    ini_set('html_errors', 0);
//     var_dump($matches);
//     /* Get match offset and match length */
//     $match = $matches[0];
//     $match_start = $match[1];
//     $match_length = mb_strlen($match[0]);

//     /* Transform $matches to the format it is usually set as (without PREG_OFFSET_CAPTURE set) */
//     $matches = array_map(function ($m) {
//       return $m[0];
//     }, $matches);
//     $match = $matches[0];

//     if (defined('DEBUG') && DEBUG > 1)
//     {
//       echo print_r(array(
//         'match_start' => $match_start,
//         'match_length' => $match_length,
//         'matches' => $matches,
//         'in_mixin' => $in_mixin,
//         'scope_level' => $this->_scope_level,
//       ), true) . "\n";
//     }

//     if (isset($matches['mixin_def']) && $matches['mixin_def'])
//     {
//       $this->_in_mixin = true;
//       $this->_current_mixin = $this->defineMixin($matches['mixin_def'], $matches['params']);
//       // $code = self::replace(mb_strlen($match), '', $code, $match_start);
//       // $match_length = 0;
//     }
//     else if (isset($matches['mixin_call']) && $matches['mixin_call'])
//     {
//       //return
//       $this->callMixin($matches['mixin_call'], $matches['args']);
//       //$code = self::replace(mb_strlen($match), '', $code, $match_start);
//       //$match_length = 0;
//       $mixin_start = $match_start;
//     }
//     else if ($match === '{')
//     {
//       ++$this->_scope_level;
//       $this->_vars[$this->_scope_level] = array();

//       if ($this->_in_mixin)
//       {
//         if ($mixin_body_start === 0)
//         {
//           $mixin_body_start = $match_start;
//         }

//         ++$this->_mixin_level;
//       }
//     }
//     else if ($match === '}')
//     {
//       --$this->_scope_level;
//       $this->_lookup_cache = $this->_vars[$this->_scope_level];

//       if ($this->_in_mixin)
//       {
//         --$this->_mixin_level;
//         if ($this->_mixin_level === 0)
//         {
//           $this->_in_mixin = false;
//           $this->_current_mixin->setBody(substr($code, $mixin_start + 1, $match_start - $mixin_start - 1));
//           echo '"'.print_r(substr($code, $mixin_start, $match_start - $mixin_start + 1), true) . "\"\n";
//           //             $code = self::replace($match_start - $mixin_start + 1, '', $code, $mixin_start);
//           //             $match_length = 0;
//         }
//       }
//     }
//     else if (isset($matches['name']) && $matches['name'])
//     {
//       $this->defineVar($matches['name'], $matches['value']);
//       $code = self::replace(mb_strlen($match), '', $code, $match_start);
//       $match_length = 0;
//     }
//     else if (isset($matches['ref']) && $matches['ref'])
//     {
//       $name = $matches['ref'];
       
//       if (!$in_mixin || !in_array($name, $mixin->params, true))
//       {
//         $value = $this->resolveVar($matches['ref']);
//         $this->_lexer->text = self::replace(mb_strlen($match), $value, $code, $match_start);
//         $this->_lexer->offset -= $match_length + mb_strlen($value);
//       }
//     }
  }
 
  public function getCompiled ()
  {
    return $this->_compiled;
  }
}

class Mixin extends \de\pointedears\Base
{
  /**
   * Parameters of the mixin
   * @var array
   */

  protected $_params;

  /**
   * Body of the mixin
   * @var string
   */

  protected $_body = '';

  /**
   * @param string $params
   */

  public function __construct ($params, $body = '')
  {
    $this->_params = is_array($params)
    ? $params
    : array_map(
      function ($e) {
      return preg_replace('/^\$/', '', $e);
    },
    preg_split('/\s*,\s*/', $params));

    if ($body !== '')
    {
      $this->setBody($body);
    }
  }

  public function setBody ($body)
  {
    $this->body = (string) $body;
  }

  /**
   * @param string $arguments
   */

  public function apply ($arguments)
  {
    return '';
  }
}