Source for file parser.php
Documentation is available at parser.php
* @package FrameworkOnFramework
* @copyright Copyright (C) 2010 - 2012 Akeeba Ltd. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
// Protect from unauthorized access
* This class is taken verbatim from:
* http://leafo.net/lessphp
* LESS css compiler, adapted from http://lesscss.org
* Copyright 2012, Leaf Corcoran <leafot@gmail.com>
* Licensed under MIT or GPLv3, see LICENSE
* Responsible for taking a string of LESS code and converting it into a syntax tree
// Used to uniquely identify blocks
protected static $nextBlockId =
0;
protected static $precedence =
array(
protected static $whitePattern;
protected static $commentMulti;
protected static $commentSingle =
"//";
protected static $commentMultiLeft =
"/*";
protected static $commentMultiRight =
"*/";
// Regex string to match any of the operators
protected static $operatorString;
// These properties will supress division unless it's inside parenthases
protected static $supressDivisionProps =
array('/border-radius$/i', '/^font$/i');
protected $blockDirectives =
array("font-face", "keyframes", "page", "-moz-document");
* if we are in parens we can be more liberal with whitespace around
* operators because it must evaluate to a single value and thus is less
* property1: 10 -5; // is two numbers, 10 and -5
* property2: (10 -5); // should evaluate to 5
// Caches preg escaped literals
protected static $literalCache =
array();
* @param [type] $lessc [description]
* @param string $sourceName [description]
public function __construct($lessc, $sourceName =
null)
$this->eatWhiteDefault =
true;
// Reference to less needed for vPrefix, mPrefix, and parentSelector
// Name used for error messages
$this->sourceName =
$sourceName;
$this->writeComments =
false;
if (!self::$operatorString)
self::$operatorString =
'(' .
implode('|', array_map(array('FOFLess', 'preg_quote'), array_keys(self::$precedence))) .
')';
$commentSingle =
FOFLess::preg_quote(self::$commentSingle);
$commentMultiLeft =
FOFLess::preg_quote(self::$commentMultiLeft);
$commentMultiRight =
FOFLess::preg_quote(self::$commentMultiRight);
self::$commentMulti =
$commentMultiLeft .
'.*?' .
$commentMultiRight;
self::$whitePattern =
'/' .
$commentSingle .
'[^\n]*\s*|(' .
self::$commentMulti .
')\s*|\s+/Ais';
* @param string $buffer [description]
* @return [type] [description]
public function parse($buffer)
$this->buffer =
$this->writeComments ?
$buffer :
$this->removeComments($buffer);
$this->eatWhiteDefault =
true;
$this->seenComments =
array();
* trim whitespace on head
* if (preg_match('/^\s+/', $this->buffer, $m)) {
* $this->line += substr_count($m[0], "\n");
* $this->buffer = ltrim($this->buffer);
$lastCount =
$this->count;
if ($this->count !=
strlen($this->buffer))
// TODO report where the block was opened
throw
new exception('parse error: unclosed block');
* Parse a single chunk off the head of the buffer and append it to the
* current parse environment.
* Returns false when the buffer is empty, or when there is an error.
* This function is called repeatedly until the entire document is
* This parser is most similar to a recursive descent parser. Single
* functions represent discrete grammatical rules for the language, and
* they are able to capture the text that represents those rules.
* Consider the function lessc::keyword(). (all parse functions are
* The function takes a single reference argument. When calling the
* function it will attempt to match a keyword on the head of the buffer.
* If it is successful, it will place the keyword in the referenced
* argument, advance the position in the buffer, and return true. If it
* fails then it won't advance the buffer and it will return false.
* All of these parse functions are powered by lessc::match(), which behaves
* the same way, but takes a literal regular expression. Sometimes it is
* more convenient to use match instead of creating a new function.
* Because of the format of the functions, to parse an entire string of
* grammatical rules, you can chain them together using &&.
* But, if some of the rules in the chain succeed before one fails, then
* the buffer position will be left at an invalid state. In order to
* avoid this, lessc::seek() is used to remember and set buffer positions.
* Before parsing a chain, use $s = $this->seek() to remember the current
* position into $s. Then if a chain fails, use $this->seek($s) to
* go back where we started.
if (empty($this->buffer))
$this->append(array('assign', $key, $value), $s);
// Look for special css blocks
$media->queries =
is_null($mediaQueries) ?
array() :
$mediaQueries;
if (($this->openString("{", $dirValue, null, array(";")) ||
true)
$this->append(array("directive", $dirName, $dirValue));
$this->append(array('assign', $var, $value), $s);
if ($this->import($importValue))
$this->append($importValue, $s);
// Opening parametric mixin
&&
($this->guards($guards) ||
true)
$block->isVararg =
$isVararg;
$block->guards =
$guards;
// Opening a simple block
if (!isset
($block->args))
foreach ($block->tags as $tag)
if (!is_string($tag) ||
$tag{0} !=
$this->lessc->mPrefix)
foreach ($block->tags as $tag)
$this->env->children[$tag][] =
$block;
$this->append(array('block', $block), $s);
// This is done here so comments aren't bundled into he block that was just closed
&&
($this->keyword($suffix) ||
true)
$this->append(array('mixin', $tags, $argv, $suffix), $s);
// Got nothing, throw error
* [isDirective description]
* @param string $dirname [description]
* @param [type] $directives [description]
// TODO: cache pattern in parser
$pattern =
implode("|", array_map(array("FOFLess", "preg_quote"), $directives));
$pattern =
'/^(-[a-z-]+-)?(' .
$pattern .
')$/i';
* @param [type] $tags [description]
* @return [type] [description]
// Move @ tags out of variable namespace
if ($tag{0} ==
$this->lessc->vPrefix)
$tag[0] =
$this->lessc->mPrefix;
* @param [type] &$exps [description]
* Attempt to consume an expression.
* @param string &$out [description]
* @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
if (!empty($this->env->supressedDivision))
unset
($this->env->supressedDivision);
array($out, array("keyword", "/"), $rhs));
* Recursively parse infix equation with $lhs at precedence $minP
* @param type $lhs [description]
* @param type $minP [description]
$whiteBefore = isset
($this->buffer[$this->count -
1]) &&
ctype_space($this->buffer[$this->count -
1]);
// If there is whitespace before the operator, then we require
// whitespace after the operator for it to be an expression
$needWhite =
$whiteBefore &&
!$this->inParens;
if ($this->match(self::$operatorString .
($needWhite ?
'\s' :
''), $m) &&
self::$precedence[$m[1]] >=
$minP)
if (!$this->inParens && isset
($this->env->currentProperty) &&
$m[1] ==
"/" &&
empty($this->env->supressedDivision))
foreach (self::$supressDivisionProps as $pattern)
if (preg_match($pattern, $this->env->currentProperty))
$this->env->supressedDivision =
true;
$whiteAfter = isset
($this->buffer[$this->count -
1]) &&
ctype_space($this->buffer[$this->count -
1]);
// Peek for next operator to see what to do with rhs
if ($this->peek(self::$operatorString, $next) &&
self::$precedence[$next[1]] >
self::$precedence[$m[1]])
$rhs =
$this->expHelper($rhs, self::$precedence[$next[1]]);
$lhs =
array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter);
* Consume a list of values for a property
* @param [type] &$value [description]
* @param [type] $keyName [description]
$this->env->currentProperty =
$keyName;
unset
($this->env->currentProperty);
* [parenValue description]
* @param [type] &$out [description]
if (isset
($this->buffer[$this->count]) &&
$this->buffer[$this->count] !=
"(")
* @param [type] &$value [description]
protected function value(&$value)
if (isset
($this->buffer[$this->count]) &&
$this->buffer[$this->count] ==
"-")
if ($this->literal("-", false) &&
(($this->variable($inner) &&
$inner =
array("variable", $inner))
$value =
array("unary", "-", $inner);
if ($this->color($value))
$value =
array('keyword', $word);
$value =
array('variable', $var);
// Unquote string (should this work on any type?
$value =
array("escape", $str);
$value =
array('keyword', '\\' .
$m[1]);
* @param [type] &$out [description]
protected function import(&$out)
* @import "something.css" media;
* @import url("something.css") media;
* @import url(something.css) media;
$out =
array("import", $value);
* [mediaQueryList description]
* @param [type] &$out [description]
if ($this->genericList($list, "mediaQuery", ",", false))
* [mediaQuery description]
* @param [type] &$out [description]
* @return [type] [description]
if (($this->literal("only") &&
($only =
true) ||
$this->literal("not") &&
($not =
true) ||
true) &&
$this->keyword($mediaType))
$prop =
array("mediaType");
if (!empty($mediaType) &&
!$this->literal("and"))
$this->genericList($expressions, "mediaExpression", "and", false);
* [mediaExpression description]
* @param [type] &$out [description]
$out =
array("mediaExp", $feature);
$out =
array('variable', $variable);
* An unbounded string stopped by $end
* @param [type] $end [description]
* @param [type] &$out [description]
* @param [type] $nestingOpen [description]
* @param [type] $rejectStrs [description]
protected function openString($end, &$out, $nestingOpen =
null, $rejectStrs =
null)
$oldWhite =
$this->eatWhiteDefault;
$this->eatWhiteDefault =
false;
$stop =
array("'", '"', "@{", $end);
$stop =
array_map(array("FOFLess", "preg_quote"), $stop);
// $stop[] = self::$commentMulti;
$patt =
'(.*?)(' .
implode("|", $stop) .
')';
while ($this->match($patt, $m, false))
if (($tok ==
"'" ||
$tok ==
'"') &&
$this->string($str))
$this->eatWhiteDefault =
$oldWhite;
if (count($content) ==
0)
$out =
array("string", "", $content);
* @param [type] &$out [description]
protected function string(&$out)
elseif ($this->literal("'", false))
// Look for either ending delim , escape, or string interpolation
$oldWhite =
$this->eatWhiteDefault;
$this->eatWhiteDefault =
false;
while ($this->match($patt, $m, false))
$this->count -=
strlen($m[2]);
$this->count +=
strlen($m[2]);
$this->count -=
strlen($delim);
$this->eatWhiteDefault =
$oldWhite;
$out =
array("string", $delim, $content);
* [interpolation description]
* @param [type] &$out [description]
$oldWhite =
$this->eatWhiteDefault;
$this->eatWhiteDefault =
true;
$out =
array("interpolate", $interp);
$this->eatWhiteDefault =
$oldWhite;
if ($this->eatWhiteDefault)
$this->eatWhiteDefault =
$oldWhite;
* @param [type] &$unit [description]
protected function unit(&$unit)
if (isset
($this->buffer[$this->count]))
$char =
$this->buffer[$this->count];
if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m))
$unit =
array("number", $m[1], empty($m[2]) ?
"" :
$m[2]);
* @param [type] &$out [description]
protected function color(&$out)
if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m))
$out =
array("string", "", array($m[1]));
$out =
array("raw_color", $m[1]);
* Consume a list of property values delimited by ; and wrapped in ()
* @param [type] &$args [description]
* @param [type] $delim [description]
* Consume an argument definition list surrounded by ()
* each argument is a variable name with optional value
* or at the end a ... or a variable named followed by ...
* @param [type] &$args [description]
* @param [type] &$isVararg [description]
* @param [type] $delim [description]
protected function argumentDef(&$args, &$isVararg, $delim =
',')
$arg =
array("arg", $vname);
if ($this->value($literal))
$values[] =
array("lit", $literal);
* This accepts a hanging delimiter
* @param [type] &$tags [description]
* @param [type] $simple [description]
* @param [type] $delim [description]
protected function tags(&$tags, $simple =
false, $delim =
',')
while ($this->tag($tt, $simple))
* List of tags of specifying mixin path
* Optionally separated by > (lazy, accepts extra >)
* @param [type] &$tags [description]
while ($this->tag($tt, true))
* A bracketed value (contained within in a tag definition)
* @param [type] &$value [description]
if (isset
($this->buffer[$this->count]) &&
$this->buffer[$this->count] !=
"[")
if ($this->literal('[') &&
$this->to(']', $c, true) &&
$this->literal(']', false))
// Escape parent selector, (yuck)
$value =
str_replace($this->lessc->parentSelector, "$&$", $value);
* [tagExpression description]
* @param [type] &$value [description]
$value =
array('exp', $exp);
* @param [type] &$tag [description]
* @param boolean $simple [description]
protected function tag(&$tag, $simple =
false)
$chars =
'^@,:;{}\][>\(\) "\'';
$oldWhite =
$this->eatWhiteDefault;
$this->eatWhiteDefault =
false;
if ($this->match('([' .
$chars .
'0-9][' .
$chars .
']*)', $m))
if (isset
($this->buffer[$this->count]) &&
$this->buffer[$this->count] ==
"@")
$this->eatWhiteDefault =
$oldWhite;
$tag =
array("exp", array("string", "", $parts));
* @param [type] &$func [description]
protected function func(&$func)
if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) &&
$this->literal('('))
$sPreArgs =
$this->seek();
// This ugly nonsense is for ie filter properties
$args[] =
array("string", "", array($name, "=", $value));
$args =
array('list', ',', $args);
$func =
array('function', $fname, $args);
// Couldn't parse and in url? treat as string
$func =
array('function', $fname, $string);
* Consume a less variable
* @param [type] &$name [description]
$name =
array('variable', $sub);
$name =
$this->lessc->vPrefix .
$name;
* Consume an assignment operator
* Can optionally take a name that will be set to the current property name
* @param string $name [description]
protected function assign($name =
null)
$this->currentProperty =
$name;
* @param [type] &$word [description]
if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m))
* Consume an end of statement delimiter
elseif ($this->count ==
strlen($this->buffer) ||
$this->buffer{$this->count} ==
'}')
// If there is end of file or a closing block next then we don't need a ;
* @param [type] &$guards [description]
protected function guards(&$guards)
* A bunch of guards that are and'd together
* @param [type] &$guardGroup [description]
* @todo rename to guardGroup
while ($this->guard($guard))
if (count($guardGroup) ==
0)
* @param [type] &$guard [description]
protected function guard(&$guard)
$guard =
array("negate", $guard);
/* raw parsing functions */
* @param [type] $what [description]
* @param [type] $eatWhitespace [description]
protected function literal($what, $eatWhitespace =
null)
if ($eatWhitespace ===
null)
$eatWhitespace =
$this->eatWhiteDefault;
// Shortcut on single letter
if (!isset
($what[1]) && isset
($this->buffer[$this->count]))
if ($this->buffer[$this->count] ==
$what)
if (!isset
(self::$literalCache[$what]))
self::$literalCache[$what] =
FOFLess::preg_quote($what);
return $this->match(self::$literalCache[$what], $m, $eatWhitespace);
* [genericList description]
* @param [type] &$out [description]
* @param [type] $parseItem [description]
* @param string $delim [description]
* @param boolean $flatten [description]
protected function genericList(&$out, $parseItem, $delim =
"", $flatten =
true)
while ($this->$parseItem($value))
if ($flatten &&
count($items) ==
1)
$out =
array("list", $delim, $items);
* Advance counter to next occurrence of $what
* $until - don't include $what in advance
* $allowNewline, if string, will be used as valid char set
* @param [type] $what [description]
* @param [type] &$out [description]
* @param boolean $until [description]
* @param boolean $allowNewline [description]
protected function to($what, &$out, $until =
false, $allowNewline =
false)
$validChars =
$allowNewline;
$validChars =
$allowNewline ?
"." :
"[^\n]";
$this->count -=
strlen($what);
* Try to match something on head of buffer
* @param [type] $regex [description]
* @param [type] &$out [description]
* @param [type] $eatWhitespace [description]
protected function match($regex, &$out, $eatWhitespace =
null)
if ($eatWhitespace ===
null)
$eatWhitespace =
$this->eatWhiteDefault;
$r =
'/' .
$regex .
($eatWhitespace &&
!$this->writeComments ?
'\s*' :
'') .
'/Ais';
if (preg_match($r, $this->buffer, $out, null, $this->count))
$this->count +=
strlen($out[0]);
if ($eatWhitespace &&
$this->writeComments)
if ($this->writeComments)
while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count))
if (isset
($m[1]) &&
empty($this->commentsSeen[$this->count]))
$this->append(array("comment", $m[1]));
$this->commentsSeen[$this->count] =
true;
$this->count +=
strlen($m[0]);
* Match something without consuming it
* @param [type] $regex [description]
* @param [type] &$out [description]
* @param [type] $from [description]
protected function peek($regex, &$out =
null, $from =
null)
$r =
'/' .
$regex .
'/Ais';
$result =
preg_match($r, $this->buffer, $out, null, $from);
* Seek to a spot in the buffer or return where we are on no argument
* @param [type] $where [description]
protected function seek($where =
null)
* [throwError description]
* @param string $msg [description]
* @param [type] $count [description]
public function throwError($msg =
"parse error", $count =
null)
$count =
is_null($count) ?
$this->count :
$count;
if (!empty($this->sourceName))
$loc =
"$this->sourceName on line $line";
// TODO this depends on $this->count
if ($this->peek("(.*?)(\n|$)", $m, $count))
throw
new exception("$msg: failed at `$m[1]` $loc");
throw
new exception("$msg: $loc");
* [pushBlock description]
* @param [type] $selectors [description]
* @param [type] $type [description]
protected function pushBlock($selectors =
null, $type =
null)
$b->id =
self::$nextBlockId++
;
// TODO: kill me from here
* Push a block that doesn't multiply tags
* @param [type] $type [description]
* Append a property to the current block
* @param [type] $prop [description]
* @param [type] $pos [description]
protected function append($prop, $pos =
null)
$this->env->props[] =
$prop;
* Pop something off the stack
* @return [type] [description]
$this->env =
$this->env->parent;
* Remove comments from $text
* @param [type] $text [description]
* @todo: make it work for all functions, not just url
* @return [type] [description]
'url(', '//', '/*', '"', "'"
foreach ($look as $token)
if (!isset
($min) ||
$pos <
$min[1])
$min =
array($token, $pos);
if (preg_match('/url\(.*?\)/', $text, $m, 0, $count))
if (preg_match('/' .
$min[0] .
'.*?' .
$min[0] .
'/', $text, $m, 0, $count))
$skip =
strpos($text, "\n", $count);
$skip =
strlen($text) -
$count;
if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count))
$text =
substr($text, $count +
$skip);
Documentation generated on Tue, 19 Nov 2013 15:10:16 +0100 by phpDocumentor 1.4.3