Plugin Directory

Changeset 2262049


Ignore:
Timestamp:
03/16/2020 09:54:42 PM (6 years ago)
Author:
johnnytee
Message:

1.3.10

Location:
wordpress-notification-bar/trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • wordpress-notification-bar/trunk/framework/framework.php

    r1480727 r2262049  
    186186                }
    187187                if ( $v[ 'menu_type' ] == 'add_submenu_page' ) {
    188                     $this->pages[ ] = call_user_func_array( $v[ 'menu_type' ], array(
     188                    // $this->pages[ ] = call_user_func_array( $v[ 'menu_type' ], array(
     189                    //     $v[ 'parent_slug' ],
     190                    //     $v[ 'page_name' ],
     191                    //     $v[ 'menu_name' ],
     192                    //     $v[ 'capability' ],
     193                    //     $v[ 'menu_slug' ],
     194                    //     $v[ 'callback' ],
     195                    //     10
     196                    // ) );
     197
     198                    $this->pages[ ] = add_submenu_page(
    189199                        $v[ 'parent_slug' ],
    190200                        $v[ 'page_name' ],
     
    192202                        $v[ 'capability' ],
    193203                        $v[ 'menu_slug' ],
    194                         $v[ 'callback' ]
    195                     ) );
     204                        $v[ 'callback' ],
     205                        10
     206                    );
    196207                } else {
    197208                    $this->pages[ ] = call_user_func_array( $v[ 'menu_type' ], array(
     
    201212                        $v[ 'menu_slug' ],
    202213                        $v[ 'callback' ],
    203                         $v[ 'icon_url' ]
     214                        10
    204215                    ) );
    205216                }
     
    295306        ?>
    296307        <div class="wrap columns-2 seed_wnb">
    297         <?php screen_icon(); ?>
     308     
    298309            <h2><?php echo $this->plugin_name; ?></h2>
    299310            <?php $this->plugin_options_tabs(); ?>
  • wordpress-notification-bar/trunk/inc/lib/seed-wnb-lessc.inc.php

    r1465044 r2262049  
    22
    33/**
    4  * lessphp v0.3.4-2
     4 * lessphp v0.5.0
    55 * http://leafo.net/lessphp
    66 *
    7  * LESS css compiler, adapted from http://lesscss.org
     7 * LESS CSS compiler, adapted from http://seed_wnb_lesscss.org
    88 *
    9  * Copyright 2012, Leaf Corcoran <leafot@gmail.com>
     9 * Copyright 2013, Leaf Corcoran <leafot@gmail.com>
    1010 * Licensed under MIT or GPLv3, see LICENSE
    1111 */
     
    1313
    1414/**
    15  * The less compiler and parser.
     15 * The LESS compiler and parser.
    1616 *
    17  * Converting LESS to CSS is a two stage process. First the incoming document
    18  * must be parsed. Parsing creates a tree in memory that represents the
    19  * structure of the document. Then, the tree of the document is recursively
    20  * compiled into the CSS text. The compile step has an implicit step called
    21  * reduction, where values are brought to their lowest form before being
    22  * turned to text, eg. mathematical equations are solved, and variables are
    23  * dereferenced.
     17 * Converting LESS to CSS is a three stage process. The incoming file is parsed
     18 * by `seed_wnb_lessc_parser` into a syntax tree, then it is compiled into another tree
     19 * representing the CSS structure by `seed_wnb_lessc`. The CSS tree is fed into a
     20 * formatter, like `seed_wnb_lessc_formatter` which then outputs CSS as a string.
    2421 *
    25  * The parsing stage produces the final structure of the document, for this
    26  * reason mixins are mixed in and attribute accessors are referenced during
    27  * the parse step. A reduction is done on the mixed in block as it is mixed in.
     22 * During the first compile, all values are *reduced*, which means that their
     23 * types are brought to the lowest form before being dump as strings. This
     24 * handles math equations, variable dereferences, and the like.
    2825 *
    29  *  See the following:
    30  *    - entry point for parsing and compiling: lessc::parse()
    31  *    - parsing: lessc::parseChunk()
    32  *    - compiling: lessc::compileBlock()
     26 * The `parse` function of `seed_wnb_lessc` is the entry point.
    3327 *
     28 * In summary:
     29 *
     30 * The `seed_wnb_lessc` class creates an instance of the parser, feeds it LESS code,
     31 * then transforms the resulting tree to a CSS tree. This class also holds the
     32 * evaluation context, such as all available mixins and variables at any given
     33 * time.
     34 *
     35 * The `seed_wnb_lessc_parser` class is only concerned with parsing its input.
     36 *
     37 * The `seed_wnb_lessc_formatter` takes a CSS tree, and dumps it to a formatted string,
     38 * handling things like indentation.
    3439 */
    3540class seed_wnb_lessc {
    36     public static $VERSION = "v0.3.4-2";
    37     protected $buffer;
    38     protected $count;
    39     protected $line;
    40     protected $libFunctions = array();
    41     static protected $nextBlockId = 0;
    42 
    43     static protected $TRUE = array("keyword", "true");
    44     static protected $FALSE = array("keyword", "false");
    45 
    46     public $indentLevel;
    47     public $indentChar = '  ';
    48 
    49     protected $env = null;
    50 
    51     protected $allParsedFiles = array();
    52 
    53     public $vPrefix = '@'; // prefix of abstract properties
    54     public $mPrefix = '$'; // prefix of abstract blocks
    55     public $imPrefix = '!'; // special character to add !important
    56     public $parentSelector = '&';
    57 
    58     // set to the parser that generated the current line when compiling
    59     // so we know how to create error messages
    60     protected $sourceParser = null;
    61 
    62     static protected $precedence = array(
    63         '=<' => 0,
    64         '>=' => 0,
    65         '=' => 0,
    66         '<' => 0,
    67         '>' => 0,
    68 
    69         '+' => 1,
    70         '-' => 1,
    71         '*' => 2,
    72         '/' => 2,
    73         '%' => 2,
    74     );
    75     static protected $operatorString; // regex string to match any of the operators
    76 
    77     // types that have delayed computation
    78     static protected $dtypes = array('expression', 'variable',
    79         'function', 'negative', 'list', 'lookup');
    80 
    81     // these properties will supress division unless it's inside parenthases
    82     static protected $supressDivisionProps = array('/border-radius$/i', '/^font$/i');
    83 
    84     /**
    85      * @link http://www.w3.org/TR/css3-values/
    86      */
    87     static protected $units = array(
    88         'em', 'ex', 'px', 'gd', 'rem', 'vw', 'vh', 'vm', 'ch', // Relative length units
    89         'in', 'cm', 'mm', 'pt', 'pc', // Absolute length units
    90         '%', // Percentages
    91         'deg', 'grad', 'rad', 'turn', // Angles
    92         'ms', 's', // Times
    93         'Hz', 'kHz', //Frequencies
    94     );
    95    
    96     public $importDisabled = false;
    97     public $importDir = '';
    98 
    99     public $compat = false; // lessjs compatibility mode, does nothing right now
    100 
    101     /**
    102      * if we are in an expression then we don't need to worry about parsing font shorthand
    103      * $inExp becomes true after the first value in an expression, or if we enter parens
    104      */
    105     protected $inExp = false;
    106 
    107     /**
    108      * if we are in parens we can be more liberal with whitespace around operators because
    109      * it must evaluate to a single value and thus is less ambiguous.
    110      *
    111      * Consider:
    112      *     property1: 10 -5; // is two numbers, 10 and -5
    113      *     property2: (10 -5); // should evaluate to 5
    114      */
    115     protected $inParens = false;
    116 
    117     /**
    118      * Parse a single chunk off the head of the buffer and place it.
    119      * @return false when the buffer is empty, or when there is an error.
    120      *
    121      * This function is called repeatedly until the entire document is
    122      * parsed.
    123      *
    124      * This parser is most similar to a recursive descent parser. Single
    125      * functions represent discrete grammatical rules for the language, and
    126      * they are able to capture the text that represents those rules.
    127      *
    128      * Consider the function lessc::keyword(). (all parse functions are
    129      * structured the same)
    130      *
    131      * The function takes a single reference argument. When calling the
    132      * function it will attempt to match a keyword on the head of the buffer.
    133      * If it is successful, it will place the keyword in the referenced
    134      * argument, advance the position in the buffer, and return true. If it
    135      * fails then it won't advance the buffer and it will return false.
    136      *
    137      * All of these parse functions are powered by lessc::match(), which behaves
    138      * the same way, but takes a literal regular expression. Sometimes it is
    139      * more convenient to use match instead of creating a new function.
    140      *
    141      * Because of the format of the functions, to parse an entire string of
    142      * grammatical rules, you can chain them together using &&.
    143      *
    144      * But, if some of the rules in the chain succeed before one fails, then
    145      * the buffer position will be left at an invalid state. In order to
    146      * avoid this, lessc::seek() is used to remember and set buffer positions.
    147      *
    148      * Before parsing a chain, use $s = $this->seek() to remember the current
    149      * position into $s. Then if a chain fails, use $this->seek($s) to
    150      * go back where we started.
    151      */
    152     function parseChunk() {
    153         if (empty($this->buffer)) return false;
    154         $s = $this->seek();
    155        
    156         // setting a property
    157         if ($this->keyword($key) && $this->assign() &&
    158             $this->propertyValue($value, $key) && $this->end())
    159         {
    160             $this->append(array('assign', $key, $value), $s);
    161             return true;
    162         } else {
    163             $this->seek($s);
    164         }
    165 
    166         // look for special css blocks
    167         if ($this->env->parent == null && $this->literal('@', false)) {
    168             $this->count--;
    169 
    170             // a font-face block
    171             if ($this->literal('@font-face') && $this->literal('{')) {
    172                 $b = $this->pushSpecialBlock('@font-face');
    173                 return true;
    174             } else {
    175                 $this->seek($s);
    176             }
    177 
    178             // charset
    179             if ($this->literal('@charset') && $this->propertyValue($value) &&
    180                 $this->end())
    181             {
    182                 $this->append(array('charset', $value), $s);
    183                 return true;
    184             } else {
    185                 $this->seek($s);
    186             }
    187 
    188 
    189             // media
    190             if ($this->literal('@media') && $this->mediaTypes($types) &&
    191                 $this->literal('{'))
    192             {
    193                 $b = $this->pushSpecialBlock('@media');
    194                 $b->media = $types;
    195                 return true;
    196             } else {
    197                 $this->seek($s);
    198             }
    199 
    200             // css animations
    201             if ($this->match('(@(-[a-z]+-)?keyframes)', $m) &&
    202                 $this->propertyValue($value) && $this->literal('{'))
    203             {
    204                 $b = $this->pushSpecialBlock(trim($m[0]));
    205                 $b->keyframes = $value;
    206                 return true;
    207             } else {
    208                 $this->seek($s);
    209             }
    210         }
    211 
    212         if (isset($this->env->keyframes)) {
    213             if ($this->match("(to|from|[0-9]+%)", $m) && $this->literal('{')) {
    214                 $this->pushSpecialBlock($m[1]);
    215                 return true;
    216             } else {
    217                 $this->seek($s);
    218             }
    219         }
    220 
    221         // setting a variable
    222         if ($this->variable($var) && $this->assign() &&
    223             $this->propertyValue($value) && $this->end())
    224         {
    225             $this->append(array('assign', $var, $value), $s);
    226             return true;
    227         } else {
    228             $this->seek($s);
    229         }
    230 
    231         if ($this->import($url, $media)) {
    232             // don't check .css files
    233             if (empty($media) && substr_compare($url, '.css', -4, 4) !== 0) {
    234                 if ($this->importDisabled) {
    235                     $this->append(array('raw', '/* import disabled */'));
    236                 } else {
    237                     $path = $this->findImport($url);
    238                     if (!is_null($path)) {
    239                         $this->append(array('import', $path), $s);
    240                         return true;
    241                     }
    242                 }
    243             }
    244 
    245             $this->append(array('raw', '@import url("'.$url.'")'.
    246                 ($media ? ' '.$media : '').';'), $s);
    247             return true;
    248         }
    249 
    250         // opening parametric mixin
    251         if ($this->tag($tag, true) && $this->argumentDef($args, $is_vararg) &&
    252             ($this->guards($guards) || true) &&
    253             $this->literal('{'))
    254         {
    255             $block = $this->pushBlock($this->fixTags(array($tag)));
    256             $block->args = $args;
    257             $block->is_vararg = $is_vararg;
    258             if (!empty($guards)) $block->guards = $guards;
    259             return true;
    260         } else {
    261             $this->seek($s);
    262         }
    263 
    264         // opening a simple block
    265         if ($this->tags($tags) && $this->literal('{')) {
    266             $tags = $this->fixTags($tags);
    267             $this->pushBlock($tags);
    268             return true;
    269         } else {
    270             $this->seek($s);
    271         }
    272 
    273         // closing a block
    274         if ($this->literal('}')) {
    275             try {
    276                 $block = $this->pop();
    277             } catch (exception $e) {
    278                 $this->seek($s);
    279                 $this->throwError($e->getMessage());
    280             }
    281 
    282             $hidden = true;
    283             if (!isset($block->args)) foreach ($block->tags as $tag) {
    284                 if (!is_string($tag) || $tag{0} != $this->mPrefix) {
    285                     $hidden = false;
    286                     break;
    287                 }
    288             }
    289 
    290             if (!$hidden) $this->append(array('block', $block), $s);
    291 
    292             foreach ($block->tags as $tag) {
    293                 if (is_string($tag)) {
    294                     $this->env->children[$tag][] = $block;
    295                 }
    296             }
    297 
    298             return true;
    299         }
    300 
    301         // mixin
    302         if ($this->mixinTags($tags) &&
    303             ($this->argumentValues($argv) || true) && $this->end())
    304         {
    305             $tags = $this->fixTags($tags);
    306             $this->append(array('mixin', $tags, $argv), $s);
    307             return true;
    308         } else {
    309             $this->seek($s);
    310         }
    311 
    312         // spare ;
    313         if ($this->literal(';')) return true;
    314 
    315         return false; // got nothing, throw error
    316     }
    317 
    318     function fixTags($tags) {
    319         // move @ tags out of variable namespace
    320         foreach ($tags as &$tag) {
    321             if ($tag{0} == $this->vPrefix) $tag[0] = $this->mPrefix;
    322         }
    323         return $tags;
    324     }
    325 
    326     // attempts to find the path of an import url, returns null for css files
    327     function findImport($url) {
    328         foreach ((array)$this->importDir as $dir) {
    329             $full = $dir.(substr($dir, -1) != '/' ? '/' : '').$url;
    330             if ($this->fileExists($file = $full.'.less') || $this->fileExists($file = $full)) {
    331                 return $file;
    332             }
    333         }
    334 
    335         return null;
    336     }
    337 
    338     function fileExists($name) {
    339         // sym link workaround
    340         return file_exists($name) || file_exists(realpath(preg_replace('/\w+\/\.\.\//', '', $name)));
    341     }
    342 
    343     // a list of expressions
    344     function expressionList(&$exps) {
    345         $values = array(); 
    346 
    347         while ($this->expression($exp)) {
    348             $values[] = $exp;
    349         }
    350        
    351         if (count($values) == 0) return false;
    352 
    353         $exps = $this->compressList($values, ' ');
    354         return true;
    355     }
    356 
    357     /**
    358      * Attempt to consume an expression.
    359      * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
    360      */
    361     function expression(&$out) {
    362         $s = $this->seek();
    363         if ($this->literal('(') && ($this->inExp = $this->inParens = true) && $this->expression($exp) && $this->literal(')')) {
    364             $lhs = $exp;
    365         } elseif ($this->seek($s) && $this->value($val)) {
    366             $lhs = $val;
    367         } else {
    368             $this->inParens = $this->inExp = false;
    369             $this->seek($s);
    370             return false;
    371         }
    372 
    373         $out = $this->expHelper($lhs, 0);
    374         $this->inParens = $this->inExp = false;
    375         return true;
    376     }
    377 
    378     /**
    379      * recursively parse infix equation with $lhs at precedence $minP
    380      */
    381     function expHelper($lhs, $minP) {
    382         $this->inExp = true;
    383         $ss = $this->seek();
    384 
    385         // if there was whitespace before the operator, then we require whitespace after
    386         // the operator for it to be a mathematical operator.
    387 
    388         $needWhite = false;
    389         if (!$this->inParens && preg_match('/\s/', $this->buffer{$this->count - 1})) {
    390             $needWhite = true;
    391         }
    392 
    393         // try to find a valid operator
    394         while ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) {
    395             if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/") {
    396                 foreach (self::$supressDivisionProps as $pattern) {
    397                     if (preg_match($pattern, $this->env->currentProperty)) {
    398                         $this->env->supressedDivision = true;
    399                         break 2;
    400                     }
    401                 }
    402             }
    403 
    404             // get rhs
    405             $s = $this->seek();
    406             $p = $this->inParens;
    407             if ($this->literal('(') && ($this->inParens = true) && $this->expression($exp) && $this->literal(')')) {
    408                 $this->inParens = $p;
    409                 $rhs = $exp;
    410             } else {
    411                 $this->inParens = $p;
    412                 if ($this->seek($s) && $this->value($val)) {
    413                     $rhs = $val;
    414                 } else {
    415                     break;
    416                 }
    417             }
    418 
    419             // peek for next operator to see what to do with rhs
    420             if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) {
    421                 $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
    422             }
    423 
    424             // don't evaluate yet if it is dynamic
    425             if (in_array($rhs[0], self::$dtypes) || in_array($lhs[0], self::$dtypes))
    426                 $lhs = array('expression', $m[1], $lhs, $rhs);
    427             else
    428                 $lhs = $this->evaluate($m[1], $lhs, $rhs);
    429 
    430             $ss = $this->seek();
    431 
    432             $needWhite = false;
    433             if (!$this->inParens && preg_match('/\s/', $this->buffer{$this->count - 1})) {
    434                 $needWhite = true;
    435             }
    436         }
    437         $this->seek($ss);
    438 
    439         return $lhs;
    440     }
    441 
    442     // consume a list of values for a property
    443     function propertyValue(&$value, $keyName=null) {
    444         $values = array(); 
    445 
    446         if (!is_null($keyName)) $this->env->currentProperty = $keyName;
    447        
    448         $s = null;
    449         while ($this->expressionList($v)) {
    450             $values[] = $v;
    451             $s = $this->seek();
    452             if (!$this->literal(',')) break;
    453         }
    454 
    455         if ($s) $this->seek($s);
    456 
    457         if (!is_null($keyName)) unset($this->env->currentProperty);
    458 
    459         if (count($values) == 0) return false;
    460 
    461         $value = $this->compressList($values, ', ');
    462         return true;
    463     }
    464 
    465     // a single value
    466     function value(&$value) {
    467         // try a unit
    468         if ($this->unit($value)) return true;   
    469 
    470         // see if there is a negation
    471         $s = $this->seek();
    472         if ($this->literal('-', false)) {
    473             $value = null;
    474             if ($this->variable($var)) {
    475                 $value = array('variable', $var);
    476             } elseif ($this->buffer{$this->count} == "(" && $this->expression($exp)) {
    477                 $value = $exp;
    478             } else {
    479                 $this->seek($s);
    480             }
    481 
    482             if (!is_null($value)) {
    483                 $value = array('negative', $value);
    484                 return true;
    485             }
    486         } else {
    487             $this->seek($s);
    488         }
    489 
    490         // accessor
    491         // must be done before color
    492         // this needs negation too
    493         if ($this->accessor($a)) {
    494             $a[1] = $this->fixTags($a[1]);
    495             $value = $a;
    496             return true;
    497         }
    498        
    499         // color
    500         if ($this->color($value)) return true;
    501 
    502         // css function
    503         // must be done after color
    504         if ($this->func($value)) return true;
    505 
    506         // string
    507         if ($this->lstring($tmp, $d)) {
    508             $value = array('string', $d.$tmp.$d);
    509             return true;
    510         }
    511 
    512         // try a keyword
    513         if ($this->keyword($word)) {
    514             $value = array('keyword', $word);
    515             return true;
    516         }
    517 
    518         // try a variable
    519         if ($this->variable($var)) {
    520             $value = array('variable', $var);
    521             return true;
    522         }
    523 
    524         // unquote string
    525         if ($this->literal("~") && $this->lstring($value, $d)) {
    526             $value = array("escape", $value);
    527             return true;
    528         } else {
    529             $this->seek($s);
    530         }
    531 
    532         // css hack: \0
    533         if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
    534             $value = array('keyword', '\\'.$m[1]);
    535             return true;
    536         } else {
    537             $this->seek($s);
    538         }
    539 
    540         // the spare / when supressing division
    541         if (!empty($this->env->supressedDivision)) {
    542             unset($this->env->supressedDivision);
    543             if ($this->literal("/")) {
    544                 $value = array('keyword', '/');
    545                 return true;
    546             }
    547         }
    548 
    549         return false;
    550     }
    551 
    552     // an import statement
    553     function import(&$url, &$media) {
    554         $s = $this->seek();
    555         if (!$this->literal('@import')) return false;
    556 
    557         // @import "something.css" media;
    558         // @import url("something.css") media;
    559         // @import url(something.css) media;
    560 
    561         if ($this->literal('url(')) $parens = true; else $parens = false;
    562 
    563         if (!$this->lstring($url)) {
    564             if ($parens && $this->to(')', $url)) {
    565                 $parens = false; // got em
    566             } else {
    567                 $this->seek($s);
    568                 return false;
    569             }
    570         }
    571 
    572         if ($parens && !$this->literal(')')) {
    573             $this->seek($s);
    574             return false;
    575         }
    576 
    577         // now the rest is media
    578         return $this->to(';', $media, false, true);
    579     }
    580 
    581     // a list of media types, very lenient
    582     function mediaTypes(&$parts) {
    583         $parts = array();
    584         while ($this->to("(", $chunk, false, "[^{]")) {
    585             $parts[] = array('raw', $chunk."(");
    586             $s = $this->seek();
    587             if ($this->keyword($name) && $this->assign() &&
    588                 $this->propertyValue($value))
    589             {
    590                 $parts[] = array('assign', $name, $value);
    591             } else {
    592                 $this->seek($s);
    593             }
    594         }
    595 
    596         if ($this->to('{', $rest, true, true)) {
    597             $parts[] = array('raw', $rest);
    598             return true;
    599         }
    600 
    601         $parts = null;
    602         return false;
    603     }
    604 
    605     // a scoped value accessor
    606     // .hello > @scope1 > @scope2['value'];
    607     function accessor(&$var) {
    608         $s = $this->seek();
    609 
    610         if (!$this->tags($scope, true, '>') || !$this->literal('[')) {
    611             $this->seek($s);
    612             return false;
    613         }
    614 
    615         // either it is a variable or a property
    616         // why is a property wrapped in quotes, who knows!
    617         if ($this->variable($name)) {
    618             // ~
    619         } elseif ($this->literal("'") && $this->keyword($name) && $this->literal("'")) {
    620             // .. $this->count is messed up if we wanted to test another access type
    621         } else {
    622             $this->seek($s);
    623             return false;
    624         }
    625 
    626         if (!$this->literal(']')) {
    627             $this->seek($s);
    628             return false;
    629         }
    630 
    631         $var = array('lookup', $scope, $name);
    632         return true;
    633     }
    634 
    635     // a string
    636     function lstring(&$string, &$d = null) {
    637         $s = $this->seek();
    638         if ($this->literal('"', false)) {
    639             $delim = '"';
    640         } elseif ($this->literal("'", false)) {
    641             $delim = "'";
    642         } else {
    643             return false;
    644         }
    645 
    646         if (!$this->to($delim, $string)) {
    647             $this->seek($s);
    648             return false;
    649         }
    650        
    651         $d = $delim;
    652         return true;
    653     }
    654 
    655     /**
    656      * Consume a number and optionally a unit.
    657      * Can also consume a font shorthand if it is a simple case.
    658      * $allowed restricts the types that are matched.
    659      */
    660     function unit(&$unit, $allowed = null) {
    661         if (!$allowed) $allowed = self::$units;
    662 
    663         if ($this->match('(-?[0-9]*(\.)?[0-9]+)('.implode('|', $allowed).')?', $m)) {
    664             if (!isset($m[3])) $m[3] = 'number';
    665             $unit = array($m[3], $m[1]);
    666 
    667             return true;
    668         }
    669 
    670         return false;
    671     }
    672 
    673     // a # color
    674     function color(&$out) {
    675         $color = array('color');
    676 
    677         if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
    678             if (isset($m[3])) {
    679                 $num = $m[3];
    680                 $width = 16;
    681             } else {
    682                 $num = $m[2];
    683                 $width = 256;
    684             }
    685 
    686             $num = hexdec($num);
    687             foreach (array(3,2,1) as $i) {
    688                 $t = $num % $width;
    689                 $num /= $width;
    690 
    691                 $color[$i] = $t * (256/$width) + $t * floor(16/$width);
    692             }
    693            
    694             $out = $color;
    695             return true;
    696         }
    697 
    698         return false;
    699     }
    700 
    701     // consume a list of property values delimited by ; and wrapped in ()
    702     function argumentValues(&$args, $delim = ',') {
    703         $s = $this->seek();
    704         if (!$this->literal('(')) return false;
    705 
    706         $values = array();
    707         while (true) {
    708             if ($this->expressionList($value)) $values[] = $value;
    709             if (!$this->literal($delim)) break;
    710             else {
    711                 if ($value == null) $values[] = null;
    712                 $value = null;
    713             }
    714         }   
    715 
    716         if (!$this->literal(')')) {
    717             $this->seek($s);
    718             return false;
    719         }
    720        
    721         $args = $values;
    722         return true;
    723     }
    724 
    725     // consume an argument definition list surrounded by ()
    726     // each argument is a variable name with optional value
    727     // or at the end a ... or a variable named followed by ...
    728     function argumentDef(&$args, &$is_vararg, $delim = ',') {
    729         $s = $this->seek();
    730         if (!$this->literal('(')) return false;
    731 
    732         $values = array();
    733 
    734         $is_vararg = false;
    735         while (true) {
    736             if ($this->literal("...")) {
    737                 $is_vararg = true;
    738                 break;
    739             }
    740 
    741             if ($this->variable($vname)) {
    742                 $arg = array("arg", $vname);
    743                 $ss = $this->seek();
    744                 if ($this->assign() && $this->expressionList($value)) {
    745                     $arg[] = $value;
    746                 } else {
    747                     $this->seek($ss);
    748                     if ($this->literal("...")) {
    749                         $arg[0] = "rest";
    750                         $is_vararg = true;
    751                     }
    752                 }
    753                 $values[] = $arg;
    754                 if ($is_vararg) break;
    755                 continue;
    756             }
    757 
    758             if ($this->value($literal)) {
    759                 $values[] = array("lit", $literal);
    760             }
    761 
    762             if (!$this->literal($delim)) break;
    763         }
    764 
    765         if (!$this->literal(')')) {
    766             $this->seek($s);
    767             return false;
    768         }
    769 
    770         $args = $values;
    771 
    772         return true;
    773     }
    774 
    775     // consume a list of tags
    776     // this accepts a hanging delimiter
    777     function tags(&$tags, $simple = false, $delim = ',') {
    778         $tags = array();
    779         while ($this->tag($tt, $simple)) {
    780             $tags[] = $tt;
    781             if (!$this->literal($delim)) break;
    782         }
    783         if (count($tags) == 0) return false;
    784 
    785         return true;
    786     }
    787 
    788     // list of tags of specifying mixin path
    789     // optionally separated by > (lazy, accepts extra >)
    790     function mixinTags(&$tags) {
    791         $s = $this->seek();
    792         $tags = array();
    793         while ($this->tag($tt, true)) {
    794             $tags[] = $tt;
    795             $this->literal(">");
    796         }
    797 
    798         if (count($tags) == 0) return false;
    799 
    800         return true;
    801     }
    802 
    803     // a bracketed value (contained within in a tag definition)
    804     function tagBracket(&$value) {
    805         $s = $this->seek();
    806         if ($this->literal('[') && $this->to(']', $c, true) && $this->literal(']', false)) {
    807             $value = '['.$c.']';
    808             // whitespace?
    809             if ($this->match('', $_)) $value .= $_[0];
    810 
    811             // escape parent selector
    812             $value = str_replace($this->parentSelector, "&&", $value);
    813             return true;
    814         }
    815 
    816         $this->seek($s);
    817         return false;
    818     }
    819 
    820     function tagExpression(&$value) {
    821         $s = $this->seek();
    822         if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) {
    823             $value = array('exp', $exp);
    824             return true;
    825         }
    826 
    827         $this->seek($s);
    828         return false;
    829     }
    830 
    831     // a single tag
    832     function tag(&$tag, $simple = false) {
    833         if ($simple)
    834             $chars = '^,:;{}\][>\(\) "\'';
    835         else
    836             $chars = '^,;{}["\'';
    837 
    838         if (!$simple && $this->tagExpression($tag)) {
    839             return true;
    840         }
    841 
    842         $tag = '';
    843         while ($this->tagBracket($first)) $tag .= $first;
    844         while ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) {
    845             $tag .= $m[1];
    846             if ($simple) break;
    847 
    848             while ($this->tagBracket($brack)) $tag .= $brack;
    849         }
    850         $tag = trim($tag);
    851         if ($tag == '') return false;
    852 
    853         return true;
    854     }
    855 
    856     // a css function
    857     function func(&$func) {
    858         $s = $this->seek();
    859 
    860         if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) {
    861             $fname = $m[1];
    862 
    863             $s_pre_args = $this->seek();
    864 
    865             $args = array();
    866             while (true) {
    867                 $ss = $this->seek();
    868                 // this ugly nonsense is for ie filter properties
    869                 if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
    870                     $args[] = array('list', '=', array(array('keyword', $name), $value));
    871                 } else {
    872                     $this->seek($ss);
    873                     if ($this->expressionList($value)) {
    874                         $args[] = $value;
    875                     }
    876                 }
    877 
    878                 if (!$this->literal(',')) break;
    879             }
    880             $args = array('list', ',', $args);
    881 
    882             if ($this->literal(')')) {
    883                 $func = array('function', $fname, $args);
    884                 return true;
    885             } elseif ($fname == 'url') {
    886                 // couldn't parse and in url? treat as string
    887                 $this->seek($s_pre_args);
    888                 if ($this->to(')', $content, true) && $this->literal(')')) {
    889                     $func = array('function', $fname,array('string', $content));
    890                     return true;
    891                 }
    892             }
    893         }
    894 
    895         $this->seek($s);
    896         return false;
    897     }
    898 
    899     // consume a less variable
    900     function variable(&$name) {
    901         $s = $this->seek();
    902         if ($this->literal($this->vPrefix, false) &&
    903             ($this->variable($sub) || $this->keyword($name)))
    904         {
    905             if (!empty($sub)) {
    906                 $name = array('variable', $sub);
    907             } else {
    908                 $name = $this->vPrefix.$name;
    909             }
    910             return true;   
    911         }
    912 
    913         $name = null;
    914         $this->seek($s);
    915         return false;
    916     }
    917 
    918     /**
    919      * Consume an assignment operator
    920      * Can optionally take a name that will be set to the current property name
    921      */
    922     function assign($name = null) {
    923         if ($name) $this->currentProperty = $name;
    924         return $this->literal(':') || $this->literal('=');
    925     }
    926 
    927     // consume a keyword
    928     function keyword(&$word) {
    929         if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
    930             $word = $m[1];
    931             return true;
    932         }
    933         return false;
    934     }
    935 
    936     // consume an end of statement delimiter
    937     function end() {
    938         if ($this->literal(';'))
    939             return true;
    940         elseif ($this->count == strlen($this->buffer) || $this->buffer{$this->count} == '}') {
    941             // if there is end of file or a closing block next then we don't need a ;
    942             return true;
    943         }
    944         return false;
    945     }
    946 
    947     function guards(&$guards) {
    948         $s = $this->seek();
    949 
    950         if (!$this->literal("when")) {
    951             $this->seek($s);
    952             return false;
    953         }
    954 
    955         $guards = array();
    956 
    957         while ($this->guard_group($g)) {
    958             $guards[] = $g;
    959             if (!$this->literal(",")) break;
    960         }
    961 
    962         if (count($guards) == 0) {
    963             $guards = null;
    964             $this->seek($s);
    965             return false;
    966         }
    967 
    968         return true;
    969     }
    970 
    971     // a bunch of guards that are and'd together
    972     function guard_group(&$guard_group) {
    973         $s = $this->seek();
    974         $guard_group = array();
    975         while ($this->guard($guard)) {
    976             $guard_group[] = $guard;
    977             if (!$this->literal("and")) break;
    978         }
    979 
    980         if (count($guard_group) == 0) {
    981             $guard_group = null;
    982             $this->seek($s);
    983             return false;
    984         }
    985 
    986         return true;
    987     }
    988 
    989     function guard(&$guard) {
    990         $s = $this->seek();
    991         $negate = $this->literal("not");
    992 
    993         if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) {
    994             $guard = $exp;
    995             if ($negate) $guard = array("negate", $guard);
    996             return true;
    997         }
    998 
    999         $this->seek($s);
    1000         return false;
    1001     }
    1002 
    1003     function compressList($items, $delim) {
    1004         if (count($items) == 1) return $items[0];   
    1005         else return array('list', $delim, $items);
    1006     }
    1007 
    1008     // just do a shallow property merge, seems to be what lessjs does
    1009     function mergeBlock($target, $from) {
    1010         $target = clone $target;
    1011         $target->props = array_merge($target->props, $from->props);
    1012         return $target;
    1013     }
    1014 
    1015     // import all imports into the block
    1016     function mixImports($block) {
    1017         $props = array();
    1018         foreach ($block->props as $prop) {
    1019             if ($prop[0] == 'import') {
    1020                 list(, $path) = $prop;
    1021                 $this->addParsedFile($path);
    1022                 $child_less = $this->createChild($path);
    1023                 $root = $child_less->parseTree();
    1024 
    1025                 $root->parent = $block;
    1026                 $this->mixImports($root);
    1027 
    1028                 // inject imported blocks into this block, local will overwrite import
    1029                 $block->children = array_merge($root->children, $block->children);
    1030 
    1031                 // splice in all the props
    1032                 foreach ($root->props as $sub_prop) {
    1033                     if (isset($sub_prop[-1])) {
    1034                         // leave a reference to the imported file for error messages
    1035                         $sub_prop[-1] = array($child_less, $sub_prop[-1]);
    1036                     }
    1037                     $props[] = $sub_prop;
    1038                 }
    1039             } else {
    1040                 $props[] = $prop;
    1041             }
    1042         }
    1043         $block->props = $props;
    1044     }
    1045 
    1046     /**
    1047      * Recursively compiles a block.
    1048      * @param $block the block
    1049      * @param $parentTags the tags of the block that contained this one
    1050      *
    1051      * A block is analogous to a CSS block in most cases. A single less document
    1052      * is encapsulated in a block when parsed, but it does not have parent tags
    1053      * so all of it's children appear on the root level when compiled.
    1054      *
    1055      * Blocks are made up of props and children.
    1056      *
    1057      * Props are property instructions, array tuples which describe an action
    1058      * to be taken, eg. write a property, set a variable, mixin a block.
    1059      *
    1060      * The children of a block are just all the blocks that are defined within.
    1061      *
    1062      * Compiling the block involves pushing a fresh environment on the stack,
    1063      * and iterating through the props, compiling each one.
    1064      *
    1065      * See lessc::compileProp()
    1066      *
    1067      */
    1068     function compileBlock($block, $parent_tags = null) {
    1069         $isRoot = $parent_tags == null && $block->tags == null;
    1070 
    1071         $indent = str_repeat($this->indentChar, $this->indentLevel);
    1072 
    1073         if (!empty($block->no_multiply)) {
    1074             $special_block = true;
    1075             $this->indentLevel++;
    1076             $tags = array();
    1077         } else {
    1078             $special_block = false;
    1079 
    1080             // evaluate expression tags
    1081             $tags = null;
    1082             if (is_array($block->tags)) {
    1083                 $tags = array();
    1084                 foreach ($block->tags as $tag) {
    1085                     if (is_array($tag)) {
    1086                         list(, $value) = $tag;
    1087                         $tags[] = $this->compileValue($this->reduce($value));
    1088                     } else {
    1089                         $tags[] = $tag;
    1090                     }
    1091                 }
    1092             }
    1093 
    1094             $tags = $this->multiplyTags($parent_tags, $tags);
    1095         }
    1096 
    1097         $env = $this->pushEnv();
    1098         $env->nameDepth = array();
    1099 
    1100         $lines = array();
    1101         $blocks = array();
    1102         $this->mixImports($block);
    1103         foreach ($block->props as $prop) {
    1104             $this->compileProp($prop, $block, $tags, $lines, $blocks);
    1105         }
    1106 
    1107         $block->scope = $env;
    1108 
    1109         $this->pop();
    1110 
    1111         $nl = $isRoot ? "\n".$indent :
    1112             "\n".$indent.$this->indentChar;
    1113 
    1114         ob_start();
    1115 
    1116         if ($special_block) {
    1117             $this->indentLevel--;
    1118             if (isset($block->media)) {
    1119                 echo $this->compileMedia($block);
    1120             } elseif (isset($block->keyframes)) {
    1121                 echo $block->tags[0]." ".
    1122                     $this->compileValue($this->reduce($block->keyframes));
    1123             } else {
    1124                 list($name) = $block->tags;
    1125                 echo $indent.$name;
    1126             }
    1127 
    1128             echo ' {'.(count($lines) > 0 ? $nl : "\n");
    1129         }
    1130 
    1131         // dump it
    1132         if (count($lines) > 0) {
    1133             if (!$special_block && !$isRoot) {
    1134                 echo $indent.implode(", ", $tags);
    1135                 if (count($lines) > 1) echo " {".$nl;
    1136                 else echo " { ";
    1137             }
    1138 
    1139             echo implode($nl, $lines);
    1140 
    1141             if (!$special_block && !$isRoot) {
    1142                 if (count($lines) > 1) echo "\n".$indent."}\n";
    1143                 else echo " }\n";
    1144             } else echo "\n";
    1145         }
    1146 
    1147         foreach ($blocks as $b) echo $b;
    1148 
    1149         if ($special_block) {
    1150             echo $indent."}\n";
    1151         }
    1152 
    1153         return ob_get_clean();
    1154     }
    1155 
    1156     // find the fully qualified tags for a block and its parent's tags
    1157     function multiplyTags($parents, $current) {
    1158         if ($parents == null) return $current;
    1159 
    1160         $tags = array();
    1161         foreach ($parents as $ptag) {
    1162             foreach ($current as $tag) {
    1163                 // inject parent in place of parent selector, ignoring escaped values
    1164                 $count = 0;
    1165                 $parts = explode("&&", $tag);
    1166 
    1167                 foreach ($parts as $i => $chunk) {
    1168                     $parts[$i] = str_replace($this->parentSelector, $ptag, $chunk, $c);
    1169                     $count += $c;
    1170                 }
    1171                
    1172                 $tag = implode("&", $parts);
    1173 
    1174                 if ($count > 0) {
    1175                     $tags[] = trim($tag);
    1176                 } else {
    1177                     $tags[] = trim($ptag . ' ' . $tag);
    1178                 }
    1179             }
    1180         }
    1181 
    1182         return $tags;
    1183     }
    1184 
    1185     function eq($left, $right) {
    1186         return $left == $right;
    1187     }
    1188 
    1189     function patternMatch($block, $callingArgs) {
    1190         // match the guards if it has them
    1191         // any one of the groups must have all its guards pass for a match
    1192         if (!empty($block->guards)) {
    1193             $group_passed = false;
    1194             foreach ($block->guards as $guard_group) {
    1195                 foreach ($guard_group as $guard) {
    1196                     $this->pushEnv();
    1197                     $this->zipSetArgs($block->args, $callingArgs);
    1198 
    1199                     $negate = false;
    1200                     if ($guard[0] == "negate") {
    1201                         $guard = $guard[1];
    1202                         $negate = true;
    1203                     }
    1204 
    1205                     $passed = $this->reduce($guard) == self::$TRUE;
    1206                     if ($negate) $passed = !$passed;
    1207 
    1208                     $this->pop();
    1209 
    1210                     if ($passed) {
    1211                         $group_passed = true;
    1212                     } else {
    1213                         $group_passed = false;
    1214                         break;
    1215                     }
    1216                 }
    1217 
    1218                 if ($group_passed) break;
    1219             }
    1220 
    1221             if (!$group_passed) {
    1222                 return false;
    1223             }
    1224         }
    1225 
    1226         $numCalling = count($callingArgs);
    1227 
    1228         if (empty($block->args)) {
    1229             return $block->is_vararg || $numCalling == 0;
    1230         }
    1231 
    1232         $i = -1; // no args
    1233         // try to match by arity or by argument literal
    1234         foreach ($block->args as $i => $arg) {
    1235             switch ($arg[0]) {
    1236             case "lit":
    1237                 if (empty($callingArgs[$i]) || !$this->eq($arg[1], $callingArgs[$i])) {
    1238                     return false;
    1239                 }
    1240                 break;
    1241             case "arg":
    1242                 // no arg and no default value
    1243                 if (!isset($callingArgs[$i]) && !isset($arg[2])) {
    1244                     return false;
    1245                 }
    1246                 break;
    1247             case "rest":
    1248                 $i--; // rest can be empty
    1249                 break 2;
    1250             }
    1251         }
    1252 
    1253         if ($block->is_vararg) {
    1254             return true; // not having enough is handled above
    1255         } else {
    1256             $numMatched = $i + 1;
    1257             // greater than becuase default values always match
    1258             return $numMatched >= $numCalling;
    1259         }
    1260     }
    1261 
    1262     function patternMatchAll($blocks, $callingArgs) {
    1263         $matches = null;
    1264         foreach ($blocks as $block) {
    1265             if ($this->patternMatch($block, $callingArgs)) {
    1266                 $matches[] = $block;
    1267             }
    1268         }
    1269 
    1270         return $matches;
    1271     }
    1272 
    1273     // attempt to find blocks matched by path and args
    1274     function findBlocks($search_in, $path, $args, $seen=array()) {
    1275         if ($search_in == null) return null;
    1276         if (isset($seen[$search_in->id])) return null;
    1277         $seen[$search_in->id] = true;
    1278 
    1279         $name = $path[0];
    1280 
    1281         if (isset($search_in->children[$name])) {
    1282             $blocks = $search_in->children[$name];
    1283             if (count($path) == 1) {
    1284                 $matches = $this->patternMatchAll($blocks, $args);
    1285                 if (!empty($matches)) {
    1286                     // This will return all blocks that match in the closest
    1287                     // scope that has any matching block, like lessjs
    1288                     return $matches;
    1289                 }
    1290             } else {
    1291                 return $this->findBlocks($blocks[0],
    1292                     array_slice($path, 1), $args, $seen);
    1293             }
    1294         }
    1295 
    1296         if ($search_in->parent === $search_in) return null;
    1297         return $this->findBlocks($search_in->parent, $path, $args, $seen);
    1298     }
    1299 
    1300     // sets all argument names in $args to either the default value
    1301     // or the one passed in through $values
    1302     function zipSetArgs($args, $values) {
    1303         $i = 0;
    1304         $assigned_values = array();
    1305         foreach ($args as $a) {
    1306             if ($a[0] == "arg") {
    1307                 if ($i < count($values) && !is_null($values[$i])) {
    1308                     $value = $values[$i];
    1309                 } elseif (isset($a[2])) {
    1310                     $value = $a[2];
    1311                 } else $value = null;
    1312 
    1313                 $value = $this->reduce($value);
    1314                 $this->set($a[1], $value);
    1315                 $assigned_values[] = $value;
    1316             }
    1317             $i++;
    1318         }
    1319 
    1320         // check for a rest
    1321         $last = end($args);
    1322         if ($last[0] == "rest") {
    1323             $rest = array_slice($values, count($args) - 1);
    1324             $this->set($last[1], $this->reduce(array("list", " ", $rest)));
    1325         }
    1326 
    1327         $this->env->arguments = $assigned_values;
    1328     }
    1329 
    1330     // compile a prop and update $lines or $blocks appropriately
    1331     function compileProp($prop, $block, $tags, &$_lines, &$_blocks) {
    1332         // set error position context
    1333         if (isset($prop[-1])) {
    1334             if (is_array($prop[-1])) {
    1335                 list($less, $count) = $prop[-1];
    1336                 $parentParser = $this->sourceParser;
    1337                 $this->sourceParser = $less;
    1338                 $this->count = $count;
    1339             } else {
    1340                 $this->count = $prop[-1];
    1341             }
    1342         } else {
    1343             $this->count = -1;
    1344         }
    1345 
    1346         switch ($prop[0]) {
    1347         case 'assign':
    1348             list(, $name, $value) = $prop;
    1349             if ($name[0] == $this->vPrefix) {
    1350                 $this->set($name, $value);
    1351             } else {
    1352                 $_lines[] = "$name:".
    1353                     $this->compileValue($this->reduce($value)).";";
    1354             }
    1355             break;
    1356         case 'block':
    1357             list(, $child) = $prop;
    1358             $_blocks[] = $this->compileBlock($child, $tags);
    1359             break;
    1360         case 'mixin':
    1361             list(, $path, $args) = $prop;
    1362 
    1363             $args = array_map(array($this, "reduce"), (array)$args);
    1364             $mixins = $this->findBlocks($block, $path, $args);
    1365             if (is_null($mixins)) {
    1366                 // echo "failed to find block: ".implode(" > ", $path)."\n";
    1367                 break; // throw error here??
    1368             }
    1369 
    1370             foreach ($mixins as $mixin) {
    1371                 $old_scope = null;
    1372                 if (isset($mixin->parent->scope)) {
    1373                     $old_scope = $this->env;
    1374                     $this->env = $mixin->parent->scope;
    1375                 }
    1376 
    1377                 $have_args = false;
    1378                 if (isset($mixin->args)) {
    1379                     $have_args = true;
    1380                     $this->pushEnv();
    1381                     $this->zipSetArgs($mixin->args, $args);
    1382                 }
    1383 
    1384                 $old_parent = $mixin->parent;
    1385                 if ($mixin != $block) $mixin->parent = $block;
    1386 
    1387                 foreach ($mixin->props as $sub_prop) {
    1388                     $this->compileProp($sub_prop, $mixin, $tags, $_lines, $_blocks);
    1389                 }
    1390 
    1391                 $mixin->parent = $old_parent;
    1392 
    1393                 if ($have_args) $this->pop();
    1394 
    1395                 if ($old_scope) {
    1396                     $this->env = $old_scope;
    1397                 }
    1398             }
    1399 
    1400             break;
    1401         case 'raw':
    1402             $_lines[] = $prop[1];
    1403             break;
    1404         case 'charset':
    1405             list(, $value) = $prop;
    1406             $_lines[] = '@charset '.$this->compileValue($this->reduce($value)).';';
    1407             break;
    1408         default:
    1409             $this->throwError("unknown op: {$prop[0]}\n");
    1410         }
    1411 
    1412         if (isset($parentParser)) {
    1413             $this->sourceParser = $parentParser;
    1414         }
    1415     }
    1416 
    1417 
    1418     /**
    1419      * Compiles a primitive value into a CSS property value.
    1420      *
    1421      * Values in lessphp are typed by being wrapped in arrays, their format is
    1422      * typically:
    1423      *
    1424      *     array(type, contents [, additional_contents]*)
    1425      *
    1426      * Will not work on non reduced values (expressions, variables, etc)
    1427      */
    1428     function compileValue($value) {
    1429         switch ($value[0]) {
    1430         case 'list':
    1431             // [1] - delimiter
    1432             // [2] - array of values
    1433             return implode($value[1], array_map(array($this, 'compileValue'), $value[2]));
    1434         case 'keyword':
    1435             // [1] - the keyword
    1436         case 'number':
    1437             // [1] - the number
    1438             return $value[1];
    1439         case 'escape':
    1440         case 'string':
    1441             // [1] - contents of string (includes quotes)
    1442            
    1443             // search for inline variables to replace
    1444             $replace = array();
    1445             if (preg_match_all('/'.$this->preg_quote($this->vPrefix).'\{([\w-_][0-9\w-_]*)\}/', $value[1], $m)) {
    1446                 foreach ($m[1] as $name) {
    1447                     if (!isset($replace[$name]))
    1448                         $replace[$name] = $this->compileValue($this->reduce(array('variable', $this->vPrefix . $name)));
    1449                 }
    1450             }
    1451 
    1452             foreach ($replace as $var=>$val) {
    1453                 if ($this->quoted($val)) {
    1454                     $val = substr($val, 1, -1);
    1455                 }
    1456                 $value[1] = str_replace($this->vPrefix. '{'.$var.'}', $val, $value[1]);
    1457             }
    1458 
    1459             return $value[1];
    1460         case 'color':
    1461             // [1] - red component (either number for a %)
    1462             // [2] - green component
    1463             // [3] - blue component
    1464             // [4] - optional alpha component
    1465             list(, $r, $g, $b) = $value;
    1466             $r = round($r);
    1467             $g = round($g);
    1468             $b = round($b);
    1469 
    1470             if (count($value) == 5 && $value[4] != 1) { // rgba
    1471                 return 'rgba('.$r.','.$g.','.$b.','.$value[4].')';
    1472             }
    1473             return sprintf("#%02x%02x%02x", $r, $g, $b);
    1474         case 'function':
    1475             // [1] - function name
    1476             // [2] - some array value representing arguments, either ['string', value] or ['list', ',', values[]]
    1477 
    1478             // see if function evaluates to something else
    1479             $value = $this->reduce($value);
    1480             if ($value[0] == 'function') {
    1481                 return $value[1].'('.$this->compileValue($value[2]).')';
    1482             }
    1483             else return $this->compileValue($value);
    1484         default: // assumed to be unit 
    1485             return $value[1].$value[0];
    1486         }
    1487     }
    1488 
    1489     function compileMedia($block) {
    1490         $mediaParts = array();
    1491         foreach ($block->media as $part) {
    1492             if ($part[0] == "raw") {
    1493                 $mediaParts[] = $part[1];
    1494             } elseif ($part[0] == "assign") {
    1495                 list(, $propName, $propVal) = $part;
    1496                 $mediaParts[] = "$propName: ".
    1497                     $this->compileValue($this->reduce($propVal));
    1498             }
    1499         }
    1500 
    1501         return "@media ".trim(implode($mediaParts));
    1502     }
    1503 
    1504     function lib_isnumber($value) {
    1505         return $this->toBool(is_numeric($value[1]));
    1506     }
    1507 
    1508     function lib_isstring($value) {
    1509         return $this->toBool($value[0] == "string");
    1510     }
    1511 
    1512     function lib_iscolor($value) {
    1513         return $this->toBool($this->coerceColor($value));
    1514     }
    1515 
    1516     function lib_iskeyword($value) {
    1517         return $this->toBool($value[0] == "keyword");
    1518     }
    1519 
    1520     function lib_ispixel($value) {
    1521         return $this->toBool($value[0] == "px");
    1522     }
    1523 
    1524     function lib_ispercentage($value) {
    1525         return $this->toBool($value[0] == "%");
    1526     }
    1527 
    1528     function lib_isem($value) {
    1529         return $this->toBool($value[0] == "em");
    1530     }
    1531 
    1532     function lib_rgbahex($color) {
    1533         $color = $this->coerceColor($color);
    1534         if (is_null($color))
    1535             $this->throwError("color expected for rgbahex");
    1536 
    1537         return sprintf("#%02x%02x%02x%02x",
    1538             isset($color[4]) ? $color[4]*255 : 0,
    1539             $color[1],$color[2], $color[3]);
    1540     }
    1541    
    1542     function lib_argb($color){
    1543             return $this->lib_rgbahex($color);
    1544         }
    1545 
    1546     // utility func to unquote a string
    1547     function lib_e($arg) {
    1548         switch ($arg[0]) {
    1549             case "list":
    1550                 $items = $arg[2];
    1551                 if (isset($items[0])) {
    1552                     return $this->lib_e($items[0]);
    1553                 }
    1554                 return "";
    1555             case "string":
    1556                 $str = $this->compileValue($arg);
    1557                 return substr($str, 1, -1);
    1558             default:
    1559                 return $this->compileValue($arg);
    1560         }
    1561     }
    1562 
    1563     function lib__sprintf($args) {
    1564         if ($args[0] != "list") return $args;
    1565         $values = $args[2];
    1566         $source = $this->reduce(array_shift($values));
    1567         if ($source[0] != "string") {
    1568             return $source;
    1569         }
    1570 
    1571         $str = $source[1];
    1572         $i = 0;
    1573         if (preg_match_all('/%[dsa]/', $str, $m)) {
    1574             foreach ($m[0] as $match) {
    1575                 $val = isset($values[$i]) ? $this->reduce($values[$i]) : array('keyword', '');
    1576                 $i++;
    1577                 switch ($match[1]) {
    1578                 case "s":
    1579                     if ($val[0] == "string") {
    1580                         $rep = substr($val[1], 1, -1);
    1581                         break;
    1582                     }
    1583                 default:
    1584                     $rep = $this->compileValue($val);
    1585                 }
    1586                 $str = preg_replace('/'.$this->preg_quote($match).'/', $rep, $str, 1);
    1587             }
    1588         }
    1589 
    1590         return array('string', $str);
    1591     }
    1592 
    1593     function lib_floor($arg) {
    1594         return array($arg[0], floor($arg[1]));
    1595     }
    1596    
    1597     function lib_ceil($arg) {
    1598         return array($arg[0], ceil($arg[1]));
    1599     }
    1600 
    1601     function lib_round($arg) {
    1602         return array($arg[0], round($arg[1]));
    1603     }
    1604 
    1605     // is a string surrounded in quotes? returns the quoting char if true
    1606     function quoted($s) {
    1607         if (preg_match('/^("|\').*?\1$/', $s, $m))
    1608             return $m[1];
    1609         else return false;
    1610     }
    1611 
    1612     /**
    1613      * Helper function to get arguments for color functions.
    1614      * Accepts invalid input, non colors interpreted as being black.
    1615      */
    1616     function colorArgs($args) {
    1617         if ($args[0] != 'list' || count($args[2]) < 2) {
    1618             return array(array('color', 0, 0, 0));
    1619         }
    1620         list($color, $delta) = $args[2];
    1621         $color = $this->coerceColor($color);
    1622         if (is_null($color))
    1623             $color = array('color', 0, 0, 0);
    1624 
    1625         $delta = floatval($delta[1]);
    1626 
    1627         return array($color, $delta);
    1628     }
    1629 
    1630     function lib_darken($args) {
    1631         list($color, $delta) = $this->colorArgs($args);
    1632 
    1633         $hsl = $this->toHSL($color);
    1634         $hsl[3] = $this->clamp($hsl[3] - $delta, 100);
    1635         return $this->toRGB($hsl);
    1636     }
    1637 
    1638     function lib_lighten($args) {
    1639         list($color, $delta) = $this->colorArgs($args);
    1640 
    1641         $hsl = $this->toHSL($color);
    1642         $hsl[3] = $this->clamp($hsl[3] + $delta, 100);
    1643         return $this->toRGB($hsl);
    1644     }
    1645 
    1646     function lib_saturate($args) {
    1647         list($color, $delta) = $this->colorArgs($args);
    1648 
    1649         $hsl = $this->toHSL($color);
    1650         $hsl[2] = $this->clamp($hsl[2] + $delta, 100);
    1651         return $this->toRGB($hsl);
    1652     }
    1653 
    1654     function lib_desaturate($args) {
    1655         list($color, $delta) = $this->colorArgs($args);
    1656 
    1657         $hsl = $this->toHSL($color);
    1658         $hsl[2] = $this->clamp($hsl[2] - $delta, 100);
    1659         return $this->toRGB($hsl);
    1660     }
    1661 
    1662     function lib_spin($args) {
    1663         list($color, $delta) = $this->colorArgs($args);
    1664 
    1665         $hsl = $this->toHSL($color);
    1666 
    1667         $hsl[1] = $hsl[1] + $delta % 360;
    1668         if ($hsl[1] < 0) $hsl[1] += 360;
    1669 
    1670         return $this->toRGB($hsl);
    1671     }
    1672 
    1673     function lib_fadeout($args) {
    1674         list($color, $delta) = $this->colorArgs($args);
    1675         $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100);
    1676         return $color;
    1677     }
    1678 
    1679     function lib_fadein($args) {
    1680         list($color, $delta) = $this->colorArgs($args);
    1681         $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100);
    1682         return $color;
    1683     }
    1684 
    1685     function lib_hue($color) {
    1686         if ($color[0] != 'color') return 0;
    1687         $hsl = $this->toHSL($color);
    1688         return round($hsl[1]);
    1689     }
    1690 
    1691     function lib_saturation($color) {
    1692         if ($color[0] != 'color') return 0;
    1693         $hsl = $this->toHSL($color);
    1694         return round($hsl[2]);
    1695     }
    1696 
    1697     function lib_lightness($color) {
    1698         if ($color[0] != 'color') return 0;
    1699         $hsl = $this->toHSL($color);
    1700         return round($hsl[3]);
    1701     }
    1702 
    1703     // get the alpha of a color
    1704     // defaults to 1 for non-colors or colors without an alpha
    1705     function lib_alpha($color) {
    1706         if ($color[0] != 'color') return 1;
    1707         return isset($color[4]) ? $color[4] : 1;
    1708     }
    1709 
    1710     // set the alpha of the color
    1711     function lib_fade($args) {
    1712         list($color, $alpha) = $this->colorArgs($args);
    1713         $color[4] = $this->clamp($alpha / 100.0);
    1714         return $color;
    1715     }
    1716 
    1717     function lib_percentage($number) {
    1718         return array('%', $number[1]*100);
    1719     }
    1720 
    1721     // mixes two colors by weight
    1722     // mix(@color1, @color2, @weight);
    1723     // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method
    1724     function lib_mix($args) {
    1725         if ($args[0] != "list" || count($args[2]) < 3)
    1726             $this->throwError("mix expects (color1, color2, weight)");
    1727 
    1728         list($first, $second, $weight) = $args[2];
    1729         $first = $this->assertColor($first);
    1730         $second = $this->assertColor($second);
    1731 
    1732         $first_a = $this->lib_alpha($first);
    1733         $second_a = $this->lib_alpha($second);
    1734         $weight = $weight[1] / 100.0;
    1735 
    1736         $w = $weight * 2 - 1;
    1737         $a = $first_a - $second_a;
    1738 
    1739         $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
    1740         $w2 = 1.0 - $w1;
    1741 
    1742         $new = array('color',
    1743             $w1 * $first[1] + $w2 * $second[1],
    1744             $w1 * $first[2] + $w2 * $second[2],
    1745             $w1 * $first[3] + $w2 * $second[3],
    1746         );
    1747 
    1748         if ($first_a != 1.0 || $second_a != 1.0) {
    1749             $new[] = $first_a * $weight + $second_a * ($weight - 1);
    1750         }
    1751 
    1752         return $this->fixColor($new);
    1753     }
    1754 
    1755     function assertColor($value, $error = "expected color value") {
    1756         $color = $this->coerceColor($value);
    1757         if (is_null($color)) $this->throwError($error);
    1758         return $color;
    1759     }
    1760 
    1761     function toHSL($color) {
    1762         if ($color[0] == 'hsl') return $color;
    1763 
    1764         $r = $color[1] / 255;
    1765         $g = $color[2] / 255;
    1766         $b = $color[3] / 255;
    1767 
    1768         $min = min($r, $g, $b);
    1769         $max = max($r, $g, $b);
    1770 
    1771         $L = ($min + $max) / 2;
    1772         if ($min == $max) {
    1773             $S = $H = 0;
    1774         } else {
    1775             if ($L < 0.5)
    1776                 $S = ($max - $min)/($max + $min);
    1777             else
    1778                 $S = ($max - $min)/(2.0 - $max - $min);
    1779 
    1780             if ($r == $max) $H = ($g - $b)/($max - $min);
    1781             elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min);
    1782             elseif ($b == $max) $H = 4.0 + ($r - $g)/($max - $min);
    1783 
    1784         }
    1785 
    1786         $out = array('hsl',
    1787             ($H < 0 ? $H + 6 : $H)*60,
    1788             $S*100,
    1789             $L*100,
    1790         );
    1791 
    1792         if (count($color) > 4) $out[] = $color[4]; // copy alpha
    1793         return $out;
    1794     }
    1795 
    1796     function toRGB_helper($comp, $temp1, $temp2) {
    1797         if ($comp < 0) $comp += 1.0;
    1798         elseif ($comp > 1) $comp -= 1.0;
    1799 
    1800         if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp;
    1801         if (2 * $comp < 1) return $temp2;
    1802         if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6;
    1803 
    1804         return $temp1;
    1805     }
    1806 
    1807     /**
    1808      * Converts a hsl array into a color value in rgb.
    1809      * Expects H to be in range of 0 to 360, S and L in 0 to 100
    1810      */
    1811     function toRGB($color) {
    1812         if ($color == 'color') return $color;
    1813 
    1814         $H = $color[1] / 360;
    1815         $S = $color[2] / 100;
    1816         $L = $color[3] / 100;
    1817 
    1818         if ($S == 0) {
    1819             $r = $g = $b = $L;
    1820         } else {
    1821             $temp2 = $L < 0.5 ?
    1822                 $L*(1.0 + $S) :
    1823                 $L + $S - $L * $S;
    1824 
    1825             $temp1 = 2.0 * $L - $temp2;
    1826 
    1827             $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2);
    1828             $g = $this->toRGB_helper($H, $temp1, $temp2);
    1829             $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2);
    1830         }
    1831 
    1832         // $out = array('color', round($r*255), round($g*255), round($b*255));
    1833         $out = array('color', $r*255, $g*255, $b*255);
    1834         if (count($color) > 4) $out[] = $color[4]; // copy alpha
    1835         return $out;
    1836     }
    1837 
    1838     function clamp($v, $max = 1, $min = 0) {
    1839         return min($max, max($min, $v));
    1840     }
    1841 
    1842     /**
    1843      * Convert the rgb, rgba, hsl color literals of function type
    1844      * as returned by the parser into values of color type.
    1845      */
    1846     function funcToColor($func) {
    1847         $fname = $func[1];
    1848         if ($func[2][0] != 'list') return false; // need a list of arguments
    1849         $rawComponents = $func[2][2];
    1850 
    1851         if ($fname == 'hsl' || $fname == 'hsla') {
    1852             $hsl = array('hsl');
    1853             $i = 0;
    1854             foreach ($rawComponents as $c) {
    1855                 $val = $this->reduce($c);
    1856                 $val = isset($val[1]) ? floatval($val[1]) : 0;
    1857 
    1858                 if ($i == 0) $clamp = 360;
    1859                 elseif ($i < 4) $clamp = 100;
    1860                 else $clamp = 1;
    1861 
    1862                 $hsl[] = $this->clamp($val, $clamp);
    1863                 $i++;
    1864             }
    1865 
    1866             while (count($hsl) < 4) $hsl[] = 0;
    1867             return $this->toRGB($hsl);
    1868 
    1869         } elseif ($fname == 'rgb' || $fname == 'rgba') {
    1870             $components = array();
    1871             $i = 1;
    1872             foreach ($rawComponents as $c) {
    1873                 $c = $this->reduce($c);
    1874                 if ($i < 4) {
    1875                     if ($c[0] == '%') $components[] = 255 * ($c[1] / 100);
    1876                     else $components[] = floatval($c[1]);
    1877                 } elseif ($i == 4) {
    1878                     if ($c[0] == '%') $components[] = 1.0 * ($c[1] / 100);
    1879                     else $components[] = floatval($c[1]);
    1880                 } else break;
    1881 
    1882                 $i++;
    1883             }
    1884             while (count($components) < 3) $components[] = 0;
    1885             array_unshift($components, 'color');
    1886             return $this->fixColor($components);
    1887         }
    1888 
    1889         return false;
    1890     }
    1891 
    1892     function toName($val) {
    1893         switch($val[0]) {
    1894         case "string":
    1895             return substr($val[1], 1, -1);
    1896         default:
    1897             return $val[1];
    1898         }
    1899     }
    1900 
    1901     // reduce a delayed type to its final value
    1902     // dereference variables and solve equations
    1903     function reduce($var) {
    1904         // this is done here for infinite loop checking
    1905         if ($var[0] == "variable") {
    1906             $key = is_array($var[1]) ?
    1907                 $this->vPrefix.$this->toName($this->reduce($var[1])) : $var[1];
    1908 
    1909             $seen =& $this->env->seenNames;
    1910 
    1911             if (!empty($seen[$key])) {
    1912                 $this->throwError("infinite loop detected: $key");
    1913             }
    1914 
    1915             $seen[$key] = true;
    1916 
    1917             $out = $this->reduce($this->get($key));
    1918 
    1919             $seen[$key] = false;
    1920 
    1921             return $out;
    1922         }
    1923 
    1924         while (in_array($var[0], self::$dtypes)) {
    1925             if ($var[0] == 'list') {
    1926                 foreach ($var[2] as &$value) $value = $this->reduce($value);
    1927                 break;
    1928             } elseif ($var[0] == 'expression') {
    1929                 $var = $this->evaluate($var[1], $var[2], $var[3]);
    1930             } elseif ($var[0] == 'lookup') {
    1931                 // do accessor here....
    1932                 $var = array('number', 0);
    1933             } elseif ($var[0] == 'function') {
    1934                 $color = $this->funcToColor($var);
    1935                 if ($color) $var = $color;
    1936                 else {
    1937                     list($_, $name, $args) = $var;
    1938                     if ($name == "%") $name = "_sprintf";
    1939                     $f = isset($this->libFunctions[$name]) ?
    1940                         $this->libFunctions[$name] : array($this, 'lib_'.$name);
    1941 
    1942                     if (is_callable($f)) {
    1943                         if ($args[0] == 'list')
    1944                             $args = $this->compressList($args[2], $args[1]);
    1945 
    1946                         $var = call_user_func($f, $this->reduce($args), $this);
    1947 
    1948                         // convert to a typed value if the result is a php primitive
    1949                         if (is_numeric($var)) $var = array('number', $var);
    1950                         elseif (!is_array($var)) $var = array('keyword', $var);
    1951                     } else {
    1952                         // plain function, reduce args
    1953                         $var[2] = $this->reduce($var[2]);
    1954                     }
    1955                 }
    1956                 break; // done reducing after a function
    1957             } elseif ($var[0] == 'negative') {
    1958                 $value = $this->reduce($var[1]);
    1959                 if (is_numeric($value[1])) {
    1960                     $value[1] = -1*$value[1];
    1961                 }
    1962                 $var = $value;
    1963             }
    1964         }
    1965 
    1966         return $var;
    1967     }
    1968 
    1969     function coerceColor($value) {
    1970         switch($value[0]) {
    1971             case 'color': return $value;
    1972             case 'keyword':
    1973                 $name = $value[1];
    1974                 if (isset(self::$cssColors[$name])) {
    1975                     list($r, $g, $b) = explode(',', self::$cssColors[$name]);
    1976                     return array('color', $r, $g, $b);
    1977                 }
    1978                 return null;
    1979         }
    1980     }
    1981 
    1982     function toBool($a) {
    1983         if ($a) return self::$TRUE;
    1984         else return self::$FALSE;
    1985     }
    1986 
    1987     // evaluate an expression
    1988     function evaluate($op, $left, $right) {
    1989         $left = $this->reduce($left);
    1990         $right = $this->reduce($right);
    1991 
    1992         if ($left_color = $this->coerceColor($left)) {
    1993             $left = $left_color;
    1994         }
    1995 
    1996         if ($right_color = $this->coerceColor($right)) {
    1997             $right = $right_color;
    1998         }
    1999 
    2000         if ($op == "and") {
    2001             return $this->toBool($left == self::$TRUE && $right == self::$TRUE);
    2002         }
    2003 
    2004         if ($op == "=") {
    2005             return $this->toBool($this->eq($left, $right) );
    2006         }
    2007 
    2008         if ($left[0] == 'color' && $right[0] == 'color') {
    2009             $out = $this->op_color_color($op, $left, $right);
    2010             return $out;
    2011         }
    2012 
    2013         if ($left[0] == 'color') {
    2014             return $this->op_color_number($op, $left, $right);
    2015         }
    2016 
    2017         if ($right[0] == 'color') {
    2018             return $this->op_number_color($op, $left, $right);
    2019         }
    2020 
    2021         // concatenate strings
    2022         if ($op == '+' && $left[0] == 'string') {
    2023             $append = $this->compileValue($right);
    2024             if ($this->quoted($append)) $append = substr($append, 1, -1);
    2025 
    2026             $lhs = $this->compileValue($left);
    2027             if ($q = $this->quoted($lhs)) $lhs = substr($lhs, 1, -1);
    2028             if (!$q) $q = '';
    2029 
    2030             return array('string', $q.$lhs.$append.$q);
    2031         }
    2032 
    2033         if ($left[0] == 'keyword' || $right[0] == 'keyword' ||
    2034             $left[0] == 'string' || $right[0] == 'string')
    2035         {
    2036             // look for negative op
    2037             if ($op == '-') $right[1] = '-'.$right[1];
    2038             return array('keyword', $this->compileValue($left) .' '. $this->compileValue($right));
    2039         }
    2040    
    2041         // default to number operation
    2042         return $this->op_number_number($op, $left, $right);
    2043     }
    2044 
    2045     // make sure a color's components don't go out of bounds
    2046     function fixColor($c) {
    2047         foreach (range(1, 3) as $i) {
    2048             if ($c[$i] < 0) $c[$i] = 0;
    2049             if ($c[$i] > 255) $c[$i] = 255;
    2050         }
    2051 
    2052         return $c;
    2053     }
    2054 
    2055     function op_number_color($op, $lft, $rgt) {
    2056         if ($op == '+' || $op = '*') {
    2057             return $this->op_color_number($op, $rgt, $lft);
    2058         }
    2059     }
    2060 
    2061     function op_color_number($op, $lft, $rgt) {
    2062         if ($rgt[0] == '%') $rgt[1] /= 100;
    2063 
    2064         return $this->op_color_color($op, $lft,
    2065             array_fill(1, count($lft) - 1, $rgt[1]));
    2066     }
    2067 
    2068     function op_color_color($op, $left, $right) {
    2069         $out = array('color');
    2070         $max = count($left) > count($right) ? count($left) : count($right);
    2071         foreach (range(1, $max - 1) as $i) {
    2072             $lval = isset($left[$i]) ? $left[$i] : 0;
    2073             $rval = isset($right[$i]) ? $right[$i] : 0;
    2074             switch ($op) {
    2075             case '+':
    2076                 $out[] = $lval + $rval;
    2077                 break;
    2078             case '-':
    2079                 $out[] = $lval - $rval;
    2080                 break;
    2081             case '*':
    2082                 $out[] = $lval * $rval;
    2083                 break;
    2084             case '%':
    2085                 $out[] = $lval % $rval;
    2086                 break;
    2087             case '/':
    2088                 if ($rval == 0) $this->throwError("evaluate error: can't divide by zero");
    2089                 $out[] = $lval / $rval;
    2090                 break;
    2091             default:
    2092                 $this->throwError('evaluate error: color op number failed on op '.$op);
    2093             }
    2094         }
    2095         return $this->fixColor($out);
    2096     }
    2097 
    2098     // operator on two numbers
    2099     function op_number_number($op, $left, $right) {
    2100         $type = is_null($left) ? "number" : $left[0];
    2101         if ($type == "number") $type = $right[0];
    2102 
    2103         $value = 0;
    2104         switch ($op) {
    2105         case '+':
    2106             $value = $left[1] + $right[1];
    2107             break; 
    2108         case '*':
    2109             $value = $left[1] * $right[1];
    2110             break; 
    2111         case '-':
    2112             $value = $left[1] - $right[1];
    2113             break; 
    2114         case '%':
    2115             $value = $left[1] % $right[1];
    2116             break; 
    2117         case '/':
    2118             if ($right[1] == 0) $this->throwError('parse error: divide by zero');
    2119             $value = $left[1] / $right[1];
    2120             break;
    2121         case '<':
    2122             return $this->toBool($left[1] < $right[1]);
    2123         case '>':
    2124             return $this->toBool($left[1] > $right[1]);
    2125         case '>=':
    2126             return $this->toBool($left[1] >= $right[1]);
    2127         case '=<':
    2128             return $this->toBool($left[1] <= $right[1]);
    2129         default:
    2130             $this->throwError('parse error: unknown number operator: '.$op);
    2131         }
    2132 
    2133         return array($type, $value);
    2134     }
    2135 
    2136 
    2137     /* environment functions */
    2138 
    2139     // push a new block on the stack, used for parsing
    2140     function pushBlock($tags) {
    2141         $b = new stdclass;
    2142         $b->parent = $this->env;
    2143 
    2144         $b->id = self::$nextBlockId++;
    2145         $b->is_vararg = false;
    2146         $b->tags = $tags;
    2147         $b->props = array();
    2148         $b->children = array();
    2149 
    2150         $this->env = $b;
    2151         return $b;
    2152     }
    2153    
    2154     // push a block that doesn't multiply tags
    2155     function pushSpecialBlock($name) {
    2156         $b = $this->pushBlock(array($name));
    2157         $b->no_multiply = true;
    2158         return $b;
    2159     }
    2160 
    2161     // used for compiliation variable state
    2162     function pushEnv() {
    2163         $e = new stdclass;
    2164         $e->parent = $this->env;
    2165 
    2166         $this->store = array();
    2167 
    2168         $this->env = $e;
    2169         return $e;
    2170     }
    2171 
    2172     // pop something off the stack
    2173     function pop() {
    2174         $old = $this->env;
    2175         $this->env = $this->env->parent;
    2176         return $old;
    2177     }
    2178 
    2179     // set something in the current env
    2180     function set($name, $value) {
    2181         $this->env->store[$name] = $value;
    2182     }
    2183 
    2184     // append an property
    2185     function append($prop, $pos = null) {
    2186         if (!is_null($pos)) $prop[-1] = $pos;
    2187         $this->env->props[] = $prop;
    2188     }
    2189 
    2190     // get the highest occurrence entry for a name
    2191     function get($name) {
    2192         $current = $this->env;
    2193 
    2194         $is_arguments = $name == $this->vPrefix . 'arguments';
    2195         while ($current) {
    2196             if ($is_arguments && isset($current->arguments)) {
    2197                 return array('list', ' ', $current->arguments);
    2198             }
    2199 
    2200             if (isset($current->store[$name]))
    2201                 return $current->store[$name];
    2202             else
    2203                 $current = $current->parent;
    2204         }
    2205 
    2206         return null;
    2207     }
    2208    
    2209     /* raw parsing functions */
    2210 
    2211     function literal($what, $eatWhitespace = true) {
    2212         // this is here mainly prevent notice from { } string accessor
    2213         if ($this->count >= strlen($this->buffer)) return false;
    2214 
    2215         // shortcut on single letter
    2216         if (!$eatWhitespace && strlen($what) == 1) {
    2217             if ($this->buffer{$this->count} == $what) {
    2218                 $this->count++;
    2219                 return true;
    2220             }
    2221             else return false;
    2222         }
    2223 
    2224         return $this->match($this->preg_quote($what), $m, $eatWhitespace);
    2225     }
    2226 
    2227     function preg_quote($what) {
    2228         return preg_quote($what, '/');
    2229     }
    2230 
    2231     // advance counter to next occurrence of $what
    2232     // $until - don't include $what in advance
    2233     // $allowNewline, if string, will be used as valid char set
    2234     function to($what, &$out, $until = false, $allowNewline = false) {
    2235         if (is_string($allowNewline)) {
    2236             $validChars = $allowNewline;
    2237         } else {
    2238             $validChars = $allowNewline ? "." : "[^\n]";
    2239         }
    2240         if (!$this->match('('.$validChars.'*?)'.$this->preg_quote($what), $m, !$until)) return false;
    2241         if ($until) $this->count -= strlen($what); // give back $what
    2242         $out = $m[1];
    2243         return true;
    2244     }
    2245    
    2246     // try to match something on head of buffer
    2247     function match($regex, &$out, $eatWhitespace = true) {
    2248         $r = '/'.$regex.($eatWhitespace ? '\s*' : '').'/Ais';
    2249         if (preg_match($r, $this->buffer, $out, null, $this->count)) {
    2250             $this->count += strlen($out[0]);
    2251             return true;
    2252         }
    2253         return false;
    2254     }
    2255 
    2256     // match something without consuming it
    2257     function peek($regex, &$out = null) {
    2258         $r = '/'.$regex.'/Ais';
    2259         $result = preg_match($r, $this->buffer, $out, null, $this->count);
    2260        
    2261         return $result;
    2262     }
    2263 
    2264     // seek to a spot in the buffer or return where we are on no argument
    2265     function seek($where = null) {
    2266         if ($where === null) return $this->count;
    2267         else $this->count = $where;
    2268         return true;
    2269     }
    2270 
    2271     /**
    2272      * Initialize state for a fresh parse
    2273      */
    2274     protected function prepareParser($buff) {
    2275         $this->env = null;
    2276         $this->expandStack = array();
    2277         $this->indentLevel = 0;
    2278         $this->count = 0;
    2279         $this->line = 1;
    2280 
    2281         $this->buffer = $this->removeComments($buff);
    2282         $this->pushBlock(null); // set up global scope
    2283 
    2284         // trim whitespace on head
    2285         if (preg_match('/^\s+/', $this->buffer, $m)) {
    2286             $this->line  += substr_count($m[0], "\n");
    2287             $this->buffer = ltrim($this->buffer);
    2288         }
    2289     }
    2290 
    2291     // create a child parser (for compiling an import)
    2292     protected function createChild($fname) {
    2293         $less = new lessc($fname);
    2294         $less->importDir = array_merge((array)$less->importDir, (array)$this->importDir);
    2295         $less->indentChar = $this->indentChar;
    2296         $less->compat = $this->compat;
    2297         return $less;
    2298     }
    2299 
    2300     // parse code and return intermediate tree
    2301     public function parseTree($str = null) {
    2302         $this->prepareParser(is_null($str) ? $this->buffer : $str);
    2303         while (false !== $this->parseChunk());
    2304 
    2305         if ($this->count != strlen($this->buffer))
    2306             $this->throwError();
    2307 
    2308         if (!is_null($this->env->parent))
    2309             throw new exception('parse error: unclosed block');
    2310 
    2311         $root = $this->env;
    2312         $this->env = null;
    2313         return $root;
    2314     }
    2315 
    2316     // inject array of unparsed strings into environment as variables
    2317     protected function injectVariables($args) {
    2318         $this->pushEnv();
    2319         $parser = new lessc();
    2320         foreach ($args as $name => $str_value) {
    2321             if ($name{0} != '@') $name = '@'.$name;
    2322             $parser->count = 0;
    2323             $parser->buffer = (string)$str_value;
    2324             if (!$parser->propertyValue($value)) {
    2325                 throw new Exception("failed to parse passed in variable $name: $str_value");
    2326             }
    2327 
    2328             $this->set($name, $value);
    2329         }
    2330     }
    2331    
    2332     // parse and compile buffer
    2333     function parse($str = null, $initial_variables = null) {
    2334         $locale = setlocale(LC_NUMERIC, 0);
    2335         setlocale(LC_NUMERIC, "C");
    2336         $root = $this->parseTree($str);
    2337 
    2338         if ($initial_variables) $this->injectVariables($initial_variables);
    2339         $out = $this->compileBlock($root);
    2340         setlocale(LC_NUMERIC, $locale);
    2341         return $out;
    2342     }
    2343 
    2344     /**
    2345      * Uses the current value of $this->count to show line and line number
    2346      */
    2347     function throwError($msg = 'parse error') {
    2348         if (!empty($this->sourceParser)) {
    2349             $this->sourceParser->count = $this->count;
    2350             return $this->sourceParser->throwError($msg);
    2351         } elseif ($this->count > 0) {
    2352             $line = $this->line + substr_count(substr($this->buffer, 0, $this->count), "\n");
    2353             if (isset($this->fileName)) {
    2354                 $loc = $this->fileName.' on line '.$line;
    2355             } else {
    2356                 $loc = "line: ".$line;
    2357             }
    2358 
    2359             if ($this->peek("(.*?)(\n|$)", $m))
    2360                 throw new exception($msg.': failed at `'.$m[1].'` '.$loc);
    2361         }
    2362 
    2363         throw new exception($msg);
    2364     }
    2365 
    2366     /**
    2367      * Initialize any static state, can initialize parser for a file
    2368      */
    2369     function __construct($fname = null, $opts = null) {
    2370         if (!self::$operatorString) {
    2371             self::$operatorString =
    2372                 '('.implode('|', array_map(array($this, 'preg_quote'),
    2373                     array_keys(self::$precedence))).')';
    2374         }
    2375 
    2376         if ($fname) {
    2377             if (!is_file($fname)) {
    2378                 throw new Exception('load error: failed to find '.$fname);
    2379             }
    2380             $pi = pathinfo($fname);
    2381 
    2382             $this->fileName = $fname;
    2383             $this->importDir = $pi['dirname'].'/';
    2384             $this->buffer = file_get_contents($fname);
    2385 
    2386             $this->addParsedFile($fname);
    2387         }
    2388     }
    2389 
    2390     public function registerFunction($name, $func) {
    2391         $this->libFunctions[$name] = $func;
    2392     }
    2393 
    2394     public function unregisterFunction($name) {
    2395         unset($this->libFunctions[$name]);
    2396     }
    2397 
    2398     // remove comments from $text
    2399     // todo: make it work for all functions, not just url
    2400     function removeComments($text) {
    2401         $look = array(
    2402             'url(', '//', '/*', '"', "'"
    2403         );
    2404 
    2405         $out = '';
    2406         $min = null;
    2407         $done = false;
    2408         while (true) {
    2409             // find the next item
    2410             foreach ($look as $token) {
    2411                 $pos = strpos($text, $token);
    2412                 if ($pos !== false) {
    2413                     if (!isset($min) || $pos < $min[1]) $min = array($token, $pos);
    2414                 }
    2415             }
    2416 
    2417             if (is_null($min)) break;
    2418 
    2419             $count = $min[1];
    2420             $skip = 0;
    2421             $newlines = 0;
    2422             switch ($min[0]) {
    2423             case 'url(':
    2424                 if (preg_match('/url\(.*?\)/', $text, $m, 0, $count))
    2425                     $count += strlen($m[0]) - strlen($min[0]);
    2426                 break;
    2427             case '"':
    2428             case "'":
    2429                 if (preg_match('/'.$min[0].'.*?'.$min[0].'/', $text, $m, 0, $count))
    2430                     $count += strlen($m[0]) - 1;
    2431                 break;
    2432             case '//':
    2433                 $skip = strpos($text, "\n", $count);
    2434                 if ($skip === false) $skip = strlen($text) - $count;
    2435                 else $skip -= $count;
    2436                 break;
    2437             case '/*':
    2438                 if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) {
    2439                     $skip = strlen($m[0]);
    2440                     $newlines = substr_count($m[0], "\n");
    2441                 }
    2442                 break;
    2443             }
    2444 
    2445             if ($skip == 0) $count += strlen($min[0]);
    2446 
    2447             $out .= substr($text, 0, $count).str_repeat("\n", $newlines);
    2448             $text = substr($text, $count + $skip);
    2449 
    2450             $min = null;
    2451         }
    2452 
    2453         return $out.$text;
    2454     }
    2455 
    2456     public function allParsedFiles() { return $this->allParsedFiles; }
    2457     protected function addParsedFile($file) {
    2458         $this->allParsedFiles[realpath($file)] = filemtime($file);
    2459     }
    2460 
    2461 
    2462     // compile file $in to file $out if $in is newer than $out
    2463     // returns true when it compiles, false otherwise
    2464     public static function ccompile($in, $out) {
    2465         if (!is_file($out) || filemtime($in) > filemtime($out)) {
    2466             $less = new lessc($in);
    2467             file_put_contents($out, $less->parse());
    2468             return true;
    2469         }
    2470 
    2471         return false;
    2472     }
    2473 
    2474     /**
    2475      * Execute lessphp on a .less file or a lessphp cache structure
    2476      *
    2477      * The lessphp cache structure contains information about a specific
    2478      * less file having been parsed. It can be used as a hint for future
    2479      * calls to determine whether or not a rebuild is required.
    2480      *
    2481      * The cache structure contains two important keys that may be used
    2482      * externally:
    2483      *
    2484      * compiled: The final compiled CSS
    2485      * updated: The time (in seconds) the CSS was last compiled
    2486      *
    2487      * The cache structure is a plain-ol' PHP associative array and can
    2488      * be serialized and unserialized without a hitch.
    2489      *
    2490      * @param mixed $in Input
    2491      * @param bool $force Force rebuild?
    2492      * @return array lessphp cache structure
    2493      */
    2494     public static function cexecute($in, $force = false) {
    2495 
    2496         // assume no root
    2497         $root = null;
    2498 
    2499         if (is_string($in)) {
    2500             $root = $in;
    2501         } elseif (is_array($in) and isset($in['root'])) {
    2502             if ($force or ! isset($in['files'])) {
    2503                 // If we are forcing a recompile or if for some reason the
    2504                 // structure does not contain any file information we should
    2505                 // specify the root to trigger a rebuild.
    2506                 $root = $in['root'];
    2507             } elseif (isset($in['files']) and is_array($in['files'])) {
    2508                 foreach ($in['files'] as $fname => $ftime ) {
    2509                     if (!file_exists($fname) or filemtime($fname) > $ftime) {
    2510                         // One of the files we knew about previously has changed
    2511                         // so we should look at our incoming root again.
    2512                         $root = $in['root'];
    2513                         break;
    2514                     }
    2515                 }
    2516             }
    2517         } else {
    2518             // TODO: Throw an exception? We got neither a string nor something
    2519             // that looks like a compatible lessphp cache structure.
    2520             return null;
    2521         }
    2522 
    2523         if ($root !== null) {
    2524             // If we have a root value which means we should rebuild.
    2525             $less = new lessc($root);
    2526             $out = array();
    2527             $out['root'] = $root;
    2528             $out['compiled'] = $less->parse();
    2529             $out['files'] = $less->allParsedFiles();
    2530             $out['updated'] = time();
    2531             return $out;
    2532         } else {
    2533             // No changes, pass back the structure
    2534             // we were given initially.
    2535             return $in;
    2536         }
    2537 
    2538     }
    2539 
    2540     static protected $cssColors = array(
    2541         'aliceblue' => '240,248,255',
    2542         'antiquewhite' => '250,235,215',
    2543         'aqua' => '0,255,255',
    2544         'aquamarine' => '127,255,212',
    2545         'azure' => '240,255,255',
    2546         'beige' => '245,245,220',
    2547         'bisque' => '255,228,196',
    2548         'black' => '0,0,0',
    2549         'blanchedalmond' => '255,235,205',
    2550         'blue' => '0,0,255',
    2551         'blueviolet' => '138,43,226',
    2552         'brown' => '165,42,42',
    2553         'burlywood' => '222,184,135',
    2554         'cadetblue' => '95,158,160',
    2555         'chartreuse' => '127,255,0',
    2556         'chocolate' => '210,105,30',
    2557         'coral' => '255,127,80',
    2558         'cornflowerblue' => '100,149,237',
    2559         'cornsilk' => '255,248,220',
    2560         'crimson' => '220,20,60',
    2561         'cyan' => '0,255,255',
    2562         'darkblue' => '0,0,139',
    2563         'darkcyan' => '0,139,139',
    2564         'darkgoldenrod' => '184,134,11',
    2565         'darkgray' => '169,169,169',
    2566         'darkgreen' => '0,100,0',
    2567         'darkgrey' => '169,169,169',
    2568         'darkkhaki' => '189,183,107',
    2569         'darkmagenta' => '139,0,139',
    2570         'darkolivegreen' => '85,107,47',
    2571         'darkorange' => '255,140,0',
    2572         'darkorchid' => '153,50,204',
    2573         'darkred' => '139,0,0',
    2574         'darksalmon' => '233,150,122',
    2575         'darkseagreen' => '143,188,143',
    2576         'darkslateblue' => '72,61,139',
    2577         'darkslategray' => '47,79,79',
    2578         'darkslategrey' => '47,79,79',
    2579         'darkturquoise' => '0,206,209',
    2580         'darkviolet' => '148,0,211',
    2581         'deeppink' => '255,20,147',
    2582         'deepskyblue' => '0,191,255',
    2583         'dimgray' => '105,105,105',
    2584         'dimgrey' => '105,105,105',
    2585         'dodgerblue' => '30,144,255',
    2586         'firebrick' => '178,34,34',
    2587         'floralwhite' => '255,250,240',
    2588         'forestgreen' => '34,139,34',
    2589         'fuchsia' => '255,0,255',
    2590         'gainsboro' => '220,220,220',
    2591         'ghostwhite' => '248,248,255',
    2592         'gold' => '255,215,0',
    2593         'goldenrod' => '218,165,32',
    2594         'gray' => '128,128,128',
    2595         'green' => '0,128,0',
    2596         'greenyellow' => '173,255,47',
    2597         'grey' => '128,128,128',
    2598         'honeydew' => '240,255,240',
    2599         'hotpink' => '255,105,180',
    2600         'indianred' => '205,92,92',
    2601         'indigo' => '75,0,130',
    2602         'ivory' => '255,255,240',
    2603         'khaki' => '240,230,140',
    2604         'lavender' => '230,230,250',
    2605         'lavenderblush' => '255,240,245',
    2606         'lawngreen' => '124,252,0',
    2607         'lemonchiffon' => '255,250,205',
    2608         'lightblue' => '173,216,230',
    2609         'lightcoral' => '240,128,128',
    2610         'lightcyan' => '224,255,255',
    2611         'lightgoldenrodyellow' => '250,250,210',
    2612         'lightgray' => '211,211,211',
    2613         'lightgreen' => '144,238,144',
    2614         'lightgrey' => '211,211,211',
    2615         'lightpink' => '255,182,193',
    2616         'lightsalmon' => '255,160,122',
    2617         'lightseagreen' => '32,178,170',
    2618         'lightskyblue' => '135,206,250',
    2619         'lightslategray' => '119,136,153',
    2620         'lightslategrey' => '119,136,153',
    2621         'lightsteelblue' => '176,196,222',
    2622         'lightyellow' => '255,255,224',
    2623         'lime' => '0,255,0',
    2624         'limegreen' => '50,205,50',
    2625         'linen' => '250,240,230',
    2626         'magenta' => '255,0,255',
    2627         'maroon' => '128,0,0',
    2628         'mediumaquamarine' => '102,205,170',
    2629         'mediumblue' => '0,0,205',
    2630         'mediumorchid' => '186,85,211',
    2631         'mediumpurple' => '147,112,219',
    2632         'mediumseagreen' => '60,179,113',
    2633         'mediumslateblue' => '123,104,238',
    2634         'mediumspringgreen' => '0,250,154',
    2635         'mediumturquoise' => '72,209,204',
    2636         'mediumvioletred' => '199,21,133',
    2637         'midnightblue' => '25,25,112',
    2638         'mintcream' => '245,255,250',
    2639         'mistyrose' => '255,228,225',
    2640         'moccasin' => '255,228,181',
    2641         'navajowhite' => '255,222,173',
    2642         'navy' => '0,0,128',
    2643         'oldlace' => '253,245,230',
    2644         'olive' => '128,128,0',
    2645         'olivedrab' => '107,142,35',
    2646         'orange' => '255,165,0',
    2647         'orangered' => '255,69,0',
    2648         'orchid' => '218,112,214',
    2649         'palegoldenrod' => '238,232,170',
    2650         'palegreen' => '152,251,152',
    2651         'paleturquoise' => '175,238,238',
    2652         'palevioletred' => '219,112,147',
    2653         'papayawhip' => '255,239,213',
    2654         'peachpuff' => '255,218,185',
    2655         'peru' => '205,133,63',
    2656         'pink' => '255,192,203',
    2657         'plum' => '221,160,221',
    2658         'powderblue' => '176,224,230',
    2659         'purple' => '128,0,128',
    2660         'red' => '255,0,0',
    2661         'rosybrown' => '188,143,143',
    2662         'royalblue' => '65,105,225',
    2663         'saddlebrown' => '139,69,19',
    2664         'salmon' => '250,128,114',
    2665         'sandybrown' => '244,164,96',
    2666         'seagreen' => '46,139,87',
    2667         'seashell' => '255,245,238',
    2668         'sienna' => '160,82,45',
    2669         'silver' => '192,192,192',
    2670         'skyblue' => '135,206,235',
    2671         'slateblue' => '106,90,205',
    2672         'slategray' => '112,128,144',
    2673         'slategrey' => '112,128,144',
    2674         'snow' => '255,250,250',
    2675         'springgreen' => '0,255,127',
    2676         'steelblue' => '70,130,180',
    2677         'tan' => '210,180,140',
    2678         'teal' => '0,128,128',
    2679         'thistle' => '216,191,216',
    2680         'tomato' => '255,99,71',
    2681         'turquoise' => '64,224,208',
    2682         'violet' => '238,130,238',
    2683         'wheat' => '245,222,179',
    2684         'white' => '255,255,255',
    2685         'whitesmoke' => '245,245,245',
    2686         'yellow' => '255,255,0',
    2687         'yellowgreen' => '154,205,50'
    2688     );
     41    static public $VERSION = "v0.5.0";
     42
     43    static public $TRUE = array("keyword", "true");
     44    static public $FALSE = array("keyword", "false");
     45
     46    protected $libFunctions = array();
     47    protected $registeredVars = array();
     48    protected $preserveComments = false;
     49
     50    public $vPrefix = '@'; // prefix of abstract properties
     51    public $mPrefix = '$'; // prefix of abstract blocks
     52    public $parentSelector = '&';
     53
     54    public $importDisabled = false;
     55    public $importDir = '';
     56
     57    protected $numberPrecision = null;
     58
     59    protected $allParsedFiles = array();
     60
     61    // set to the parser that generated the current line when compiling
     62    // so we know how to create error messages
     63    protected $sourceParser = null;
     64    protected $sourceLoc = null;
     65
     66    static protected $nextImportId = 0; // uniquely identify imports
     67
     68    // attempts to find the path of an import url, returns null for css files
     69    protected function findImport($url) {
     70        foreach ((array)$this->importDir as $dir) {
     71            $full = $dir.(substr($dir, -1) != '/' ? '/' : '').$url;
     72            if ($this->fileExists($file = $full.'.less') || $this->fileExists($file = $full)) {
     73                return $file;
     74            }
     75        }
     76
     77        return null;
     78    }
     79
     80    protected function fileExists($name) {
     81        return is_file($name);
     82    }
     83
     84    public static function compressList($items, $delim) {
     85        if (!isset($items[1]) && isset($items[0])) return $items[0];
     86        else return array('list', $delim, $items);
     87    }
     88
     89    public static function preg_quote($what) {
     90        return preg_quote($what, '/');
     91    }
     92
     93    protected function tryImport($importPath, $parentBlock, $out) {
     94        if ($importPath[0] == "function" && $importPath[1] == "url") {
     95            $importPath = $this->flattenList($importPath[2]);
     96        }
     97
     98        $str = $this->coerceString($importPath);
     99        if ($str === null) return false;
     100
     101        $url = $this->compileValue($this->lib_e($str));
     102
     103        // don't import if it ends in css
     104        if (substr_compare($url, '.css', -4, 4) === 0) return false;
     105
     106        $realPath = $this->findImport($url);
     107
     108        if ($realPath === null) return false;
     109
     110        if ($this->importDisabled) {
     111            return array(false, "/* import disabled */");
     112        }
     113
     114        if (isset($this->allParsedFiles[realpath($realPath)])) {
     115            return array(false, null);
     116        }
     117
     118        $this->addParsedFile($realPath);
     119        $parser = $this->makeParser($realPath);
     120        $root = $parser->parse(file_get_contents($realPath));
     121
     122        // set the parents of all the block props
     123        foreach ($root->props as $prop) {
     124            if ($prop[0] == "block") {
     125                $prop[1]->parent = $parentBlock;
     126            }
     127        }
     128
     129        // copy mixins into scope, set their parents
     130        // bring blocks from import into current block
     131        // TODO: need to mark the source parser these came from this file
     132        foreach ($root->children as $childName => $child) {
     133            if (isset($parentBlock->children[$childName])) {
     134                $parentBlock->children[$childName] = array_merge(
     135                    $parentBlock->children[$childName],
     136                    $child);
     137            } else {
     138                $parentBlock->children[$childName] = $child;
     139            }
     140        }
     141
     142        $pi = pathinfo($realPath);
     143        $dir = $pi["dirname"];
     144
     145        list($top, $bottom) = $this->sortProps($root->props, true);
     146        $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir);
     147
     148        return array(true, $bottom, $parser, $dir);
     149    }
     150
     151    protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir) {
     152        $oldSourceParser = $this->sourceParser;
     153
     154        $oldImport = $this->importDir;
     155
     156        // TODO: this is because the importDir api is stupid
     157        $this->importDir = (array)$this->importDir;
     158        array_unshift($this->importDir, $importDir);
     159
     160        foreach ($props as $prop) {
     161            $this->compileProp($prop, $block, $out);
     162        }
     163
     164        $this->importDir = $oldImport;
     165        $this->sourceParser = $oldSourceParser;
     166    }
     167
     168    /**
     169     * Recursively compiles a block.
     170     *
     171     * A block is analogous to a CSS block in most cases. A single LESS document
     172     * is encapsulated in a block when parsed, but it does not have parent tags
     173     * so all of it's children appear on the root level when compiled.
     174     *
     175     * Blocks are made up of props and children.
     176     *
     177     * Props are property instructions, array tuples which describe an action
     178     * to be taken, eg. write a property, set a variable, mixin a block.
     179     *
     180     * The children of a block are just all the blocks that are defined within.
     181     * This is used to look up mixins when performing a mixin.
     182     *
     183     * Compiling the block involves pushing a fresh environment on the stack,
     184     * and iterating through the props, compiling each one.
     185     *
     186     * See seed_wnb_lessc::compileProp()
     187     *
     188     */
     189    protected function compileBlock($block) {
     190        switch ($block->type) {
     191        case "root":
     192            $this->compileRoot($block);
     193            break;
     194        case null:
     195            $this->compileCSSBlock($block);
     196            break;
     197        case "media":
     198            $this->compileMedia($block);
     199            break;
     200        case "directive":
     201            $name = "@" . $block->name;
     202            if (!empty($block->value)) {
     203                $name .= " " . $this->compileValue($this->reduce($block->value));
     204            }
     205
     206            $this->compileNestedBlock($block, array($name));
     207            break;
     208        default:
     209            $this->throwError("unknown block type: $block->type\n");
     210        }
     211    }
     212
     213    protected function compileCSSBlock($block) {
     214        $env = $this->pushEnv();
     215
     216        $selectors = $this->compileSelectors($block->tags);
     217        $env->selectors = $this->multiplySelectors($selectors);
     218        $out = $this->makeOutputBlock(null, $env->selectors);
     219
     220        $this->scope->children[] = $out;
     221        $this->compileProps($block, $out);
     222
     223        $block->scope = $env; // mixins carry scope with them!
     224        $this->popEnv();
     225    }
     226
     227    protected function compileMedia($media) {
     228        $env = $this->pushEnv($media);
     229        $parentScope = $this->mediaParent($this->scope);
     230
     231        $query = $this->compileMediaQuery($this->multiplyMedia($env));
     232
     233        $this->scope = $this->makeOutputBlock($media->type, array($query));
     234        $parentScope->children[] = $this->scope;
     235
     236        $this->compileProps($media, $this->scope);
     237
     238        if (count($this->scope->lines) > 0) {
     239            $orphanSelelectors = $this->findClosestSelectors();
     240            if (!is_null($orphanSelelectors)) {
     241                $orphan = $this->makeOutputBlock(null, $orphanSelelectors);
     242                $orphan->lines = $this->scope->lines;
     243                array_unshift($this->scope->children, $orphan);
     244                $this->scope->lines = array();
     245            }
     246        }
     247
     248        $this->scope = $this->scope->parent;
     249        $this->popEnv();
     250    }
     251
     252    protected function mediaParent($scope) {
     253        while (!empty($scope->parent)) {
     254            if (!empty($scope->type) && $scope->type != "media") {
     255                break;
     256            }
     257            $scope = $scope->parent;
     258        }
     259
     260        return $scope;
     261    }
     262
     263    protected function compileNestedBlock($block, $selectors) {
     264        $this->pushEnv($block);
     265        $this->scope = $this->makeOutputBlock($block->type, $selectors);
     266        $this->scope->parent->children[] = $this->scope;
     267
     268        $this->compileProps($block, $this->scope);
     269
     270        $this->scope = $this->scope->parent;
     271        $this->popEnv();
     272    }
     273
     274    protected function compileRoot($root) {
     275        $this->pushEnv();
     276        $this->scope = $this->makeOutputBlock($root->type);
     277        $this->compileProps($root, $this->scope);
     278        $this->popEnv();
     279    }
     280
     281    protected function compileProps($block, $out) {
     282        foreach ($this->sortProps($block->props) as $prop) {
     283            $this->compileProp($prop, $block, $out);
     284        }
     285        $out->lines = $this->deduplicate($out->lines);
     286    }
     287
     288    /**
     289     * Deduplicate lines in a block. Comments are not deduplicated. If a
     290     * duplicate rule is detected, the comments immediately preceding each
     291     * occurence are consolidated.
     292     */
     293    protected function deduplicate($lines) {
     294        $unique = array();
     295        $comments = array();
     296
     297        foreach ($lines as $line) {
     298            if (strpos($line, '/*') === 0) {
     299                $comments[] = $line;
     300                continue;
     301            }
     302            if (!in_array($line, $unique)) {
     303                $unique[] = $line;
     304            }
     305            array_splice($unique, array_search($line, $unique), 0, $comments);
     306            $comments = array();
     307        }
     308        return array_merge($unique, $comments);
     309    }
     310
     311    protected function sortProps($props, $split = false) {
     312        $vars = array();
     313        $imports = array();
     314        $other = array();
     315        $stack = array();
     316
     317        foreach ($props as $prop) {
     318            switch ($prop[0]) {
     319            case "comment":
     320                $stack[] = $prop;
     321                break;
     322            case "assign":
     323                $stack[] = $prop;
     324                if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) {
     325                    $vars = array_merge($vars, $stack);
     326                } else {
     327                    $other = array_merge($other, $stack);
     328                }
     329                $stack = array();
     330                break;
     331            case "import":
     332                $id = self::$nextImportId++;
     333                $prop[] = $id;
     334                $stack[] = $prop;
     335                $imports = array_merge($imports, $stack);
     336                $other[] = array("import_mixin", $id);
     337                $stack = array();
     338                break;
     339            default:
     340                $stack[] = $prop;
     341                $other = array_merge($other, $stack);
     342                $stack = array();
     343                break;
     344            }
     345        }
     346        $other = array_merge($other, $stack);
     347
     348        if ($split) {
     349            return array(array_merge($imports, $vars), $other);
     350        } else {
     351            return array_merge($imports, $vars, $other);
     352        }
     353    }
     354
     355    protected function compileMediaQuery($queries) {
     356        $compiledQueries = array();
     357        foreach ($queries as $query) {
     358            $parts = array();
     359            foreach ($query as $q) {
     360                switch ($q[0]) {
     361                case "mediaType":
     362                    $parts[] = implode(" ", array_slice($q, 1));
     363                    break;
     364                case "mediaExp":
     365                    if (isset($q[2])) {
     366                        $parts[] = "($q[1]: " .
     367                            $this->compileValue($this->reduce($q[2])) . ")";
     368                    } else {
     369                        $parts[] = "($q[1])";
     370                    }
     371                    break;
     372                case "variable":
     373                    $parts[] = $this->compileValue($this->reduce($q));
     374                break;
     375                }
     376            }
     377
     378            if (count($parts) > 0) {
     379                $compiledQueries[] =  implode(" and ", $parts);
     380            }
     381        }
     382
     383        $out = "@media";
     384        if (!empty($parts)) {
     385            $out .= " " .
     386                implode($this->formatter->selectorSeparator, $compiledQueries);
     387        }
     388        return $out;
     389    }
     390
     391    protected function multiplyMedia($env, $childQueries = null) {
     392        if (is_null($env) ||
     393            !empty($env->block->type) && $env->block->type != "media"
     394        ) {
     395            return $childQueries;
     396        }
     397
     398        // plain old block, skip
     399        if (empty($env->block->type)) {
     400            return $this->multiplyMedia($env->parent, $childQueries);
     401        }
     402
     403        $out = array();
     404        $queries = $env->block->queries;
     405        if (is_null($childQueries)) {
     406            $out = $queries;
     407        } else {
     408            foreach ($queries as $parent) {
     409                foreach ($childQueries as $child) {
     410                    $out[] = array_merge($parent, $child);
     411                }
     412            }
     413        }
     414
     415        return $this->multiplyMedia($env->parent, $out);
     416    }
     417
     418    protected function expandParentSelectors(&$tag, $replace) {
     419        $parts = explode("$&$", $tag);
     420        $count = 0;
     421        foreach ($parts as &$part) {
     422            $part = str_replace($this->parentSelector, $replace, $part, $c);
     423            $count += $c;
     424        }
     425        $tag = implode($this->parentSelector, $parts);
     426        return $count;
     427    }
     428
     429    protected function findClosestSelectors() {
     430        $env = $this->env;
     431        $selectors = null;
     432        while ($env !== null) {
     433            if (isset($env->selectors)) {
     434                $selectors = $env->selectors;
     435                break;
     436            }
     437            $env = $env->parent;
     438        }
     439
     440        return $selectors;
     441    }
     442
     443
     444    // multiply $selectors against the nearest selectors in env
     445    protected function multiplySelectors($selectors) {
     446        // find parent selectors
     447
     448        $parentSelectors = $this->findClosestSelectors();
     449        if (is_null($parentSelectors)) {
     450            // kill parent reference in top level selector
     451            foreach ($selectors as &$s) {
     452                $this->expandParentSelectors($s, "");
     453            }
     454
     455            return $selectors;
     456        }
     457
     458        $out = array();
     459        foreach ($parentSelectors as $parent) {
     460            foreach ($selectors as $child) {
     461                $count = $this->expandParentSelectors($child, $parent);
     462
     463                // don't prepend the parent tag if & was used
     464                if ($count > 0) {
     465                    $out[] = trim($child);
     466                } else {
     467                    $out[] = trim($parent . ' ' . $child);
     468                }
     469            }
     470        }
     471
     472        return $out;
     473    }
     474
     475    // reduces selector expressions
     476    protected function compileSelectors($selectors) {
     477        $out = array();
     478
     479        foreach ($selectors as $s) {
     480            if (is_array($s)) {
     481                list(, $value) = $s;
     482                $out[] = trim($this->compileValue($this->reduce($value)));
     483            } else {
     484                $out[] = $s;
     485            }
     486        }
     487
     488        return $out;
     489    }
     490
     491    protected function eq($left, $right) {
     492        return $left == $right;
     493    }
     494
     495    protected function patternMatch($block, $orderedArgs, $keywordArgs) {
     496        // match the guards if it has them
     497        // any one of the groups must have all its guards pass for a match
     498        if (!empty($block->guards)) {
     499            $groupPassed = false;
     500            foreach ($block->guards as $guardGroup) {
     501                foreach ($guardGroup as $guard) {
     502                    $this->pushEnv();
     503                    $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs);
     504
     505                    $negate = false;
     506                    if ($guard[0] == "negate") {
     507                        $guard = $guard[1];
     508                        $negate = true;
     509                    }
     510
     511                    $passed = $this->reduce($guard) == self::$TRUE;
     512                    if ($negate) $passed = !$passed;
     513
     514                    $this->popEnv();
     515
     516                    if ($passed) {
     517                        $groupPassed = true;
     518                    } else {
     519                        $groupPassed = false;
     520                        break;
     521                    }
     522                }
     523
     524                if ($groupPassed) break;
     525            }
     526
     527            if (!$groupPassed) {
     528                return false;
     529            }
     530        }
     531
     532        if (empty($block->args)) {
     533            return $block->isVararg || empty($orderedArgs) && empty($keywordArgs);
     534        }
     535
     536        $remainingArgs = $block->args;
     537        if ($keywordArgs) {
     538            $remainingArgs = array();
     539            foreach ($block->args as $arg) {
     540                if ($arg[0] == "arg" && isset($keywordArgs[$arg[1]])) {
     541                    continue;
     542                }
     543
     544                $remainingArgs[] = $arg;
     545            }
     546        }
     547
     548        $i = -1; // no args
     549        // try to match by arity or by argument literal
     550        foreach ($remainingArgs as $i => $arg) {
     551            switch ($arg[0]) {
     552            case "lit":
     553                if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) {
     554                    return false;
     555                }
     556                break;
     557            case "arg":
     558                // no arg and no default value
     559                if (!isset($orderedArgs[$i]) && !isset($arg[2])) {
     560                    return false;
     561                }
     562                break;
     563            case "rest":
     564                $i--; // rest can be empty
     565                break 2;
     566            }
     567        }
     568
     569        if ($block->isVararg) {
     570            return true; // not having enough is handled above
     571        } else {
     572            $numMatched = $i + 1;
     573            // greater than because default values always match
     574            return $numMatched >= count($orderedArgs);
     575        }
     576    }
     577
     578    protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip=array()) {
     579        $matches = null;
     580        foreach ($blocks as $block) {
     581            // skip seen blocks that don't have arguments
     582            if (isset($skip[$block->id]) && !isset($block->args)) {
     583                continue;
     584            }
     585
     586            if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) {
     587                $matches[] = $block;
     588            }
     589        }
     590
     591        return $matches;
     592    }
     593
     594    // attempt to find blocks matched by path and args
     595    protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen=array()) {
     596        if ($searchIn == null) return null;
     597        if (isset($seen[$searchIn->id])) return null;
     598        $seen[$searchIn->id] = true;
     599
     600        $name = $path[0];
     601
     602        if (isset($searchIn->children[$name])) {
     603            $blocks = $searchIn->children[$name];
     604            if (count($path) == 1) {
     605                $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen);
     606                if (!empty($matches)) {
     607                    // This will return all blocks that match in the closest
     608                    // scope that has any matching block, like lessjs
     609                    return $matches;
     610                }
     611            } else {
     612                $matches = array();
     613                foreach ($blocks as $subBlock) {
     614                    $subMatches = $this->findBlocks($subBlock,
     615                        array_slice($path, 1), $orderedArgs, $keywordArgs, $seen);
     616
     617                    if (!is_null($subMatches)) {
     618                        foreach ($subMatches as $sm) {
     619                            $matches[] = $sm;
     620                        }
     621                    }
     622                }
     623
     624                return count($matches) > 0 ? $matches : null;
     625            }
     626        }
     627        if ($searchIn->parent === $searchIn) return null;
     628        return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen);
     629    }
     630
     631    // sets all argument names in $args to either the default value
     632    // or the one passed in through $values
     633    protected function zipSetArgs($args, $orderedValues, $keywordValues) {
     634        $assignedValues = array();
     635
     636        $i = 0;
     637        foreach ($args as $a) {
     638            if ($a[0] == "arg") {
     639                if (isset($keywordValues[$a[1]])) {
     640                    // has keyword arg
     641                    $value = $keywordValues[$a[1]];
     642                } elseif (isset($orderedValues[$i])) {
     643                    // has ordered arg
     644                    $value = $orderedValues[$i];
     645                    $i++;
     646                } elseif (isset($a[2])) {
     647                    // has default value
     648                    $value = $a[2];
     649                } else {
     650                    $this->throwError("Failed to assign arg " . $a[1]);
     651                    $value = null; // :(
     652                }
     653
     654                $value = $this->reduce($value);
     655                $this->set($a[1], $value);
     656                $assignedValues[] = $value;
     657            } else {
     658                // a lit
     659                $i++;
     660            }
     661        }
     662
     663        // check for a rest
     664        $last = end($args);
     665        if ($last[0] == "rest") {
     666            $rest = array_slice($orderedValues, count($args) - 1);
     667            $this->set($last[1], $this->reduce(array("list", " ", $rest)));
     668        }
     669
     670        // wow is this the only true use of PHP's + operator for arrays?
     671        $this->env->arguments = $assignedValues + $orderedValues;
     672    }
     673
     674    // compile a prop and update $lines or $blocks appropriately
     675    protected function compileProp($prop, $block, $out) {
     676        // set error position context
     677        $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1;
     678
     679        switch ($prop[0]) {
     680        case 'assign':
     681            list(, $name, $value) = $prop;
     682            if ($name[0] == $this->vPrefix) {
     683                $this->set($name, $value);
     684            } else {
     685                $out->lines[] = $this->formatter->property($name,
     686                        $this->compileValue($this->reduce($value)));
     687            }
     688            break;
     689        case 'block':
     690            list(, $child) = $prop;
     691            $this->compileBlock($child);
     692            break;
     693        case 'mixin':
     694            list(, $path, $args, $suffix) = $prop;
     695
     696            $orderedArgs = array();
     697            $keywordArgs = array();
     698            foreach ((array)$args as $arg) {
     699                $argval = null;
     700                switch ($arg[0]) {
     701                case "arg":
     702                    if (!isset($arg[2])) {
     703                        $orderedArgs[] = $this->reduce(array("variable", $arg[1]));
     704                    } else {
     705                        $keywordArgs[$arg[1]] = $this->reduce($arg[2]);
     706                    }
     707                    break;
     708
     709                case "lit":
     710                    $orderedArgs[] = $this->reduce($arg[1]);
     711                    break;
     712                default:
     713                    $this->throwError("Unknown arg type: " . $arg[0]);
     714                }
     715            }
     716
     717            $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs);
     718
     719            if ($mixins === null) {
     720                $this->throwError("{$prop[1][0]} is undefined");
     721            }
     722
     723            foreach ($mixins as $mixin) {
     724                if ($mixin === $block && !$orderedArgs) {
     725                    continue;
     726                }
     727
     728                $haveScope = false;
     729                if (isset($mixin->parent->scope)) {
     730                    $haveScope = true;
     731                    $mixinParentEnv = $this->pushEnv();
     732                    $mixinParentEnv->storeParent = $mixin->parent->scope;
     733                }
     734
     735                $haveArgs = false;
     736                if (isset($mixin->args)) {
     737                    $haveArgs = true;
     738                    $this->pushEnv();
     739                    $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs);
     740                }
     741
     742                $oldParent = $mixin->parent;
     743                if ($mixin != $block) $mixin->parent = $block;
     744
     745                foreach ($this->sortProps($mixin->props) as $subProp) {
     746                    if ($suffix !== null &&
     747                        $subProp[0] == "assign" &&
     748                        is_string($subProp[1]) &&
     749                        $subProp[1]{0} != $this->vPrefix
     750                    ) {
     751                        $subProp[2] = array(
     752                            'list', ' ',
     753                            array($subProp[2], array('keyword', $suffix))
     754                        );
     755                    }
     756
     757                    $this->compileProp($subProp, $mixin, $out);
     758                }
     759
     760                $mixin->parent = $oldParent;
     761
     762                if ($haveArgs) $this->popEnv();
     763                if ($haveScope) $this->popEnv();
     764            }
     765
     766            break;
     767        case 'raw':
     768            $out->lines[] = $prop[1];
     769            break;
     770        case "directive":
     771            list(, $name, $value) = $prop;
     772            $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)).';';
     773            break;
     774        case "comment":
     775            $out->lines[] = $prop[1];
     776            break;
     777        case "import":
     778            list(, $importPath, $importId) = $prop;
     779            $importPath = $this->reduce($importPath);
     780
     781            if (!isset($this->env->imports)) {
     782                $this->env->imports = array();
     783            }
     784
     785            $result = $this->tryImport($importPath, $block, $out);
     786
     787            $this->env->imports[$importId] = $result === false ?
     788                array(false, "@import " . $this->compileValue($importPath).";") :
     789                $result;
     790
     791            break;
     792        case "import_mixin":
     793            list(,$importId) = $prop;
     794            $import = $this->env->imports[$importId];
     795            if ($import[0] === false) {
     796                if (isset($import[1])) {
     797                    $out->lines[] = $import[1];
     798                }
     799            } else {
     800                list(, $bottom, $parser, $importDir) = $import;
     801                $this->compileImportedProps($bottom, $block, $out, $parser, $importDir);
     802            }
     803
     804            break;
     805        default:
     806            $this->throwError("unknown op: {$prop[0]}\n");
     807        }
     808    }
     809
     810
     811    /**
     812     * Compiles a primitive value into a CSS property value.
     813     *
     814     * Values in lessphp are typed by being wrapped in arrays, their format is
     815     * typically:
     816     *
     817     *     array(type, contents [, additional_contents]*)
     818     *
     819     * The input is expected to be reduced. This function will not work on
     820     * things like expressions and variables.
     821     */
     822    public function compileValue($value) {
     823        switch ($value[0]) {
     824        case 'list':
     825            // [1] - delimiter
     826            // [2] - array of values
     827            return implode($value[1], array_map(array($this, 'compileValue'), $value[2]));
     828        case 'raw_color':
     829            if (!empty($this->formatter->compressColors)) {
     830                return $this->compileValue($this->coerceColor($value));
     831            }
     832            return $value[1];
     833        case 'keyword':
     834            // [1] - the keyword
     835            return $value[1];
     836        case 'number':
     837            list(, $num, $unit) = $value;
     838            // [1] - the number
     839            // [2] - the unit
     840            if ($this->numberPrecision !== null) {
     841                $num = round($num, $this->numberPrecision);
     842            }
     843            return $num . $unit;
     844        case 'string':
     845            // [1] - contents of string (includes quotes)
     846            list(, $delim, $content) = $value;
     847            foreach ($content as &$part) {
     848                if (is_array($part)) {
     849                    $part = $this->compileValue($part);
     850                }
     851            }
     852            return $delim . implode($content) . $delim;
     853        case 'color':
     854            // [1] - red component (either number or a %)
     855            // [2] - green component
     856            // [3] - blue component
     857            // [4] - optional alpha component
     858            list(, $r, $g, $b) = $value;
     859            $r = round($r);
     860            $g = round($g);
     861            $b = round($b);
     862
     863            if (count($value) == 5 && $value[4] != 1) { // rgba
     864                return 'rgba('.$r.','.$g.','.$b.','.$value[4].')';
     865            }
     866
     867            $h = sprintf("#%02x%02x%02x", $r, $g, $b);
     868
     869            if (!empty($this->formatter->compressColors)) {
     870                // Converting hex color to short notation (e.g. #003399 to #039)
     871                if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
     872                    $h = '#' . $h[1] . $h[3] . $h[5];
     873                }
     874            }
     875
     876            return $h;
     877
     878        case 'function':
     879            list(, $name, $args) = $value;
     880            return $name.'('.$this->compileValue($args).')';
     881        default: // assumed to be unit
     882            $this->throwError("unknown value type: $value[0]");
     883        }
     884    }
     885
     886    protected function lib_pow($args) {
     887        list($base, $exp) = $this->assertArgs($args, 2, "pow");
     888        return pow($this->assertNumber($base), $this->assertNumber($exp));
     889    }
     890
     891    protected function lib_pi() {
     892        return pi();
     893    }
     894
     895    protected function lib_mod($args) {
     896        list($a, $b) = $this->assertArgs($args, 2, "mod");
     897        return $this->assertNumber($a) % $this->assertNumber($b);
     898    }
     899
     900    protected function lib_tan($num) {
     901        return tan($this->assertNumber($num));
     902    }
     903
     904    protected function lib_sin($num) {
     905        return sin($this->assertNumber($num));
     906    }
     907
     908    protected function lib_cos($num) {
     909        return cos($this->assertNumber($num));
     910    }
     911
     912    protected function lib_atan($num) {
     913        $num = atan($this->assertNumber($num));
     914        return array("number", $num, "rad");
     915    }
     916
     917    protected function lib_asin($num) {
     918        $num = asin($this->assertNumber($num));
     919        return array("number", $num, "rad");
     920    }
     921
     922    protected function lib_acos($num) {
     923        $num = acos($this->assertNumber($num));
     924        return array("number", $num, "rad");
     925    }
     926
     927    protected function lib_sqrt($num) {
     928        return sqrt($this->assertNumber($num));
     929    }
     930
     931    protected function lib_extract($value) {
     932        list($list, $idx) = $this->assertArgs($value, 2, "extract");
     933        $idx = $this->assertNumber($idx);
     934        // 1 indexed
     935        if ($list[0] == "list" && isset($list[2][$idx - 1])) {
     936            return $list[2][$idx - 1];
     937        }
     938    }
     939
     940    protected function lib_isnumber($value) {
     941        return $this->toBool($value[0] == "number");
     942    }
     943
     944    protected function lib_isstring($value) {
     945        return $this->toBool($value[0] == "string");
     946    }
     947
     948    protected function lib_iscolor($value) {
     949        return $this->toBool($this->coerceColor($value));
     950    }
     951
     952    protected function lib_iskeyword($value) {
     953        return $this->toBool($value[0] == "keyword");
     954    }
     955
     956    protected function lib_ispixel($value) {
     957        return $this->toBool($value[0] == "number" && $value[2] == "px");
     958    }
     959
     960    protected function lib_ispercentage($value) {
     961        return $this->toBool($value[0] == "number" && $value[2] == "%");
     962    }
     963
     964    protected function lib_isem($value) {
     965        return $this->toBool($value[0] == "number" && $value[2] == "em");
     966    }
     967
     968    protected function lib_isrem($value) {
     969        return $this->toBool($value[0] == "number" && $value[2] == "rem");
     970    }
     971
     972    protected function lib_rgbahex($color) {
     973        $color = $this->coerceColor($color);
     974        if (is_null($color)) {
     975            $this->throwError("color expected for rgbahex");
     976        }
     977
     978        return sprintf("#%02x%02x%02x%02x",
     979            isset($color[4]) ? $color[4] * 255 : 255,
     980            $color[1],
     981            $color[2],
     982            $color[3]
     983        );
     984    }
     985
     986    protected function lib_argb($color){
     987        return $this->lib_rgbahex($color);
     988    }
     989
     990    /**
     991     * Given an url, decide whether to output a regular link or the base64-encoded contents of the file
     992     *
     993     * @param  array  $value either an argument list (two strings) or a single string
     994     * @return string        formatted url(), either as a link or base64-encoded
     995     */
     996    protected function lib_data_uri($value) {
     997        $mime = ($value[0] === 'list') ? $value[2][0][2] : null;
     998        $url = ($value[0] === 'list') ? $value[2][1][2][0] : $value[2][0];
     999
     1000        $fullpath = $this->findImport($url);
     1001
     1002        if ($fullpath && ($fsize = filesize($fullpath)) !== false) {
     1003            // IE8 can't handle data uris larger than 32KB
     1004            if ($fsize/1024 < 32) {
     1005                if (is_null($mime)) {
     1006                    if (class_exists('finfo')) { // php 5.3+
     1007                        $finfo = new finfo(FILEINFO_MIME);
     1008                        $mime = explode('; ', $finfo->file($fullpath));
     1009                        $mime = $mime[0];
     1010                    } elseif (function_exists('mime_content_type')) { // PHP 5.2
     1011                        $mime = mime_content_type($fullpath);
     1012                    }
     1013                }
     1014
     1015                if (!is_null($mime)) // fallback if the mime type is still unknown
     1016                    $url = sprintf('data:%s;base64,%s', $mime, base64_encode(file_get_contents($fullpath)));
     1017            }
     1018        }
     1019
     1020        return 'url("'.$url.'")';
     1021    }
     1022
     1023    // utility func to unquote a string
     1024    protected function lib_e($arg) {
     1025        switch ($arg[0]) {
     1026            case "list":
     1027                $items = $arg[2];
     1028                if (isset($items[0])) {
     1029                    return $this->lib_e($items[0]);
     1030                }
     1031                $this->throwError("unrecognised input");
     1032            case "string":
     1033                $arg[1] = "";
     1034                return $arg;
     1035            case "keyword":
     1036                return $arg;
     1037            default:
     1038                return array("keyword", $this->compileValue($arg));
     1039        }
     1040    }
     1041
     1042    protected function lib__sprintf($args) {
     1043        if ($args[0] != "list") return $args;
     1044        $values = $args[2];
     1045        $string = array_shift($values);
     1046        $template = $this->compileValue($this->lib_e($string));
     1047
     1048        $i = 0;
     1049        if (preg_match_all('/%[dsa]/', $template, $m)) {
     1050            foreach ($m[0] as $match) {
     1051                $val = isset($values[$i]) ?
     1052                    $this->reduce($values[$i]) : array('keyword', '');
     1053
     1054                // lessjs compat, renders fully expanded color, not raw color
     1055                if ($color = $this->coerceColor($val)) {
     1056                    $val = $color;
     1057                }
     1058
     1059                $i++;
     1060                $rep = $this->compileValue($this->lib_e($val));
     1061                $template = preg_replace('/'.self::preg_quote($match).'/',
     1062                    $rep, $template, 1);
     1063            }
     1064        }
     1065
     1066        $d = $string[0] == "string" ? $string[1] : '"';
     1067        return array("string", $d, array($template));
     1068    }
     1069
     1070    protected function lib_floor($arg) {
     1071        $value = $this->assertNumber($arg);
     1072        return array("number", floor($value), $arg[2]);
     1073    }
     1074
     1075    protected function lib_ceil($arg) {
     1076        $value = $this->assertNumber($arg);
     1077        return array("number", ceil($value), $arg[2]);
     1078    }
     1079
     1080    protected function lib_round($arg) {
     1081        if ($arg[0] != "list") {
     1082            $value = $this->assertNumber($arg);
     1083            return array("number", round($value), $arg[2]);
     1084        } else {
     1085            $value = $this->assertNumber($arg[2][0]);
     1086            $precision = $this->assertNumber($arg[2][1]);
     1087            return array("number", round($value, $precision), $arg[2][0][2]);
     1088        }
     1089    }
     1090
     1091    protected function lib_unit($arg) {
     1092        if ($arg[0] == "list") {
     1093            list($number, $newUnit) = $arg[2];
     1094            return array("number", $this->assertNumber($number),
     1095                $this->compileValue($this->lib_e($newUnit)));
     1096        } else {
     1097            return array("number", $this->assertNumber($arg), "");
     1098        }
     1099    }
     1100
     1101    /**
     1102     * Helper function to get arguments for color manipulation functions.
     1103     * takes a list that contains a color like thing and a percentage
     1104     */
     1105    public function colorArgs($args) {
     1106        if ($args[0] != 'list' || count($args[2]) < 2) {
     1107            return array(array('color', 0, 0, 0), 0);
     1108        }
     1109        list($color, $delta) = $args[2];
     1110        $color = $this->assertColor($color);
     1111        $delta = floatval($delta[1]);
     1112
     1113        return array($color, $delta);
     1114    }
     1115
     1116    protected function lib_darken($args) {
     1117        list($color, $delta) = $this->colorArgs($args);
     1118
     1119        $hsl = $this->toHSL($color);
     1120        $hsl[3] = $this->clamp($hsl[3] - $delta, 100);
     1121        return $this->toRGB($hsl);
     1122    }
     1123
     1124    protected function lib_lighten($args) {
     1125        list($color, $delta) = $this->colorArgs($args);
     1126
     1127        $hsl = $this->toHSL($color);
     1128        $hsl[3] = $this->clamp($hsl[3] + $delta, 100);
     1129        return $this->toRGB($hsl);
     1130    }
     1131
     1132    protected function lib_saturate($args) {
     1133        list($color, $delta) = $this->colorArgs($args);
     1134
     1135        $hsl = $this->toHSL($color);
     1136        $hsl[2] = $this->clamp($hsl[2] + $delta, 100);
     1137        return $this->toRGB($hsl);
     1138    }
     1139
     1140    protected function lib_desaturate($args) {
     1141        list($color, $delta) = $this->colorArgs($args);
     1142
     1143        $hsl = $this->toHSL($color);
     1144        $hsl[2] = $this->clamp($hsl[2] - $delta, 100);
     1145        return $this->toRGB($hsl);
     1146    }
     1147
     1148    protected function lib_spin($args) {
     1149        list($color, $delta) = $this->colorArgs($args);
     1150
     1151        $hsl = $this->toHSL($color);
     1152
     1153        $hsl[1] = $hsl[1] + $delta % 360;
     1154        if ($hsl[1] < 0) {
     1155            $hsl[1] += 360;
     1156        }
     1157
     1158        return $this->toRGB($hsl);
     1159    }
     1160
     1161    protected function lib_fadeout($args) {
     1162        list($color, $delta) = $this->colorArgs($args);
     1163        $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100);
     1164        return $color;
     1165    }
     1166
     1167    protected function lib_fadein($args) {
     1168        list($color, $delta) = $this->colorArgs($args);
     1169        $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100);
     1170        return $color;
     1171    }
     1172
     1173    protected function lib_hue($color) {
     1174        $hsl = $this->toHSL($this->assertColor($color));
     1175        return round($hsl[1]);
     1176    }
     1177
     1178    protected function lib_saturation($color) {
     1179        $hsl = $this->toHSL($this->assertColor($color));
     1180        return round($hsl[2]);
     1181    }
     1182
     1183    protected function lib_lightness($color) {
     1184        $hsl = $this->toHSL($this->assertColor($color));
     1185        return round($hsl[3]);
     1186    }
     1187
     1188    // get the alpha of a color
     1189    // defaults to 1 for non-colors or colors without an alpha
     1190    protected function lib_alpha($value) {
     1191        if (!is_null($color = $this->coerceColor($value))) {
     1192            return isset($color[4]) ? $color[4] : 1;
     1193        }
     1194    }
     1195
     1196    // set the alpha of the color
     1197    protected function lib_fade($args) {
     1198        list($color, $alpha) = $this->colorArgs($args);
     1199        $color[4] = $this->clamp($alpha / 100.0);
     1200        return $color;
     1201    }
     1202
     1203    protected function lib_percentage($arg) {
     1204        $num = $this->assertNumber($arg);
     1205        return array("number", $num*100, "%");
     1206    }
     1207
     1208    /**
     1209     * Mix color with white in variable proportion.
     1210     *
     1211     * It is the same as calling `mix(#ffffff, @color, @weight)`.
     1212     *
     1213     *     tint(@color, [@weight: 50%]);
     1214     *
     1215     * http://seed_wnb_lesscss.org/functions/#color-operations-tint
     1216     *
     1217     * @return array Color
     1218     */
     1219    protected function lib_tint($args) {
     1220        $white = ['color', 255, 255, 255];
     1221        if ($args[0] == 'color') {
     1222            return $this->lib_mix([ 'list', ',', [$white, $args] ]);
     1223        } elseif ($args[0] == "list" && count($args[2]) == 2) {
     1224            return $this->lib_mix([ $args[0], $args[1], [$white, $args[2][0], $args[2][1]] ]);
     1225        } else {
     1226            $this->throwError("tint expects (color, weight)");
     1227        }
     1228    }
     1229
     1230    /**
     1231     * Mix color with black in variable proportion.
     1232     *
     1233     * It is the same as calling `mix(#000000, @color, @weight)`
     1234     *
     1235     *     shade(@color, [@weight: 50%]);
     1236     *
     1237     * http://seed_wnb_lesscss.org/functions/#color-operations-shade
     1238     *
     1239     * @return array Color
     1240     */
     1241    protected function lib_shade($args) {
     1242        $black = ['color', 0, 0, 0];
     1243        if ($args[0] == 'color') {
     1244            return $this->lib_mix([ 'list', ',', [$black, $args] ]);
     1245        } elseif ($args[0] == "list" && count($args[2]) == 2) {
     1246            return $this->lib_mix([ $args[0], $args[1], [$black, $args[2][0], $args[2][1]] ]);
     1247        } else {
     1248            $this->throwError("shade expects (color, weight)");
     1249        }
     1250    }
     1251
     1252    // mixes two colors by weight
     1253    // mix(@color1, @color2, [@weight: 50%]);
     1254    // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method
     1255    protected function lib_mix($args) {
     1256        if ($args[0] != "list" || count($args[2]) < 2)
     1257            $this->throwError("mix expects (color1, color2, weight)");
     1258
     1259        list($first, $second) = $args[2];
     1260        $first = $this->assertColor($first);
     1261        $second = $this->assertColor($second);
     1262
     1263        $first_a = $this->lib_alpha($first);
     1264        $second_a = $this->lib_alpha($second);
     1265
     1266        if (isset($args[2][2])) {
     1267            $weight = $args[2][2][1] / 100.0;
     1268        } else {
     1269            $weight = 0.5;
     1270        }
     1271
     1272        $w = $weight * 2 - 1;
     1273        $a = $first_a - $second_a;
     1274
     1275        $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
     1276        $w2 = 1.0 - $w1;
     1277
     1278        $new = array('color',
     1279            $w1 * $first[1] + $w2 * $second[1],
     1280            $w1 * $first[2] + $w2 * $second[2],
     1281            $w1 * $first[3] + $w2 * $second[3],
     1282        );
     1283
     1284        if ($first_a != 1.0 || $second_a != 1.0) {
     1285            $new[] = $first_a * $weight + $second_a * ($weight - 1);
     1286        }
     1287
     1288        return $this->fixColor($new);
     1289    }
     1290
     1291    protected function lib_contrast($args) {
     1292        $darkColor  = array('color', 0, 0, 0);
     1293        $lightColor = array('color', 255, 255, 255);
     1294        $threshold  = 0.43;
     1295
     1296        if ( $args[0] == 'list' ) {
     1297            $inputColor = ( isset($args[2][0]) ) ? $this->assertColor($args[2][0])  : $lightColor;
     1298            $darkColor  = ( isset($args[2][1]) ) ? $this->assertColor($args[2][1])  : $darkColor;
     1299            $lightColor = ( isset($args[2][2]) ) ? $this->assertColor($args[2][2])  : $lightColor;
     1300            $threshold  = ( isset($args[2][3]) ) ? $this->assertNumber($args[2][3]) : $threshold;
     1301        }
     1302        else {
     1303            $inputColor  = $this->assertColor($args);
     1304        }
     1305
     1306        $inputColor = $this->coerceColor($inputColor);
     1307        $darkColor  = $this->coerceColor($darkColor);
     1308        $lightColor = $this->coerceColor($lightColor);
     1309
     1310        //Figure out which is actually light and dark!
     1311        if ( $this->toLuma($darkColor) > $this->toLuma($lightColor) ) {
     1312            $t  = $lightColor;
     1313            $lightColor = $darkColor;
     1314            $darkColor  = $t;
     1315        }
     1316
     1317        $inputColor_alpha = $this->lib_alpha($inputColor);
     1318        if ( ( $this->toLuma($inputColor) * $inputColor_alpha) < $threshold) {
     1319            return $lightColor;
     1320        }
     1321        return $darkColor;
     1322    }
     1323
     1324    private function toLuma($color) {
     1325        list(, $r, $g, $b) = $this->coerceColor($color);
     1326
     1327        $r = $r / 255;
     1328        $g = $g / 255;
     1329        $b = $b / 255;
     1330
     1331        $r = ($r <= 0.03928) ? $r / 12.92 : pow((($r + 0.055) / 1.055), 2.4);
     1332        $g = ($g <= 0.03928) ? $g / 12.92 : pow((($g + 0.055) / 1.055), 2.4);
     1333        $b = ($b <= 0.03928) ? $b / 12.92 : pow((($b + 0.055) / 1.055), 2.4);
     1334
     1335        return (0.2126 * $r) + (0.7152 * $g) + (0.0722 * $b);
     1336    }
     1337
     1338    protected function lib_luma($color) {
     1339        return array("number", round($this->toLuma($color) * 100, 8), "%");
     1340    }
     1341
     1342
     1343    public function assertColor($value, $error = "expected color value") {
     1344        $color = $this->coerceColor($value);
     1345        if (is_null($color)) $this->throwError($error);
     1346        return $color;
     1347    }
     1348
     1349    public function assertNumber($value, $error = "expecting number") {
     1350        if ($value[0] == "number") return $value[1];
     1351        $this->throwError($error);
     1352    }
     1353
     1354    public function assertArgs($value, $expectedArgs, $name="") {
     1355        if ($expectedArgs == 1) {
     1356            return $value;
     1357        } else {
     1358            if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list");
     1359            $values = $value[2];
     1360            $numValues = count($values);
     1361            if ($expectedArgs != $numValues) {
     1362                if ($name) {
     1363                    $name = $name . ": ";
     1364                }
     1365
     1366                $this->throwError("${name}expecting $expectedArgs arguments, got $numValues");
     1367            }
     1368
     1369            return $values;
     1370        }
     1371    }
     1372
     1373    protected function toHSL($color) {
     1374        if ($color[0] === 'hsl') {
     1375            return $color;
     1376        }
     1377
     1378        $r = $color[1] / 255;
     1379        $g = $color[2] / 255;
     1380        $b = $color[3] / 255;
     1381
     1382        $min = min($r, $g, $b);
     1383        $max = max($r, $g, $b);
     1384
     1385        $L = ($min + $max) / 2;
     1386        if ($min == $max) {
     1387            $S = $H = 0;
     1388        } else {
     1389            if ($L < 0.5) {
     1390                $S = ($max - $min) / ($max + $min);
     1391            } else {
     1392                $S = ($max - $min) / (2.0 - $max - $min);
     1393            }
     1394            if ($r == $max) {
     1395                $H = ($g - $b) / ($max - $min);
     1396            } elseif ($g == $max) {
     1397                $H = 2.0 + ($b - $r) / ($max - $min);
     1398            } elseif ($b == $max) {
     1399                $H = 4.0 + ($r - $g) / ($max - $min);
     1400            }
     1401
     1402        }
     1403
     1404        $out = array('hsl',
     1405            ($H < 0 ? $H + 6 : $H)*60,
     1406            $S * 100,
     1407            $L * 100,
     1408        );
     1409
     1410        if (count($color) > 4) {
     1411            // copy alpha
     1412            $out[] = $color[4];
     1413        }
     1414        return $out;
     1415    }
     1416
     1417    protected function toRGB_helper($comp, $temp1, $temp2) {
     1418        if ($comp < 0) {
     1419            $comp += 1.0;
     1420        } elseif ($comp > 1) {
     1421            $comp -= 1.0;
     1422        }
     1423
     1424        if (6 * $comp < 1) {
     1425            return $temp1 + ($temp2 - $temp1) * 6 * $comp;
     1426        }
     1427        if (2 * $comp < 1) {
     1428            return $temp2;
     1429        }
     1430        if (3 * $comp < 2) {
     1431            return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6;
     1432        }
     1433
     1434        return $temp1;
     1435    }
     1436
     1437    /**
     1438     * Converts a hsl array into a color value in rgb.
     1439     * Expects H to be in range of 0 to 360, S and L in 0 to 100
     1440     */
     1441    protected function toRGB($color) {
     1442        if ($color[0] === 'color') {
     1443            return $color;
     1444        }
     1445
     1446        $H = $color[1] / 360;
     1447        $S = $color[2] / 100;
     1448        $L = $color[3] / 100;
     1449
     1450        if ($S == 0) {
     1451            $r = $g = $b = $L;
     1452        } else {
     1453            $temp2 = $L < 0.5 ?
     1454                $L * (1.0 + $S) :
     1455                $L + $S - $L * $S;
     1456
     1457            $temp1 = 2.0 * $L - $temp2;
     1458
     1459            $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2);
     1460            $g = $this->toRGB_helper($H, $temp1, $temp2);
     1461            $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2);
     1462        }
     1463
     1464        // $out = array('color', round($r*255), round($g*255), round($b*255));
     1465        $out = array('color', $r*255, $g*255, $b*255);
     1466        if (count($color) > 4) {
     1467            // copy alpha
     1468            $out[] = $color[4];
     1469        }
     1470        return $out;
     1471    }
     1472
     1473    protected function clamp($v, $max = 1, $min = 0) {
     1474        return min($max, max($min, $v));
     1475    }
     1476
     1477    /**
     1478     * Convert the rgb, rgba, hsl color literals of function type
     1479     * as returned by the parser into values of color type.
     1480     */
     1481    protected function funcToColor($func) {
     1482        $fname = $func[1];
     1483        if ($func[2][0] != 'list') {
     1484            // need a list of arguments
     1485            return false;
     1486        }
     1487        $rawComponents = $func[2][2];
     1488
     1489        if ($fname == 'hsl' || $fname == 'hsla') {
     1490            $hsl = array('hsl');
     1491            $i = 0;
     1492            foreach ($rawComponents as $c) {
     1493                $val = $this->reduce($c);
     1494                $val = isset($val[1]) ? floatval($val[1]) : 0;
     1495
     1496                if ($i == 0) {
     1497                    $clamp = 360;
     1498                } elseif ($i < 3) {
     1499                    $clamp = 100;
     1500                } else {
     1501                    $clamp = 1;
     1502                }
     1503
     1504                $hsl[] = $this->clamp($val, $clamp);
     1505                $i++;
     1506            }
     1507
     1508            while (count($hsl) < 4) {
     1509                $hsl[] = 0;
     1510            }
     1511            return $this->toRGB($hsl);
     1512
     1513        } elseif ($fname == 'rgb' || $fname == 'rgba') {
     1514            $components = array();
     1515            $i = 1;
     1516            foreach ($rawComponents as $c) {
     1517                $c = $this->reduce($c);
     1518                if ($i < 4) {
     1519                    if ($c[0] == "number" && $c[2] == "%") {
     1520                        $components[] = 255 * ($c[1] / 100);
     1521                    } else {
     1522                        $components[] = floatval($c[1]);
     1523                    }
     1524                } elseif ($i == 4) {
     1525                    if ($c[0] == "number" && $c[2] == "%") {
     1526                        $components[] = 1.0 * ($c[1] / 100);
     1527                    } else {
     1528                        $components[] = floatval($c[1]);
     1529                    }
     1530                } else break;
     1531
     1532                $i++;
     1533            }
     1534            while (count($components) < 3) {
     1535                $components[] = 0;
     1536            }
     1537            array_unshift($components, 'color');
     1538            return $this->fixColor($components);
     1539        }
     1540
     1541        return false;
     1542    }
     1543
     1544    protected function reduce($value, $forExpression = false) {
     1545        switch ($value[0]) {
     1546        case "interpolate":
     1547            $reduced = $this->reduce($value[1]);
     1548            $var = $this->compileValue($reduced);
     1549            $res = $this->reduce(array("variable", $this->vPrefix . $var));
     1550
     1551            if ($res[0] == "raw_color") {
     1552                $res = $this->coerceColor($res);
     1553            }
     1554
     1555            if (empty($value[2])) $res = $this->lib_e($res);
     1556
     1557            return $res;
     1558        case "variable":
     1559            $key = $value[1];
     1560            if (is_array($key)) {
     1561                $key = $this->reduce($key);
     1562                $key = $this->vPrefix . $this->compileValue($this->lib_e($key));
     1563            }
     1564
     1565            $seen =& $this->env->seenNames;
     1566
     1567            if (!empty($seen[$key])) {
     1568                $this->throwError("infinite loop detected: $key");
     1569            }
     1570
     1571            $seen[$key] = true;
     1572            $out = $this->reduce($this->get($key));
     1573            $seen[$key] = false;
     1574            return $out;
     1575        case "list":
     1576            foreach ($value[2] as &$item) {
     1577                $item = $this->reduce($item, $forExpression);
     1578            }
     1579            return $value;
     1580        case "expression":
     1581            return $this->evaluate($value);
     1582        case "string":
     1583            foreach ($value[2] as &$part) {
     1584                if (is_array($part)) {
     1585                    $strip = $part[0] == "variable";
     1586                    $part = $this->reduce($part);
     1587                    if ($strip) $part = $this->lib_e($part);
     1588                }
     1589            }
     1590            return $value;
     1591        case "escape":
     1592            list(,$inner) = $value;
     1593            return $this->lib_e($this->reduce($inner));
     1594        case "function":
     1595            $color = $this->funcToColor($value);
     1596            if ($color) return $color;
     1597
     1598            list(, $name, $args) = $value;
     1599            if ($name == "%") $name = "_sprintf";
     1600
     1601            $f = isset($this->libFunctions[$name]) ?
     1602                $this->libFunctions[$name] : array($this, 'lib_'.str_replace('-', '_', $name));
     1603
     1604            if (is_callable($f)) {
     1605                if ($args[0] == 'list')
     1606                    $args = self::compressList($args[2], $args[1]);
     1607
     1608                $ret = call_user_func($f, $this->reduce($args, true), $this);
     1609
     1610                if (is_null($ret)) {
     1611                    return array("string", "", array(
     1612                        $name, "(", $args, ")"
     1613                    ));
     1614                }
     1615
     1616                // convert to a typed value if the result is a php primitive
     1617                if (is_numeric($ret)) {
     1618                    $ret = array('number', $ret, "");
     1619                } elseif (!is_array($ret)) {
     1620                    $ret = array('keyword', $ret);
     1621                }
     1622
     1623                return $ret;
     1624            }
     1625
     1626            // plain function, reduce args
     1627            $value[2] = $this->reduce($value[2]);
     1628            return $value;
     1629        case "unary":
     1630            list(, $op, $exp) = $value;
     1631            $exp = $this->reduce($exp);
     1632
     1633            if ($exp[0] == "number") {
     1634                switch ($op) {
     1635                case "+":
     1636                    return $exp;
     1637                case "-":
     1638                    $exp[1] *= -1;
     1639                    return $exp;
     1640                }
     1641            }
     1642            return array("string", "", array($op, $exp));
     1643        }
     1644
     1645        if ($forExpression) {
     1646            switch ($value[0]) {
     1647            case "keyword":
     1648                if ($color = $this->coerceColor($value)) {
     1649                    return $color;
     1650                }
     1651                break;
     1652            case "raw_color":
     1653                return $this->coerceColor($value);
     1654            }
     1655        }
     1656
     1657        return $value;
     1658    }
     1659
     1660
     1661    // coerce a value for use in color operation
     1662    protected function coerceColor($value) {
     1663        switch ($value[0]) {
     1664            case 'color': return $value;
     1665            case 'raw_color':
     1666                $c = array("color", 0, 0, 0);
     1667                $colorStr = substr($value[1], 1);
     1668                $num = hexdec($colorStr);
     1669                $width = strlen($colorStr) == 3 ? 16 : 256;
     1670
     1671                for ($i = 3; $i > 0; $i--) { // 3 2 1
     1672                    $t = $num % $width;
     1673                    $num /= $width;
     1674
     1675                    $c[$i] = $t * (256/$width) + $t * floor(16/$width);
     1676                }
     1677
     1678                return $c;
     1679            case 'keyword':
     1680                $name = $value[1];
     1681                if (isset(self::$cssColors[$name])) {
     1682                    $rgba = explode(',', self::$cssColors[$name]);
     1683
     1684                    if (isset($rgba[3])) {
     1685                        return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]);
     1686                    }
     1687                    return array('color', $rgba[0], $rgba[1], $rgba[2]);
     1688                }
     1689                return null;
     1690        }
     1691    }
     1692
     1693    // make something string like into a string
     1694    protected function coerceString($value) {
     1695        switch ($value[0]) {
     1696        case "string":
     1697            return $value;
     1698        case "keyword":
     1699            return array("string", "", array($value[1]));
     1700        }
     1701        return null;
     1702    }
     1703
     1704    // turn list of length 1 into value type
     1705    protected function flattenList($value) {
     1706        if ($value[0] == "list" && count($value[2]) == 1) {
     1707            return $this->flattenList($value[2][0]);
     1708        }
     1709        return $value;
     1710    }
     1711
     1712    public function toBool($a) {
     1713        return $a ? self::$TRUE : self::$FALSE;
     1714    }
     1715
     1716    // evaluate an expression
     1717    protected function evaluate($exp) {
     1718        list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
     1719
     1720        $left = $this->reduce($left, true);
     1721        $right = $this->reduce($right, true);
     1722
     1723        if ($leftColor = $this->coerceColor($left)) {
     1724            $left = $leftColor;
     1725        }
     1726
     1727        if ($rightColor = $this->coerceColor($right)) {
     1728            $right = $rightColor;
     1729        }
     1730
     1731        $ltype = $left[0];
     1732        $rtype = $right[0];
     1733
     1734        // operators that work on all types
     1735        if ($op == "and") {
     1736            return $this->toBool($left == self::$TRUE && $right == self::$TRUE);
     1737        }
     1738
     1739        if ($op == "=") {
     1740            return $this->toBool($this->eq($left, $right) );
     1741        }
     1742
     1743        if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right))) {
     1744            return $str;
     1745        }
     1746
     1747        // type based operators
     1748        $fname = "op_${ltype}_${rtype}";
     1749        if (is_callable(array($this, $fname))) {
     1750            $out = $this->$fname($op, $left, $right);
     1751            if (!is_null($out)) return $out;
     1752        }
     1753
     1754        // make the expression look it did before being parsed
     1755        $paddedOp = $op;
     1756        if ($whiteBefore) {
     1757            $paddedOp = " " . $paddedOp;
     1758        }
     1759        if ($whiteAfter) {
     1760            $paddedOp .= " ";
     1761        }
     1762
     1763        return array("string", "", array($left, $paddedOp, $right));
     1764    }
     1765
     1766    protected function stringConcatenate($left, $right) {
     1767        if ($strLeft = $this->coerceString($left)) {
     1768            if ($right[0] == "string") {
     1769                $right[1] = "";
     1770            }
     1771            $strLeft[2][] = $right;
     1772            return $strLeft;
     1773        }
     1774
     1775        if ($strRight = $this->coerceString($right)) {
     1776            array_unshift($strRight[2], $left);
     1777            return $strRight;
     1778        }
     1779    }
     1780
     1781
     1782    // make sure a color's components don't go out of bounds
     1783    protected function fixColor($c) {
     1784        foreach (range(1, 3) as $i) {
     1785            if ($c[$i] < 0) $c[$i] = 0;
     1786            if ($c[$i] > 255) $c[$i] = 255;
     1787        }
     1788
     1789        return $c;
     1790    }
     1791
     1792    protected function op_number_color($op, $lft, $rgt) {
     1793        if ($op == '+' || $op == '*') {
     1794            return $this->op_color_number($op, $rgt, $lft);
     1795        }
     1796    }
     1797
     1798    protected function op_color_number($op, $lft, $rgt) {
     1799        if ($rgt[0] == '%') $rgt[1] /= 100;
     1800
     1801        return $this->op_color_color($op, $lft,
     1802            array_fill(1, count($lft) - 1, $rgt[1]));
     1803    }
     1804
     1805    protected function op_color_color($op, $left, $right) {
     1806        $out = array('color');
     1807        $max = count($left) > count($right) ? count($left) : count($right);
     1808        foreach (range(1, $max - 1) as $i) {
     1809            $lval = isset($left[$i]) ? $left[$i] : 0;
     1810            $rval = isset($right[$i]) ? $right[$i] : 0;
     1811            switch ($op) {
     1812            case '+':
     1813                $out[] = $lval + $rval;
     1814                break;
     1815            case '-':
     1816                $out[] = $lval - $rval;
     1817                break;
     1818            case '*':
     1819                $out[] = $lval * $rval;
     1820                break;
     1821            case '%':
     1822                $out[] = $lval % $rval;
     1823                break;
     1824            case '/':
     1825                if ($rval == 0) {
     1826                    $this->throwError("evaluate error: can't divide by zero");
     1827                }
     1828                $out[] = $lval / $rval;
     1829                break;
     1830            default:
     1831                $this->throwError('evaluate error: color op number failed on op '.$op);
     1832            }
     1833        }
     1834        return $this->fixColor($out);
     1835    }
     1836
     1837    public function lib_red($color){
     1838        $color = $this->coerceColor($color);
     1839        if (is_null($color)) {
     1840            $this->throwError('color expected for red()');
     1841        }
     1842
     1843        return $color[1];
     1844    }
     1845
     1846    public function lib_green($color){
     1847        $color = $this->coerceColor($color);
     1848        if (is_null($color)) {
     1849            $this->throwError('color expected for green()');
     1850        }
     1851
     1852        return $color[2];
     1853    }
     1854
     1855    public function lib_blue($color){
     1856        $color = $this->coerceColor($color);
     1857        if (is_null($color)) {
     1858            $this->throwError('color expected for blue()');
     1859        }
     1860
     1861        return $color[3];
     1862    }
     1863
     1864
     1865    // operator on two numbers
     1866    protected function op_number_number($op, $left, $right) {
     1867        $unit = empty($left[2]) ? $right[2] : $left[2];
     1868
     1869        $value = 0;
     1870        switch ($op) {
     1871        case '+':
     1872            $value = $left[1] + $right[1];
     1873            break;
     1874        case '*':
     1875            $value = $left[1] * $right[1];
     1876            break;
     1877        case '-':
     1878            $value = $left[1] - $right[1];
     1879            break;
     1880        case '%':
     1881            $value = $left[1] % $right[1];
     1882            break;
     1883        case '/':
     1884            if ($right[1] == 0) $this->throwError('parse error: divide by zero');
     1885            $value = $left[1] / $right[1];
     1886            break;
     1887        case '<':
     1888            return $this->toBool($left[1] < $right[1]);
     1889        case '>':
     1890            return $this->toBool($left[1] > $right[1]);
     1891        case '>=':
     1892            return $this->toBool($left[1] >= $right[1]);
     1893        case '=<':
     1894            return $this->toBool($left[1] <= $right[1]);
     1895        default:
     1896            $this->throwError('parse error: unknown number operator: '.$op);
     1897        }
     1898
     1899        return array("number", $value, $unit);
     1900    }
     1901
     1902
     1903    /* environment functions */
     1904
     1905    protected function makeOutputBlock($type, $selectors = null) {
     1906        $b = new stdclass;
     1907        $b->lines = array();
     1908        $b->children = array();
     1909        $b->selectors = $selectors;
     1910        $b->type = $type;
     1911        $b->parent = $this->scope;
     1912        return $b;
     1913    }
     1914
     1915    // the state of execution
     1916    protected function pushEnv($block = null) {
     1917        $e = new stdclass;
     1918        $e->parent = $this->env;
     1919        $e->store = array();
     1920        $e->block = $block;
     1921
     1922        $this->env = $e;
     1923        return $e;
     1924    }
     1925
     1926    // pop something off the stack
     1927    protected function popEnv() {
     1928        $old = $this->env;
     1929        $this->env = $this->env->parent;
     1930        return $old;
     1931    }
     1932
     1933    // set something in the current env
     1934    protected function set($name, $value) {
     1935        $this->env->store[$name] = $value;
     1936    }
     1937
     1938
     1939    // get the highest occurrence entry for a name
     1940    protected function get($name) {
     1941        $current = $this->env;
     1942
     1943        $isArguments = $name == $this->vPrefix . 'arguments';
     1944        while ($current) {
     1945            if ($isArguments && isset($current->arguments)) {
     1946                return array('list', ' ', $current->arguments);
     1947            }
     1948
     1949            if (isset($current->store[$name])) {
     1950                return $current->store[$name];
     1951            }
     1952
     1953            $current = isset($current->storeParent) ?
     1954                $current->storeParent :
     1955                $current->parent;
     1956        }
     1957
     1958        $this->throwError("variable $name is undefined");
     1959    }
     1960
     1961    // inject array of unparsed strings into environment as variables
     1962    protected function injectVariables($args) {
     1963        $this->pushEnv();
     1964        $parser = new seed_wnb_lessc_parser($this, __METHOD__);
     1965        foreach ($args as $name => $strValue) {
     1966            if ($name{0} !== '@') {
     1967                $name = '@' . $name;
     1968            }
     1969            $parser->count = 0;
     1970            $parser->buffer = (string)$strValue;
     1971            if (!$parser->propertyValue($value)) {
     1972                throw new Exception("failed to parse passed in variable $name: $strValue");
     1973            }
     1974
     1975            $this->set($name, $value);
     1976        }
     1977    }
     1978
     1979    /**
     1980     * Initialize any static state, can initialize parser for a file
     1981     * $opts isn't used yet
     1982     */
     1983    public function __construct($fname = null) {
     1984        if ($fname !== null) {
     1985            // used for deprecated parse method
     1986            $this->_parseFile = $fname;
     1987        }
     1988    }
     1989
     1990    public function compile($string, $name = null) {
     1991        $locale = setlocale(LC_NUMERIC, 0);
     1992        setlocale(LC_NUMERIC, "C");
     1993
     1994        $this->parser = $this->makeParser($name);
     1995        $root = $this->parser->parse($string);
     1996
     1997        $this->env = null;
     1998        $this->scope = null;
     1999
     2000        $this->formatter = $this->newFormatter();
     2001
     2002        if (!empty($this->registeredVars)) {
     2003            $this->injectVariables($this->registeredVars);
     2004        }
     2005
     2006        $this->sourceParser = $this->parser; // used for error messages
     2007        $this->compileBlock($root);
     2008
     2009        ob_start();
     2010        $this->formatter->block($this->scope);
     2011        $out = ob_get_clean();
     2012        setlocale(LC_NUMERIC, $locale);
     2013        return $out;
     2014    }
     2015
     2016    public function compileFile($fname, $outFname = null) {
     2017        if (!is_readable($fname)) {
     2018            throw new Exception('load error: failed to find '.$fname);
     2019        }
     2020
     2021        $pi = pathinfo($fname);
     2022
     2023        $oldImport = $this->importDir;
     2024
     2025        $this->importDir = (array)$this->importDir;
     2026        $this->importDir[] = $pi['dirname'].'/';
     2027
     2028        $this->addParsedFile($fname);
     2029
     2030        $out = $this->compile(file_get_contents($fname), $fname);
     2031
     2032        $this->importDir = $oldImport;
     2033
     2034        if ($outFname !== null) {
     2035            return file_put_contents($outFname, $out);
     2036        }
     2037
     2038        return $out;
     2039    }
     2040
     2041    // compile only if changed input has changed or output doesn't exist
     2042    public function checkedCompile($in, $out) {
     2043        if (!is_file($out) || filemtime($in) > filemtime($out)) {
     2044            $this->compileFile($in, $out);
     2045            return true;
     2046        }
     2047        return false;
     2048    }
     2049
     2050    /**
     2051     * Execute lessphp on a .less file or a lessphp cache structure
     2052     *
     2053     * The lessphp cache structure contains information about a specific
     2054     * less file having been parsed. It can be used as a hint for future
     2055     * calls to determine whether or not a rebuild is required.
     2056     *
     2057     * The cache structure contains two important keys that may be used
     2058     * externally:
     2059     *
     2060     * compiled: The final compiled CSS
     2061     * updated: The time (in seconds) the CSS was last compiled
     2062     *
     2063     * The cache structure is a plain-ol' PHP associative array and can
     2064     * be serialized and unserialized without a hitch.
     2065     *
     2066     * @param mixed $in Input
     2067     * @param bool $force Force rebuild?
     2068     * @return array lessphp cache structure
     2069     */
     2070    public function cachedCompile($in, $force = false) {
     2071        // assume no root
     2072        $root = null;
     2073
     2074        if (is_string($in)) {
     2075            $root = $in;
     2076        } elseif (is_array($in) && isset($in['root'])) {
     2077            if ($force || !isset($in['files'])) {
     2078                // If we are forcing a recompile or if for some reason the
     2079                // structure does not contain any file information we should
     2080                // specify the root to trigger a rebuild.
     2081                $root = $in['root'];
     2082            } elseif (isset($in['files']) && is_array($in['files'])) {
     2083                foreach ($in['files'] as $fname => $ftime) {
     2084                    if (!file_exists($fname) || filemtime($fname) > $ftime) {
     2085                        // One of the files we knew about previously has changed
     2086                        // so we should look at our incoming root again.
     2087                        $root = $in['root'];
     2088                        break;
     2089                    }
     2090                }
     2091            }
     2092        } else {
     2093            // TODO: Throw an exception? We got neither a string nor something
     2094            // that looks like a compatible lessphp cache structure.
     2095            return null;
     2096        }
     2097
     2098        if ($root !== null) {
     2099            // If we have a root value which means we should rebuild.
     2100            $out = array();
     2101            $out['root'] = $root;
     2102            $out['compiled'] = $this->compileFile($root);
     2103            $out['files'] = $this->allParsedFiles();
     2104            $out['updated'] = time();
     2105            return $out;
     2106        } else {
     2107            // No changes, pass back the structure
     2108            // we were given initially.
     2109            return $in;
     2110        }
     2111
     2112    }
     2113
     2114    // parse and compile buffer
     2115    // This is deprecated
     2116    public function parse($str = null, $initialVariables = null) {
     2117        if (is_array($str)) {
     2118            $initialVariables = $str;
     2119            $str = null;
     2120        }
     2121
     2122        $oldVars = $this->registeredVars;
     2123        if ($initialVariables !== null) {
     2124            $this->setVariables($initialVariables);
     2125        }
     2126
     2127        if ($str == null) {
     2128            if (empty($this->_parseFile)) {
     2129                throw new exception("nothing to parse");
     2130            }
     2131
     2132            $out = $this->compileFile($this->_parseFile);
     2133        } else {
     2134            $out = $this->compile($str);
     2135        }
     2136
     2137        $this->registeredVars = $oldVars;
     2138        return $out;
     2139    }
     2140
     2141    protected function makeParser($name) {
     2142        $parser = new seed_wnb_lessc_parser($this, $name);
     2143        $parser->writeComments = $this->preserveComments;
     2144
     2145        return $parser;
     2146    }
     2147
     2148    public function setFormatter($name) {
     2149        $this->formatterName = $name;
     2150    }
     2151
     2152    protected function newFormatter() {
     2153        $className = "seed_wnb_lessc_formatter_lessjs";
     2154        if (!empty($this->formatterName)) {
     2155            if (!is_string($this->formatterName))
     2156                return $this->formatterName;
     2157            $className = "seed_wnb_lessc_formatter_$this->formatterName";
     2158        }
     2159
     2160        return new $className;
     2161    }
     2162
     2163    public function setPreserveComments($preserve) {
     2164        $this->preserveComments = $preserve;
     2165    }
     2166
     2167    public function registerFunction($name, $func) {
     2168        $this->libFunctions[$name] = $func;
     2169    }
     2170
     2171    public function unregisterFunction($name) {
     2172        unset($this->libFunctions[$name]);
     2173    }
     2174
     2175    public function setVariables($variables) {
     2176        $this->registeredVars = array_merge($this->registeredVars, $variables);
     2177    }
     2178
     2179    public function unsetVariable($name) {
     2180        unset($this->registeredVars[$name]);
     2181    }
     2182
     2183    public function setImportDir($dirs) {
     2184        $this->importDir = (array)$dirs;
     2185    }
     2186
     2187    public function addImportDir($dir) {
     2188        $this->importDir = (array)$this->importDir;
     2189        $this->importDir[] = $dir;
     2190    }
     2191
     2192    public function allParsedFiles() {
     2193        return $this->allParsedFiles;
     2194    }
     2195
     2196    public function addParsedFile($file) {
     2197        $this->allParsedFiles[realpath($file)] = filemtime($file);
     2198    }
     2199
     2200    /**
     2201     * Uses the current value of $this->count to show line and line number
     2202     */
     2203    public function throwError($msg = null) {
     2204        if ($this->sourceLoc >= 0) {
     2205            $this->sourceParser->throwError($msg, $this->sourceLoc);
     2206        }
     2207        throw new exception($msg);
     2208    }
     2209
     2210    // compile file $in to file $out if $in is newer than $out
     2211    // returns true when it compiles, false otherwise
     2212    public static function ccompile($in, $out, $less = null) {
     2213        if ($less === null) {
     2214            $less = new self;
     2215        }
     2216        return $less->checkedCompile($in, $out);
     2217    }
     2218
     2219    public static function cexecute($in, $force = false, $less = null) {
     2220        if ($less === null) {
     2221            $less = new self;
     2222        }
     2223        return $less->cachedCompile($in, $force);
     2224    }
     2225
     2226    static protected $cssColors = array(
     2227        'aliceblue' => '240,248,255',
     2228        'antiquewhite' => '250,235,215',
     2229        'aqua' => '0,255,255',
     2230        'aquamarine' => '127,255,212',
     2231        'azure' => '240,255,255',
     2232        'beige' => '245,245,220',
     2233        'bisque' => '255,228,196',
     2234        'black' => '0,0,0',
     2235        'blanchedalmond' => '255,235,205',
     2236        'blue' => '0,0,255',
     2237        'blueviolet' => '138,43,226',
     2238        'brown' => '165,42,42',
     2239        'burlywood' => '222,184,135',
     2240        'cadetblue' => '95,158,160',
     2241        'chartreuse' => '127,255,0',
     2242        'chocolate' => '210,105,30',
     2243        'coral' => '255,127,80',
     2244        'cornflowerblue' => '100,149,237',
     2245        'cornsilk' => '255,248,220',
     2246        'crimson' => '220,20,60',
     2247        'cyan' => '0,255,255',
     2248        'darkblue' => '0,0,139',
     2249        'darkcyan' => '0,139,139',
     2250        'darkgoldenrod' => '184,134,11',
     2251        'darkgray' => '169,169,169',
     2252        'darkgreen' => '0,100,0',
     2253        'darkgrey' => '169,169,169',
     2254        'darkkhaki' => '189,183,107',
     2255        'darkmagenta' => '139,0,139',
     2256        'darkolivegreen' => '85,107,47',
     2257        'darkorange' => '255,140,0',
     2258        'darkorchid' => '153,50,204',
     2259        'darkred' => '139,0,0',
     2260        'darksalmon' => '233,150,122',
     2261        'darkseagreen' => '143,188,143',
     2262        'darkslateblue' => '72,61,139',
     2263        'darkslategray' => '47,79,79',
     2264        'darkslategrey' => '47,79,79',
     2265        'darkturquoise' => '0,206,209',
     2266        'darkviolet' => '148,0,211',
     2267        'deeppink' => '255,20,147',
     2268        'deepskyblue' => '0,191,255',
     2269        'dimgray' => '105,105,105',
     2270        'dimgrey' => '105,105,105',
     2271        'dodgerblue' => '30,144,255',
     2272        'firebrick' => '178,34,34',
     2273        'floralwhite' => '255,250,240',
     2274        'forestgreen' => '34,139,34',
     2275        'fuchsia' => '255,0,255',
     2276        'gainsboro' => '220,220,220',
     2277        'ghostwhite' => '248,248,255',
     2278        'gold' => '255,215,0',
     2279        'goldenrod' => '218,165,32',
     2280        'gray' => '128,128,128',
     2281        'green' => '0,128,0',
     2282        'greenyellow' => '173,255,47',
     2283        'grey' => '128,128,128',
     2284        'honeydew' => '240,255,240',
     2285        'hotpink' => '255,105,180',
     2286        'indianred' => '205,92,92',
     2287        'indigo' => '75,0,130',
     2288        'ivory' => '255,255,240',
     2289        'khaki' => '240,230,140',
     2290        'lavender' => '230,230,250',
     2291        'lavenderblush' => '255,240,245',
     2292        'lawngreen' => '124,252,0',
     2293        'lemonchiffon' => '255,250,205',
     2294        'lightblue' => '173,216,230',
     2295        'lightcoral' => '240,128,128',
     2296        'lightcyan' => '224,255,255',
     2297        'lightgoldenrodyellow' => '250,250,210',
     2298        'lightgray' => '211,211,211',
     2299        'lightgreen' => '144,238,144',
     2300        'lightgrey' => '211,211,211',
     2301        'lightpink' => '255,182,193',
     2302        'lightsalmon' => '255,160,122',
     2303        'lightseagreen' => '32,178,170',
     2304        'lightskyblue' => '135,206,250',
     2305        'lightslategray' => '119,136,153',
     2306        'lightslategrey' => '119,136,153',
     2307        'lightsteelblue' => '176,196,222',
     2308        'lightyellow' => '255,255,224',
     2309        'lime' => '0,255,0',
     2310        'limegreen' => '50,205,50',
     2311        'linen' => '250,240,230',
     2312        'magenta' => '255,0,255',
     2313        'maroon' => '128,0,0',
     2314        'mediumaquamarine' => '102,205,170',
     2315        'mediumblue' => '0,0,205',
     2316        'mediumorchid' => '186,85,211',
     2317        'mediumpurple' => '147,112,219',
     2318        'mediumseagreen' => '60,179,113',
     2319        'mediumslateblue' => '123,104,238',
     2320        'mediumspringgreen' => '0,250,154',
     2321        'mediumturquoise' => '72,209,204',
     2322        'mediumvioletred' => '199,21,133',
     2323        'midnightblue' => '25,25,112',
     2324        'mintcream' => '245,255,250',
     2325        'mistyrose' => '255,228,225',
     2326        'moccasin' => '255,228,181',
     2327        'navajowhite' => '255,222,173',
     2328        'navy' => '0,0,128',
     2329        'oldlace' => '253,245,230',
     2330        'olive' => '128,128,0',
     2331        'olivedrab' => '107,142,35',
     2332        'orange' => '255,165,0',
     2333        'orangered' => '255,69,0',
     2334        'orchid' => '218,112,214',
     2335        'palegoldenrod' => '238,232,170',
     2336        'palegreen' => '152,251,152',
     2337        'paleturquoise' => '175,238,238',
     2338        'palevioletred' => '219,112,147',
     2339        'papayawhip' => '255,239,213',
     2340        'peachpuff' => '255,218,185',
     2341        'peru' => '205,133,63',
     2342        'pink' => '255,192,203',
     2343        'plum' => '221,160,221',
     2344        'powderblue' => '176,224,230',
     2345        'purple' => '128,0,128',
     2346        'red' => '255,0,0',
     2347        'rosybrown' => '188,143,143',
     2348        'royalblue' => '65,105,225',
     2349        'saddlebrown' => '139,69,19',
     2350        'salmon' => '250,128,114',
     2351        'sandybrown' => '244,164,96',
     2352        'seagreen' => '46,139,87',
     2353        'seashell' => '255,245,238',
     2354        'sienna' => '160,82,45',
     2355        'silver' => '192,192,192',
     2356        'skyblue' => '135,206,235',
     2357        'slateblue' => '106,90,205',
     2358        'slategray' => '112,128,144',
     2359        'slategrey' => '112,128,144',
     2360        'snow' => '255,250,250',
     2361        'springgreen' => '0,255,127',
     2362        'steelblue' => '70,130,180',
     2363        'tan' => '210,180,140',
     2364        'teal' => '0,128,128',
     2365        'thistle' => '216,191,216',
     2366        'tomato' => '255,99,71',
     2367        'transparent' => '0,0,0,0',
     2368        'turquoise' => '64,224,208',
     2369        'violet' => '238,130,238',
     2370        'wheat' => '245,222,179',
     2371        'white' => '255,255,255',
     2372        'whitesmoke' => '245,245,245',
     2373        'yellow' => '255,255,0',
     2374        'yellowgreen' => '154,205,50'
     2375    );
    26892376}
    26902377
     2378// responsible for taking a string of LESS code and converting it into a
     2379// syntax tree
     2380class seed_wnb_lessc_parser {
     2381    static protected $nextBlockId = 0; // used to uniquely identify blocks
     2382
     2383    static protected $precedence = array(
     2384        '=<' => 0,
     2385        '>=' => 0,
     2386        '=' => 0,
     2387        '<' => 0,
     2388        '>' => 0,
     2389
     2390        '+' => 1,
     2391        '-' => 1,
     2392        '*' => 2,
     2393        '/' => 2,
     2394        '%' => 2,
     2395    );
     2396
     2397    static protected $whitePattern;
     2398    static protected $commentMulti;
     2399
     2400    static protected $commentSingle = "//";
     2401    static protected $commentMultiLeft = "/*";
     2402    static protected $commentMultiRight = "*/";
     2403
     2404    // regex string to match any of the operators
     2405    static protected $operatorString;
     2406
     2407    // these properties will supress division unless it's inside parenthases
     2408    static protected $supressDivisionProps =
     2409        array('/border-radius$/i', '/^font$/i');
     2410
     2411    protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport");
     2412    protected $lineDirectives = array("charset");
     2413
     2414    /**
     2415     * if we are in parens we can be more liberal with whitespace around
     2416     * operators because it must evaluate to a single value and thus is less
     2417     * ambiguous.
     2418     *
     2419     * Consider:
     2420     *     property1: 10 -5; // is two numbers, 10 and -5
     2421     *     property2: (10 -5); // should evaluate to 5
     2422     */
     2423    protected $inParens = false;
     2424
     2425    // caches preg escaped literals
     2426    static protected $literalCache = array();
     2427
     2428    public function __construct($seed_wnb_lessc, $sourceName = null) {
     2429        $this->eatWhiteDefault = true;
     2430        // reference to less needed for vPrefix, mPrefix, and parentSelector
     2431        $this->seed_wnb_lessc = $seed_wnb_lessc;
     2432
     2433        $this->sourceName = $sourceName; // name used for error messages
     2434
     2435        $this->writeComments = false;
     2436
     2437        if (!self::$operatorString) {
     2438            self::$operatorString =
     2439                '('.implode('|', array_map(array('seed_wnb_lessc', 'preg_quote'),
     2440                    array_keys(self::$precedence))).')';
     2441
     2442            $commentSingle = seed_wnb_lessc::preg_quote(self::$commentSingle);
     2443            $commentMultiLeft = seed_wnb_lessc::preg_quote(self::$commentMultiLeft);
     2444            $commentMultiRight = seed_wnb_lessc::preg_quote(self::$commentMultiRight);
     2445
     2446            self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight;
     2447            self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais';
     2448        }
     2449    }
     2450
     2451    public function parse($buffer) {
     2452        $this->count = 0;
     2453        $this->line = 1;
     2454
     2455        $this->env = null; // block stack
     2456        $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer);
     2457        $this->pushSpecialBlock("root");
     2458        $this->eatWhiteDefault = true;
     2459        $this->seenComments = array();
     2460
     2461        // trim whitespace on head
     2462        // if (preg_match('/^\s+/', $this->buffer, $m)) {
     2463        //  $this->line += substr_count($m[0], "\n");
     2464        //  $this->buffer = ltrim($this->buffer);
     2465        // }
     2466        $this->whitespace();
     2467
     2468        // parse the entire file
     2469        while (false !== $this->parseChunk());
     2470
     2471        if ($this->count != strlen($this->buffer))
     2472            $this->throwError();
     2473
     2474        // TODO report where the block was opened
     2475        if ( !property_exists($this->env, 'parent') || !is_null($this->env->parent) )
     2476            throw new exception('parse error: unclosed block');
     2477
     2478        return $this->env;
     2479    }
     2480
     2481    /**
     2482     * Parse a single chunk off the head of the buffer and append it to the
     2483     * current parse environment.
     2484     * Returns false when the buffer is empty, or when there is an error.
     2485     *
     2486     * This function is called repeatedly until the entire document is
     2487     * parsed.
     2488     *
     2489     * This parser is most similar to a recursive descent parser. Single
     2490     * functions represent discrete grammatical rules for the language, and
     2491     * they are able to capture the text that represents those rules.
     2492     *
     2493     * Consider the function seed_wnb_lessc::keyword(). (all parse functions are
     2494     * structured the same)
     2495     *
     2496     * The function takes a single reference argument. When calling the
     2497     * function it will attempt to match a keyword on the head of the buffer.
     2498     * If it is successful, it will place the keyword in the referenced
     2499     * argument, advance the position in the buffer, and return true. If it
     2500     * fails then it won't advance the buffer and it will return false.
     2501     *
     2502     * All of these parse functions are powered by seed_wnb_lessc::match(), which behaves
     2503     * the same way, but takes a literal regular expression. Sometimes it is
     2504     * more convenient to use match instead of creating a new function.
     2505     *
     2506     * Because of the format of the functions, to parse an entire string of
     2507     * grammatical rules, you can chain them together using &&.
     2508     *
     2509     * But, if some of the rules in the chain succeed before one fails, then
     2510     * the buffer position will be left at an invalid state. In order to
     2511     * avoid this, seed_wnb_lessc::seek() is used to remember and set buffer positions.
     2512     *
     2513     * Before parsing a chain, use $s = $this->seek() to remember the current
     2514     * position into $s. Then if a chain fails, use $this->seek($s) to
     2515     * go back where we started.
     2516     */
     2517    protected function parseChunk() {
     2518        if (empty($this->buffer)) return false;
     2519        $s = $this->seek();
     2520
     2521        if ($this->whitespace()) {
     2522            return true;
     2523        }
     2524
     2525        // setting a property
     2526        if ($this->keyword($key) && $this->assign() &&
     2527            $this->propertyValue($value, $key) && $this->end()
     2528        ) {
     2529            $this->append(array('assign', $key, $value), $s);
     2530            return true;
     2531        } else {
     2532            $this->seek($s);
     2533        }
     2534
     2535
     2536        // look for special css blocks
     2537        if ($this->literal('@', false)) {
     2538            $this->count--;
     2539
     2540            // media
     2541            if ($this->literal('@media')) {
     2542                if (($this->mediaQueryList($mediaQueries) || true)
     2543                    && $this->literal('{')
     2544                ) {
     2545                    $media = $this->pushSpecialBlock("media");
     2546                    $media->queries = is_null($mediaQueries) ? array() : $mediaQueries;
     2547                    return true;
     2548                } else {
     2549                    $this->seek($s);
     2550                    return false;
     2551                }
     2552            }
     2553
     2554            if ($this->literal("@", false) && $this->keyword($dirName)) {
     2555                if ($this->isDirective($dirName, $this->blockDirectives)) {
     2556                    if (($this->openString("{", $dirValue, null, array(";")) || true) &&
     2557                        $this->literal("{")
     2558                    ) {
     2559                        $dir = $this->pushSpecialBlock("directive");
     2560                        $dir->name = $dirName;
     2561                        if (isset($dirValue)) $dir->value = $dirValue;
     2562                        return true;
     2563                    }
     2564                } elseif ($this->isDirective($dirName, $this->lineDirectives)) {
     2565                    if ($this->propertyValue($dirValue) && $this->end()) {
     2566                        $this->append(array("directive", $dirName, $dirValue));
     2567                        return true;
     2568                    }
     2569                }
     2570            }
     2571
     2572            $this->seek($s);
     2573        }
     2574
     2575        // setting a variable
     2576        if ($this->variable($var) && $this->assign() &&
     2577            $this->propertyValue($value) && $this->end()
     2578        ) {
     2579            $this->append(array('assign', $var, $value), $s);
     2580            return true;
     2581        } else {
     2582            $this->seek($s);
     2583        }
     2584
     2585        if ($this->import($importValue)) {
     2586            $this->append($importValue, $s);
     2587            return true;
     2588        }
     2589
     2590        // opening parametric mixin
     2591        if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) &&
     2592            ($this->guards($guards) || true) &&
     2593            $this->literal('{')
     2594        ) {
     2595            $block = $this->pushBlock($this->fixTags(array($tag)));
     2596            $block->args = $args;
     2597            $block->isVararg = $isVararg;
     2598            if (!empty($guards)) $block->guards = $guards;
     2599            return true;
     2600        } else {
     2601            $this->seek($s);
     2602        }
     2603
     2604        // opening a simple block
     2605        if ($this->tags($tags) && $this->literal('{', false)) {
     2606            $tags = $this->fixTags($tags);
     2607            $this->pushBlock($tags);
     2608            return true;
     2609        } else {
     2610            $this->seek($s);
     2611        }
     2612
     2613        // closing a block
     2614        if ($this->literal('}', false)) {
     2615            try {
     2616                $block = $this->pop();
     2617            } catch (exception $e) {
     2618                $this->seek($s);
     2619                $this->throwError($e->getMessage());
     2620            }
     2621
     2622            $hidden = false;
     2623            if (is_null($block->type)) {
     2624                $hidden = true;
     2625                if (!isset($block->args)) {
     2626                    foreach ($block->tags as $tag) {
     2627                        if (!is_string($tag) || $tag{0} != $this->seed_wnb_lessc->mPrefix) {
     2628                            $hidden = false;
     2629                            break;
     2630                        }
     2631                    }
     2632                }
     2633
     2634                foreach ($block->tags as $tag) {
     2635                    if (is_string($tag)) {
     2636                        $this->env->children[$tag][] = $block;
     2637                    }
     2638                }
     2639            }
     2640
     2641            if (!$hidden) {
     2642                $this->append(array('block', $block), $s);
     2643            }
     2644
     2645            // this is done here so comments aren't bundled into he block that
     2646            // was just closed
     2647            $this->whitespace();
     2648            return true;
     2649        }
     2650
     2651        // mixin
     2652        if ($this->mixinTags($tags) &&
     2653            ($this->argumentDef($argv, $isVararg) || true) &&
     2654            ($this->keyword($suffix) || true) && $this->end()
     2655        ) {
     2656            $tags = $this->fixTags($tags);
     2657            $this->append(array('mixin', $tags, $argv, $suffix), $s);
     2658            return true;
     2659        } else {
     2660            $this->seek($s);
     2661        }
     2662
     2663        // spare ;
     2664        if ($this->literal(';')) return true;
     2665
     2666        return false; // got nothing, throw error
     2667    }
     2668
     2669    protected function isDirective($dirname, $directives) {
     2670        // TODO: cache pattern in parser
     2671        $pattern = implode("|",
     2672            array_map(array("seed_wnb_lessc", "preg_quote"), $directives));
     2673        $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i';
     2674
     2675        return preg_match($pattern, $dirname);
     2676    }
     2677
     2678    protected function fixTags($tags) {
     2679        // move @ tags out of variable namespace
     2680        foreach ($tags as &$tag) {
     2681            if ($tag{0} == $this->seed_wnb_lessc->vPrefix)
     2682                $tag[0] = $this->seed_wnb_lessc->mPrefix;
     2683        }
     2684        return $tags;
     2685    }
     2686
     2687    // a list of expressions
     2688    protected function expressionList(&$exps) {
     2689        $values = array();
     2690
     2691        while ($this->expression($exp)) {
     2692            $values[] = $exp;
     2693        }
     2694
     2695        if (count($values) == 0) return false;
     2696
     2697        $exps = seed_wnb_lessc::compressList($values, ' ');
     2698        return true;
     2699    }
     2700
     2701    /**
     2702     * Attempt to consume an expression.
     2703     * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
     2704     */
     2705    protected function expression(&$out) {
     2706        if ($this->value($lhs)) {
     2707            $out = $this->expHelper($lhs, 0);
     2708
     2709            // look for / shorthand
     2710            if (!empty($this->env->supressedDivision)) {
     2711                unset($this->env->supressedDivision);
     2712                $s = $this->seek();
     2713                if ($this->literal("/") && $this->value($rhs)) {
     2714                    $out = array("list", "",
     2715                        array($out, array("keyword", "/"), $rhs));
     2716                } else {
     2717                    $this->seek($s);
     2718                }
     2719            }
     2720
     2721            return true;
     2722        }
     2723        return false;
     2724    }
     2725
     2726    /**
     2727     * recursively parse infix equation with $lhs at precedence $minP
     2728     */
     2729    protected function expHelper($lhs, $minP) {
     2730        $this->inExp = true;
     2731        $ss = $this->seek();
     2732
     2733        while (true) {
     2734            $whiteBefore = isset($this->buffer[$this->count - 1]) &&
     2735                ctype_space($this->buffer[$this->count - 1]);
     2736
     2737            // If there is whitespace before the operator, then we require
     2738            // whitespace after the operator for it to be an expression
     2739            $needWhite = $whiteBefore && !$this->inParens;
     2740
     2741            if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) {
     2742                if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) {
     2743                    foreach (self::$supressDivisionProps as $pattern) {
     2744                        if (preg_match($pattern, $this->env->currentProperty)) {
     2745                            $this->env->supressedDivision = true;
     2746                            break 2;
     2747                        }
     2748                    }
     2749                }
     2750
     2751
     2752                $whiteAfter = isset($this->buffer[$this->count - 1]) &&
     2753                    ctype_space($this->buffer[$this->count - 1]);
     2754
     2755                if (!$this->value($rhs)) break;
     2756
     2757                // peek for next operator to see what to do with rhs
     2758                if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) {
     2759                    $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
     2760                }
     2761
     2762                $lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter);
     2763                $ss = $this->seek();
     2764
     2765                continue;
     2766            }
     2767
     2768            break;
     2769        }
     2770
     2771        $this->seek($ss);
     2772
     2773        return $lhs;
     2774    }
     2775
     2776    // consume a list of values for a property
     2777    public function propertyValue(&$value, $keyName = null) {
     2778        $values = array();
     2779
     2780        if ($keyName !== null) $this->env->currentProperty = $keyName;
     2781
     2782        $s = null;
     2783        while ($this->expressionList($v)) {
     2784            $values[] = $v;
     2785            $s = $this->seek();
     2786            if (!$this->literal(',')) break;
     2787        }
     2788
     2789        if ($s) $this->seek($s);
     2790
     2791        if ($keyName !== null) unset($this->env->currentProperty);
     2792
     2793        if (count($values) == 0) return false;
     2794
     2795        $value = seed_wnb_lessc::compressList($values, ', ');
     2796        return true;
     2797    }
     2798
     2799    protected function parenValue(&$out) {
     2800        $s = $this->seek();
     2801
     2802        // speed shortcut
     2803        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") {
     2804            return false;
     2805        }
     2806
     2807        $inParens = $this->inParens;
     2808        if ($this->literal("(") &&
     2809            ($this->inParens = true) && $this->expression($exp) &&
     2810            $this->literal(")")
     2811        ) {
     2812            $out = $exp;
     2813            $this->inParens = $inParens;
     2814            return true;
     2815        } else {
     2816            $this->inParens = $inParens;
     2817            $this->seek($s);
     2818        }
     2819
     2820        return false;
     2821    }
     2822
     2823    // a single value
     2824    protected function value(&$value) {
     2825        $s = $this->seek();
     2826
     2827        // speed shortcut
     2828        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") {
     2829            // negation
     2830            if ($this->literal("-", false) &&
     2831                (($this->variable($inner) && $inner = array("variable", $inner)) ||
     2832                $this->unit($inner) ||
     2833                $this->parenValue($inner))
     2834            ) {
     2835                $value = array("unary", "-", $inner);
     2836                return true;
     2837            } else {
     2838                $this->seek($s);
     2839            }
     2840        }
     2841
     2842        if ($this->parenValue($value)) return true;
     2843        if ($this->unit($value)) return true;
     2844        if ($this->color($value)) return true;
     2845        if ($this->func($value)) return true;
     2846        if ($this->string($value)) return true;
     2847
     2848        if ($this->keyword($word)) {
     2849            $value = array('keyword', $word);
     2850            return true;
     2851        }
     2852
     2853        // try a variable
     2854        if ($this->variable($var)) {
     2855            $value = array('variable', $var);
     2856            return true;
     2857        }
     2858
     2859        // unquote string (should this work on any type?
     2860        if ($this->literal("~") && $this->string($str)) {
     2861            $value = array("escape", $str);
     2862            return true;
     2863        } else {
     2864            $this->seek($s);
     2865        }
     2866
     2867        // css hack: \0
     2868        if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
     2869            $value = array('keyword', '\\'.$m[1]);
     2870            return true;
     2871        } else {
     2872            $this->seek($s);
     2873        }
     2874
     2875        return false;
     2876    }
     2877
     2878    // an import statement
     2879    protected function import(&$out) {
     2880        if (!$this->literal('@import')) return false;
     2881
     2882        // @import "something.css" media;
     2883        // @import url("something.css") media;
     2884        // @import url(something.css) media;
     2885
     2886        if ($this->propertyValue($value)) {
     2887            $out = array("import", $value);
     2888            return true;
     2889        }
     2890    }
     2891
     2892    protected function mediaQueryList(&$out) {
     2893        if ($this->genericList($list, "mediaQuery", ",", false)) {
     2894            $out = $list[2];
     2895            return true;
     2896        }
     2897        return false;
     2898    }
     2899
     2900    protected function mediaQuery(&$out) {
     2901        $s = $this->seek();
     2902
     2903        $expressions = null;
     2904        $parts = array();
     2905
     2906        if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) {
     2907            $prop = array("mediaType");
     2908            if (isset($only)) $prop[] = "only";
     2909            if (isset($not)) $prop[] = "not";
     2910            $prop[] = $mediaType;
     2911            $parts[] = $prop;
     2912        } else {
     2913            $this->seek($s);
     2914        }
     2915
     2916
     2917        if (!empty($mediaType) && !$this->literal("and")) {
     2918            // ~
     2919        } else {
     2920            $this->genericList($expressions, "mediaExpression", "and", false);
     2921            if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]);
     2922        }
     2923
     2924        if (count($parts) == 0) {
     2925            $this->seek($s);
     2926            return false;
     2927        }
     2928
     2929        $out = $parts;
     2930        return true;
     2931    }
     2932
     2933    protected function mediaExpression(&$out) {
     2934        $s = $this->seek();
     2935        $value = null;
     2936        if ($this->literal("(") &&
     2937            $this->keyword($feature) &&
     2938            ($this->literal(":") && $this->expression($value) || true) &&
     2939            $this->literal(")")
     2940        ) {
     2941            $out = array("mediaExp", $feature);
     2942            if ($value) $out[] = $value;
     2943            return true;
     2944        } elseif ($this->variable($variable)) {
     2945            $out = array('variable', $variable);
     2946            return true;
     2947        }
     2948
     2949        $this->seek($s);
     2950        return false;
     2951    }
     2952
     2953    // an unbounded string stopped by $end
     2954    protected function openString($end, &$out, $nestingOpen=null, $rejectStrs = null) {
     2955        $oldWhite = $this->eatWhiteDefault;
     2956        $this->eatWhiteDefault = false;
     2957
     2958        $stop = array("'", '"', "@{", $end);
     2959        $stop = array_map(array("seed_wnb_lessc", "preg_quote"), $stop);
     2960        // $stop[] = self::$commentMulti;
     2961
     2962        if (!is_null($rejectStrs)) {
     2963            $stop = array_merge($stop, $rejectStrs);
     2964        }
     2965
     2966        $patt = '(.*?)('.implode("|", $stop).')';
     2967
     2968        $nestingLevel = 0;
     2969
     2970        $content = array();
     2971        while ($this->match($patt, $m, false)) {
     2972            if (!empty($m[1])) {
     2973                $content[] = $m[1];
     2974                if ($nestingOpen) {
     2975                    $nestingLevel += substr_count($m[1], $nestingOpen);
     2976                }
     2977            }
     2978
     2979            $tok = $m[2];
     2980
     2981            $this->count-= strlen($tok);
     2982            if ($tok == $end) {
     2983                if ($nestingLevel == 0) {
     2984                    break;
     2985                } else {
     2986                    $nestingLevel--;
     2987                }
     2988            }
     2989
     2990            if (($tok == "'" || $tok == '"') && $this->string($str)) {
     2991                $content[] = $str;
     2992                continue;
     2993            }
     2994
     2995            if ($tok == "@{" && $this->interpolation($inter)) {
     2996                $content[] = $inter;
     2997                continue;
     2998            }
     2999
     3000            if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) {
     3001                break;
     3002            }
     3003
     3004            $content[] = $tok;
     3005            $this->count+= strlen($tok);
     3006        }
     3007
     3008        $this->eatWhiteDefault = $oldWhite;
     3009
     3010        if (count($content) == 0) return false;
     3011
     3012        // trim the end
     3013        if (is_string(end($content))) {
     3014            $content[count($content) - 1] = rtrim(end($content));
     3015        }
     3016
     3017        $out = array("string", "", $content);
     3018        return true;
     3019    }
     3020
     3021    protected function string(&$out) {
     3022        $s = $this->seek();
     3023        if ($this->literal('"', false)) {
     3024            $delim = '"';
     3025        } elseif ($this->literal("'", false)) {
     3026            $delim = "'";
     3027        } else {
     3028            return false;
     3029        }
     3030
     3031        $content = array();
     3032
     3033        // look for either ending delim , escape, or string interpolation
     3034        $patt = '([^\n]*?)(@\{|\\\\|' .
     3035            seed_wnb_lessc::preg_quote($delim).')';
     3036
     3037        $oldWhite = $this->eatWhiteDefault;
     3038        $this->eatWhiteDefault = false;
     3039
     3040        while ($this->match($patt, $m, false)) {
     3041            $content[] = $m[1];
     3042            if ($m[2] == "@{") {
     3043                $this->count -= strlen($m[2]);
     3044                if ($this->interpolation($inter, false)) {
     3045                    $content[] = $inter;
     3046                } else {
     3047                    $this->count += strlen($m[2]);
     3048                    $content[] = "@{"; // ignore it
     3049                }
     3050            } elseif ($m[2] == '\\') {
     3051                $content[] = $m[2];
     3052                if ($this->literal($delim, false)) {
     3053                    $content[] = $delim;
     3054                }
     3055            } else {
     3056                $this->count -= strlen($delim);
     3057                break; // delim
     3058            }
     3059        }
     3060
     3061        $this->eatWhiteDefault = $oldWhite;
     3062
     3063        if ($this->literal($delim)) {
     3064            $out = array("string", $delim, $content);
     3065            return true;
     3066        }
     3067
     3068        $this->seek($s);
     3069        return false;
     3070    }
     3071
     3072    protected function interpolation(&$out) {
     3073        $oldWhite = $this->eatWhiteDefault;
     3074        $this->eatWhiteDefault = true;
     3075
     3076        $s = $this->seek();
     3077        if ($this->literal("@{") &&
     3078            $this->openString("}", $interp, null, array("'", '"', ";")) &&
     3079            $this->literal("}", false)
     3080        ) {
     3081            $out = array("interpolate", $interp);
     3082            $this->eatWhiteDefault = $oldWhite;
     3083            if ($this->eatWhiteDefault) $this->whitespace();
     3084            return true;
     3085        }
     3086
     3087        $this->eatWhiteDefault = $oldWhite;
     3088        $this->seek($s);
     3089        return false;
     3090    }
     3091
     3092    protected function unit(&$unit) {
     3093        // speed shortcut
     3094        if (isset($this->buffer[$this->count])) {
     3095            $char = $this->buffer[$this->count];
     3096            if (!ctype_digit($char) && $char != ".") return false;
     3097        }
     3098
     3099        if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) {
     3100            $unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]);
     3101            return true;
     3102        }
     3103        return false;
     3104    }
     3105
     3106    // a # color
     3107    protected function color(&$out) {
     3108        if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) {
     3109            if (strlen($m[1]) > 7) {
     3110                $out = array("string", "", array($m[1]));
     3111            } else {
     3112                $out = array("raw_color", $m[1]);
     3113            }
     3114            return true;
     3115        }
     3116
     3117        return false;
     3118    }
     3119
     3120    // consume an argument definition list surrounded by ()
     3121    // each argument is a variable name with optional value
     3122    // or at the end a ... or a variable named followed by ...
     3123    // arguments are separated by , unless a ; is in the list, then ; is the
     3124    // delimiter.
     3125    protected function argumentDef(&$args, &$isVararg) {
     3126        $s = $this->seek();
     3127        if (!$this->literal('(')) {
     3128            return false;
     3129        }
     3130
     3131        $values = array();
     3132        $delim = ",";
     3133        $method = "expressionList";
     3134
     3135        $isVararg = false;
     3136        while (true) {
     3137            if ($this->literal("...")) {
     3138                $isVararg = true;
     3139                break;
     3140            }
     3141
     3142            if ($this->$method($value)) {
     3143                if ($value[0] == "variable") {
     3144                    $arg = array("arg", $value[1]);
     3145                    $ss = $this->seek();
     3146
     3147                    if ($this->assign() && $this->$method($rhs)) {
     3148                        $arg[] = $rhs;
     3149                    } else {
     3150                        $this->seek($ss);
     3151                        if ($this->literal("...")) {
     3152                            $arg[0] = "rest";
     3153                            $isVararg = true;
     3154                        }
     3155                    }
     3156
     3157                    $values[] = $arg;
     3158                    if ($isVararg) {
     3159                        break;
     3160                    }
     3161                    continue;
     3162                } else {
     3163                    $values[] = array("lit", $value);
     3164                }
     3165            }
     3166
     3167
     3168            if (!$this->literal($delim)) {
     3169                if ($delim == "," && $this->literal(";")) {
     3170                    // found new delim, convert existing args
     3171                    $delim = ";";
     3172                    $method = "propertyValue";
     3173
     3174                    // transform arg list
     3175                    if (isset($values[1])) { // 2 items
     3176                        $newList = array();
     3177                        foreach ($values as $i => $arg) {
     3178                            switch ($arg[0]) {
     3179                            case "arg":
     3180                                if ($i) {
     3181                                    $this->throwError("Cannot mix ; and , as delimiter types");
     3182                                }
     3183                                $newList[] = $arg[2];
     3184                                break;
     3185                            case "lit":
     3186                                $newList[] = $arg[1];
     3187                                break;
     3188                            case "rest":
     3189                                $this->throwError("Unexpected rest before semicolon");
     3190                            }
     3191                        }
     3192
     3193                        $newList = array("list", ", ", $newList);
     3194
     3195                        switch ($values[0][0]) {
     3196                        case "arg":
     3197                            $newArg = array("arg", $values[0][1], $newList);
     3198                            break;
     3199                        case "lit":
     3200                            $newArg = array("lit", $newList);
     3201                            break;
     3202                        }
     3203
     3204                    } elseif ($values) { // 1 item
     3205                        $newArg = $values[0];
     3206                    }
     3207
     3208                    if ($newArg) {
     3209                        $values = array($newArg);
     3210                    }
     3211                } else {
     3212                    break;
     3213                }
     3214            }
     3215        }
     3216
     3217        if (!$this->literal(')')) {
     3218            $this->seek($s);
     3219            return false;
     3220        }
     3221
     3222        $args = $values;
     3223
     3224        return true;
     3225    }
     3226
     3227    // consume a list of tags
     3228    // this accepts a hanging delimiter
     3229    protected function tags(&$tags, $simple = false, $delim = ',') {
     3230        $tags = array();
     3231        while ($this->tag($tt, $simple)) {
     3232            $tags[] = $tt;
     3233            if (!$this->literal($delim)) break;
     3234        }
     3235        if (count($tags) == 0) return false;
     3236
     3237        return true;
     3238    }
     3239
     3240    // list of tags of specifying mixin path
     3241    // optionally separated by > (lazy, accepts extra >)
     3242    protected function mixinTags(&$tags) {
     3243        $tags = array();
     3244        while ($this->tag($tt, true)) {
     3245            $tags[] = $tt;
     3246            $this->literal(">");
     3247        }
     3248
     3249        if (!$tags) {
     3250            return false;
     3251        }
     3252
     3253        return true;
     3254    }
     3255
     3256    // a bracketed value (contained within in a tag definition)
     3257    protected function tagBracket(&$parts, &$hasExpression) {
     3258        // speed shortcut
     3259        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") {
     3260            return false;
     3261        }
     3262
     3263        $s = $this->seek();
     3264
     3265        $hasInterpolation = false;
     3266
     3267        if ($this->literal("[", false)) {
     3268            $attrParts = array("[");
     3269            // keyword, string, operator
     3270            while (true) {
     3271                if ($this->literal("]", false)) {
     3272                    $this->count--;
     3273                    break; // get out early
     3274                }
     3275
     3276                if ($this->match('\s+', $m)) {
     3277                    $attrParts[] = " ";
     3278                    continue;
     3279                }
     3280                if ($this->string($str)) {
     3281                    // escape parent selector, (yuck)
     3282                    foreach ($str[2] as &$chunk) {
     3283                        $chunk = str_replace($this->seed_wnb_lessc->parentSelector, "$&$", $chunk);
     3284                    }
     3285
     3286                    $attrParts[] = $str;
     3287                    $hasInterpolation = true;
     3288                    continue;
     3289                }
     3290
     3291                if ($this->keyword($word)) {
     3292                    $attrParts[] = $word;
     3293                    continue;
     3294                }
     3295
     3296                if ($this->interpolation($inter, false)) {
     3297                    $attrParts[] = $inter;
     3298                    $hasInterpolation = true;
     3299                    continue;
     3300                }
     3301
     3302                // operator, handles attr namespace too
     3303                if ($this->match('[|-~\$\*\^=]+', $m)) {
     3304                    $attrParts[] = $m[0];
     3305                    continue;
     3306                }
     3307
     3308                break;
     3309            }
     3310
     3311            if ($this->literal("]", false)) {
     3312                $attrParts[] = "]";
     3313                foreach ($attrParts as $part) {
     3314                    $parts[] = $part;
     3315                }
     3316                $hasExpression = $hasExpression || $hasInterpolation;
     3317                return true;
     3318            }
     3319            $this->seek($s);
     3320        }
     3321
     3322        $this->seek($s);
     3323        return false;
     3324    }
     3325
     3326    // a space separated list of selectors
     3327    protected function tag(&$tag, $simple = false) {
     3328        if ($simple) {
     3329            $chars = '^@,:;{}\][>\(\) "\'';
     3330        } else {
     3331            $chars = '^@,;{}["\'';
     3332        }
     3333        $s = $this->seek();
     3334
     3335        $hasExpression = false;
     3336        $parts = array();
     3337        while ($this->tagBracket($parts, $hasExpression));
     3338
     3339        $oldWhite = $this->eatWhiteDefault;
     3340        $this->eatWhiteDefault = false;
     3341
     3342        while (true) {
     3343            if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) {
     3344                $parts[] = $m[1];
     3345                if ($simple) break;
     3346
     3347                while ($this->tagBracket($parts, $hasExpression));
     3348                continue;
     3349            }
     3350
     3351            if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") {
     3352                if ($this->interpolation($interp)) {
     3353                    $hasExpression = true;
     3354                    $interp[2] = true; // don't unescape
     3355                    $parts[] = $interp;
     3356                    continue;
     3357                }
     3358
     3359                if ($this->literal("@")) {
     3360                    $parts[] = "@";
     3361                    continue;
     3362                }
     3363            }
     3364
     3365            if ($this->unit($unit)) { // for keyframes
     3366                $parts[] = $unit[1];
     3367                $parts[] = $unit[2];
     3368                continue;
     3369            }
     3370
     3371            break;
     3372        }
     3373
     3374        $this->eatWhiteDefault = $oldWhite;
     3375        if (!$parts) {
     3376            $this->seek($s);
     3377            return false;
     3378        }
     3379
     3380        if ($hasExpression) {
     3381            $tag = array("exp", array("string", "", $parts));
     3382        } else {
     3383            $tag = trim(implode($parts));
     3384        }
     3385
     3386        $this->whitespace();
     3387        return true;
     3388    }
     3389
     3390    // a css function
     3391    protected function func(&$func) {
     3392        $s = $this->seek();
     3393
     3394        if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) {
     3395            $fname = $m[1];
     3396
     3397            $sPreArgs = $this->seek();
     3398
     3399            $args = array();
     3400            while (true) {
     3401                $ss = $this->seek();
     3402                // this ugly nonsense is for ie filter properties
     3403                if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
     3404                    $args[] = array("string", "", array($name, "=", $value));
     3405                } else {
     3406                    $this->seek($ss);
     3407                    if ($this->expressionList($value)) {
     3408                        $args[] = $value;
     3409                    }
     3410                }
     3411
     3412                if (!$this->literal(',')) break;
     3413            }
     3414            $args = array('list', ',', $args);
     3415
     3416            if ($this->literal(')')) {
     3417                $func = array('function', $fname, $args);
     3418                return true;
     3419            } elseif ($fname == 'url') {
     3420                // couldn't parse and in url? treat as string
     3421                $this->seek($sPreArgs);
     3422                if ($this->openString(")", $string) && $this->literal(")")) {
     3423                    $func = array('function', $fname, $string);
     3424                    return true;
     3425                }
     3426            }
     3427        }
     3428
     3429        $this->seek($s);
     3430        return false;
     3431    }
     3432
     3433    // consume a less variable
     3434    protected function variable(&$name) {
     3435        $s = $this->seek();
     3436        if ($this->literal($this->seed_wnb_lessc->vPrefix, false) &&
     3437            ($this->variable($sub) || $this->keyword($name))
     3438        ) {
     3439            if (!empty($sub)) {
     3440                $name = array('variable', $sub);
     3441            } else {
     3442                $name = $this->seed_wnb_lessc->vPrefix.$name;
     3443            }
     3444            return true;
     3445        }
     3446
     3447        $name = null;
     3448        $this->seek($s);
     3449        return false;
     3450    }
     3451
     3452    /**
     3453     * Consume an assignment operator
     3454     * Can optionally take a name that will be set to the current property name
     3455     */
     3456    protected function assign($name = null) {
     3457        if ($name) $this->currentProperty = $name;
     3458        return $this->literal(':') || $this->literal('=');
     3459    }
     3460
     3461    // consume a keyword
     3462    protected function keyword(&$word) {
     3463        if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
     3464            $word = $m[1];
     3465            return true;
     3466        }
     3467        return false;
     3468    }
     3469
     3470    // consume an end of statement delimiter
     3471    protected function end() {
     3472        if ($this->literal(';', false)) {
     3473            return true;
     3474        } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
     3475            // if there is end of file or a closing block next then we don't need a ;
     3476            return true;
     3477        }
     3478        return false;
     3479    }
     3480
     3481    protected function guards(&$guards) {
     3482        $s = $this->seek();
     3483
     3484        if (!$this->literal("when")) {
     3485            $this->seek($s);
     3486            return false;
     3487        }
     3488
     3489        $guards = array();
     3490
     3491        while ($this->guardGroup($g)) {
     3492            $guards[] = $g;
     3493            if (!$this->literal(",")) break;
     3494        }
     3495
     3496        if (count($guards) == 0) {
     3497            $guards = null;
     3498            $this->seek($s);
     3499            return false;
     3500        }
     3501
     3502        return true;
     3503    }
     3504
     3505    // a bunch of guards that are and'd together
     3506    // TODO rename to guardGroup
     3507    protected function guardGroup(&$guardGroup) {
     3508        $s = $this->seek();
     3509        $guardGroup = array();
     3510        while ($this->guard($guard)) {
     3511            $guardGroup[] = $guard;
     3512            if (!$this->literal("and")) break;
     3513        }
     3514
     3515        if (count($guardGroup) == 0) {
     3516            $guardGroup = null;
     3517            $this->seek($s);
     3518            return false;
     3519        }
     3520
     3521        return true;
     3522    }
     3523
     3524    protected function guard(&$guard) {
     3525        $s = $this->seek();
     3526        $negate = $this->literal("not");
     3527
     3528        if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) {
     3529            $guard = $exp;
     3530            if ($negate) $guard = array("negate", $guard);
     3531            return true;
     3532        }
     3533
     3534        $this->seek($s);
     3535        return false;
     3536    }
     3537
     3538    /* raw parsing functions */
     3539
     3540    protected function literal($what, $eatWhitespace = null) {
     3541        if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
     3542
     3543        // shortcut on single letter
     3544        if (!isset($what[1]) && isset($this->buffer[$this->count])) {
     3545            if ($this->buffer[$this->count] == $what) {
     3546                if (!$eatWhitespace) {
     3547                    $this->count++;
     3548                    return true;
     3549                }
     3550                // goes below...
     3551            } else {
     3552                return false;
     3553            }
     3554        }
     3555
     3556        if (!isset(self::$literalCache[$what])) {
     3557            self::$literalCache[$what] = seed_wnb_lessc::preg_quote($what);
     3558        }
     3559
     3560        return $this->match(self::$literalCache[$what], $m, $eatWhitespace);
     3561    }
     3562
     3563    protected function genericList(&$out, $parseItem, $delim="", $flatten=true) {
     3564        $s = $this->seek();
     3565        $items = array();
     3566        while ($this->$parseItem($value)) {
     3567            $items[] = $value;
     3568            if ($delim) {
     3569                if (!$this->literal($delim)) break;
     3570            }
     3571        }
     3572
     3573        if (count($items) == 0) {
     3574            $this->seek($s);
     3575            return false;
     3576        }
     3577
     3578        if ($flatten && count($items) == 1) {
     3579            $out = $items[0];
     3580        } else {
     3581            $out = array("list", $delim, $items);
     3582        }
     3583
     3584        return true;
     3585    }
     3586
     3587
     3588    // advance counter to next occurrence of $what
     3589    // $until - don't include $what in advance
     3590    // $allowNewline, if string, will be used as valid char set
     3591    protected function to($what, &$out, $until = false, $allowNewline = false) {
     3592        if (is_string($allowNewline)) {
     3593            $validChars = $allowNewline;
     3594        } else {
     3595            $validChars = $allowNewline ? "." : "[^\n]";
     3596        }
     3597        if (!$this->match('('.$validChars.'*?)'.seed_wnb_lessc::preg_quote($what), $m, !$until)) return false;
     3598        if ($until) $this->count -= strlen($what); // give back $what
     3599        $out = $m[1];
     3600        return true;
     3601    }
     3602
     3603    // try to match something on head of buffer
     3604    protected function match($regex, &$out, $eatWhitespace = null) {
     3605        if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
     3606
     3607        $r = '/'.$regex.($eatWhitespace && !$this->writeComments ? '\s*' : '').'/Ais';
     3608        if (preg_match($r, $this->buffer, $out, null, $this->count)) {
     3609            $this->count += strlen($out[0]);
     3610            if ($eatWhitespace && $this->writeComments) $this->whitespace();
     3611            return true;
     3612        }
     3613        return false;
     3614    }
     3615
     3616    // match some whitespace
     3617    protected function whitespace() {
     3618        if ($this->writeComments) {
     3619            $gotWhite = false;
     3620            while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
     3621                if (isset($m[1]) && empty($this->seenComments[$this->count])) {
     3622                    $this->append(array("comment", $m[1]));
     3623                    $this->seenComments[$this->count] = true;
     3624                }
     3625                $this->count += strlen($m[0]);
     3626                $gotWhite = true;
     3627            }
     3628            return $gotWhite;
     3629        } else {
     3630            $this->match("", $m);
     3631            return strlen($m[0]) > 0;
     3632        }
     3633    }
     3634
     3635    // match something without consuming it
     3636    protected function peek($regex, &$out = null, $from=null) {
     3637        if (is_null($from)) $from = $this->count;
     3638        $r = '/'.$regex.'/Ais';
     3639        $result = preg_match($r, $this->buffer, $out, null, $from);
     3640
     3641        return $result;
     3642    }
     3643
     3644    // seek to a spot in the buffer or return where we are on no argument
     3645    protected function seek($where = null) {
     3646        if ($where === null) return $this->count;
     3647        else $this->count = $where;
     3648        return true;
     3649    }
     3650
     3651    /* misc functions */
     3652
     3653    public function throwError($msg = "parse error", $count = null) {
     3654        $count = is_null($count) ? $this->count : $count;
     3655
     3656        $line = $this->line +
     3657            substr_count(substr($this->buffer, 0, $count), "\n");
     3658
     3659        if (!empty($this->sourceName)) {
     3660            $loc = "$this->sourceName on line $line";
     3661        } else {
     3662            $loc = "line: $line";
     3663        }
     3664
     3665        // TODO this depends on $this->count
     3666        if ($this->peek("(.*?)(\n|$)", $m, $count)) {
     3667            throw new exception("$msg: failed at `$m[1]` $loc");
     3668        } else {
     3669            throw new exception("$msg: $loc");
     3670        }
     3671    }
     3672
     3673    protected function pushBlock($selectors=null, $type=null) {
     3674        $b = new stdclass;
     3675        $b->parent = $this->env;
     3676
     3677        $b->type = $type;
     3678        $b->id = self::$nextBlockId++;
     3679
     3680        $b->isVararg = false; // TODO: kill me from here
     3681        $b->tags = $selectors;
     3682
     3683        $b->props = array();
     3684        $b->children = array();
     3685
     3686        $this->env = $b;
     3687        return $b;
     3688    }
     3689
     3690    // push a block that doesn't multiply tags
     3691    protected function pushSpecialBlock($type) {
     3692        return $this->pushBlock(null, $type);
     3693    }
     3694
     3695    // append a property to the current block
     3696    protected function append($prop, $pos = null) {
     3697        if ($pos !== null) $prop[-1] = $pos;
     3698        $this->env->props[] = $prop;
     3699    }
     3700
     3701    // pop something off the stack
     3702    protected function pop() {
     3703        $old = $this->env;
     3704        $this->env = $this->env->parent;
     3705        return $old;
     3706    }
     3707
     3708    // remove comments from $text
     3709    // todo: make it work for all functions, not just url
     3710    protected function removeComments($text) {
     3711        $look = array(
     3712            'url(', '//', '/*', '"', "'"
     3713        );
     3714
     3715        $out = '';
     3716        $min = null;
     3717        while (true) {
     3718            // find the next item
     3719            foreach ($look as $token) {
     3720                $pos = strpos($text, $token);
     3721                if ($pos !== false) {
     3722                    if (!isset($min) || $pos < $min[1]) $min = array($token, $pos);
     3723                }
     3724            }
     3725
     3726            if (is_null($min)) break;
     3727
     3728            $count = $min[1];
     3729            $skip = 0;
     3730            $newlines = 0;
     3731            switch ($min[0]) {
     3732            case 'url(':
     3733                if (preg_match('/url\(.*?\)/', $text, $m, 0, $count))
     3734                    $count += strlen($m[0]) - strlen($min[0]);
     3735                break;
     3736            case '"':
     3737            case "'":
     3738                if (preg_match('/'.$min[0].'.*?(?<!\\\\)'.$min[0].'/', $text, $m, 0, $count))
     3739                    $count += strlen($m[0]) - 1;
     3740                break;
     3741            case '//':
     3742                $skip = strpos($text, "\n", $count);
     3743                if ($skip === false) $skip = strlen($text) - $count;
     3744                else $skip -= $count;
     3745                break;
     3746            case '/*':
     3747                if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) {
     3748                    $skip = strlen($m[0]);
     3749                    $newlines = substr_count($m[0], "\n");
     3750                }
     3751                break;
     3752            }
     3753
     3754            if ($skip == 0) $count += strlen($min[0]);
     3755
     3756            $out .= substr($text, 0, $count).str_repeat("\n", $newlines);
     3757            $text = substr($text, $count + $skip);
     3758
     3759            $min = null;
     3760        }
     3761
     3762        return $out.$text;
     3763    }
     3764
     3765}
     3766
     3767class seed_wnb_lessc_formatter_classic {
     3768    public $indentChar = "  ";
     3769
     3770    public $break = "\n";
     3771    public $open = " {";
     3772    public $close = "}";
     3773    public $selectorSeparator = ", ";
     3774    public $assignSeparator = ":";
     3775
     3776    public $openSingle = " { ";
     3777    public $closeSingle = " }";
     3778
     3779    public $disableSingle = false;
     3780    public $breakSelectors = false;
     3781
     3782    public $compressColors = false;
     3783
     3784    public function __construct() {
     3785        $this->indentLevel = 0;
     3786    }
     3787
     3788    public function indentStr($n = 0) {
     3789        return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
     3790    }
     3791
     3792    public function property($name, $value) {
     3793        return $name . $this->assignSeparator . $value . ";";
     3794    }
     3795
     3796    protected function isEmpty($block) {
     3797        if (empty($block->lines)) {
     3798            foreach ($block->children as $child) {
     3799                if (!$this->isEmpty($child)) return false;
     3800            }
     3801
     3802            return true;
     3803        }
     3804        return false;
     3805    }
     3806
     3807    public function block($block) {
     3808        if ($this->isEmpty($block)) return;
     3809
     3810        $inner = $pre = $this->indentStr();
     3811
     3812        $isSingle = !$this->disableSingle &&
     3813            is_null($block->type) && count($block->lines) == 1;
     3814
     3815        if (!empty($block->selectors)) {
     3816            $this->indentLevel++;
     3817
     3818            if ($this->breakSelectors) {
     3819                $selectorSeparator = $this->selectorSeparator . $this->break . $pre;
     3820            } else {
     3821                $selectorSeparator = $this->selectorSeparator;
     3822            }
     3823
     3824            echo $pre .
     3825                implode($selectorSeparator, $block->selectors);
     3826            if ($isSingle) {
     3827                echo $this->openSingle;
     3828                $inner = "";
     3829            } else {
     3830                echo $this->open . $this->break;
     3831                $inner = $this->indentStr();
     3832            }
     3833
     3834        }
     3835
     3836        if (!empty($block->lines)) {
     3837            $glue = $this->break.$inner;
     3838            echo $inner . implode($glue, $block->lines);
     3839            if (!$isSingle && !empty($block->children)) {
     3840                echo $this->break;
     3841            }
     3842        }
     3843
     3844        foreach ($block->children as $child) {
     3845            $this->block($child);
     3846        }
     3847
     3848        if (!empty($block->selectors)) {
     3849            if (!$isSingle && empty($block->children)) echo $this->break;
     3850
     3851            if ($isSingle) {
     3852                echo $this->closeSingle . $this->break;
     3853            } else {
     3854                echo $pre . $this->close . $this->break;
     3855            }
     3856
     3857            $this->indentLevel--;
     3858        }
     3859    }
     3860}
     3861
     3862class seed_wnb_lessc_formatter_compressed extends seed_wnb_lessc_formatter_classic {
     3863    public $disableSingle = true;
     3864    public $open = "{";
     3865    public $selectorSeparator = ",";
     3866    public $assignSeparator = ":";
     3867    public $break = "";
     3868    public $compressColors = true;
     3869
     3870    public function indentStr($n = 0) {
     3871        return "";
     3872    }
     3873}
     3874
     3875class seed_wnb_lessc_formatter_lessjs extends seed_wnb_lessc_formatter_classic {
     3876    public $disableSingle = true;
     3877    public $breakSelectors = true;
     3878    public $assignSeparator = ": ";
     3879    public $selectorSeparator = ",";
     3880}
  • wordpress-notification-bar/trunk/readme.txt

    r1661055 r2262049  
    44Tags: message, floating bar, notice, notification, sticky header, special offer, discount offer, offer, important, notification bar, attention bar, highlight bar
    55Requires at least: 3.0.0
    6 Tested up to: 4.7.5
    7 Stable tag: 1.3.9
     6Tested up to: 5.3
     7Stable tag: 1.3.10
    88
    99A quick and easy notification bar and call to action for your site.
     
    5858
    5959== Changelog ==
     60= 1.3.10 =
     61* Fixed php warnings
     62
    6063= 1.3.9 =
    6164* Added Italian Translation
  • wordpress-notification-bar/trunk/wordpress-notification-bar.php

    r1661055 r2262049  
    44Plugin URI: http://seedprod.com/wordpress-notification-bar/
    55Description: Global Notification Bar for WordPress
    6 Version:  1.3.9
     6Version:  1.3.10
    77Text Domain: wordpress-notification-bar
    88Domain Path: /languages
Note: See TracChangeset for help on using the changeset viewer.