123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842 |
- <?php
- /**
- * SCSSPHP
- *
- * @copyright 2012-2015 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://leafo.github.io/scssphp
- */
- namespace Leafo\ScssPhp;
- use Leafo\ScssPhp\Block;
- use Leafo\ScssPhp\Compiler;
- use Leafo\ScssPhp\Exception\ParserException;
- use Leafo\ScssPhp\Node;
- use Leafo\ScssPhp\Type;
- /**
- * Parser
- *
- * @author Leaf Corcoran <leafot@gmail.com>
- */
- class Parser
- {
- const SOURCE_INDEX = -1;
- const SOURCE_LINE = -2;
- const SOURCE_COLUMN = -3;
- /**
- * @var array
- */
- protected static $precedence = array('=' => 0, 'or' => 1, 'and' => 2, '==' => 3, '!=' => 3, '<=>' => 3, '<=' => 4, '>=' => 4, '<' => 4, '>' => 4, '+' => 5, '-' => 5, '*' => 6, '/' => 6, '%' => 6);
- protected static $commentPattern;
- protected static $operatorPattern;
- protected static $whitePattern;
- private $sourceName;
- private $sourceIndex;
- private $sourcePositions;
- private $charset;
- private $count;
- private $env;
- private $inParens;
- private $eatWhiteDefault;
- private $buffer;
- private $utf8;
- private $encoding;
- private $patternModifiers;
- /**
- * Constructor
- *
- * @api
- *
- * @param string $sourceName
- * @param integer $sourceIndex
- * @param string $encoding
- */
- public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8')
- {
- $this->sourceName = $sourceName ?: '(stdin)';
- $this->sourceIndex = $sourceIndex;
- $this->charset = null;
- $this->utf8 = !$encoding || strtolower($encoding) === 'utf-8';
- $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais';
- if (empty(self::$operatorPattern)) {
- self::$operatorPattern = '([*\\/%+-]|[!=]\\=|\\>\\=?|\\<\\=\\>|\\<\\=?|and|or)';
- $commentSingle = '\\/\\/';
- $commentMultiLeft = '\\/\\*';
- $commentMultiRight = '\\*\\/';
- self::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight;
- self::$whitePattern = $this->utf8 ? '/' . $commentSingle . '[^\\n]*\\s*|(' . self::$commentPattern . ')\\s*|\\s+/AisuS' : '/' . $commentSingle . '[^\\n]*\\s*|(' . self::$commentPattern . ')\\s*|\\s+/AisS';
- }
- }
- /**
- * Get source file name
- *
- * @api
- *
- * @return string
- */
- public function getSourceName()
- {
- return $this->sourceName;
- }
- /**
- * Throw parser error
- *
- * @api
- *
- * @param string $msg
- *
- * @throws \Leafo\ScssPhp\Exception\ParserException
- */
- public function throwParseError($msg = 'parse error')
- {
- list($line, ) = $this->getSourcePosition($this->count);
- $loc = empty($this->sourceName) ? "line: {$line}" : "{$this->sourceName} on line {$line}";
- if ($this->peek('(.*?)(
- |$)', $m, $this->count)) {
- throw new ParserException("{$msg}: failed at `{$m['1']}` {$loc}");
- }
- throw new ParserException("{$msg}: {$loc}");
- }
- /**
- * Parser buffer
- *
- * @api
- *
- * @param string $buffer
- *
- * @return \Leafo\ScssPhp\Block
- */
- public function parse($buffer)
- {
- $this->count = 0;
- $this->env = null;
- $this->inParens = false;
- $this->eatWhiteDefault = true;
- $this->buffer = rtrim($buffer, '�..');
- $this->saveEncoding();
- $this->extractLineNumbers($buffer);
- $this->pushBlock(null);
- // root block
- $this->whitespace();
- $this->pushBlock(null);
- $this->popBlock();
- while ($this->parseChunk()) {
- }
- if ($this->count !== strlen($this->buffer)) {
- $this->throwParseError();
- }
- if (!empty($this->env->parent)) {
- $this->throwParseError('unclosed block');
- }
- if ($this->charset) {
- array_unshift($this->env->children, $this->charset);
- }
- $this->env->isRoot = true;
- $this->restoreEncoding();
- return $this->env;
- }
- /**
- * Parse a value or value list
- *
- * @api
- *
- * @param string $buffer
- * @param string $out
- *
- * @return boolean
- */
- public function parseValue($buffer, &$out)
- {
- $this->count = 0;
- $this->env = null;
- $this->inParens = false;
- $this->eatWhiteDefault = true;
- $this->buffer = (string) $buffer;
- $this->saveEncoding();
- $list = $this->valueList($out);
- $this->restoreEncoding();
- return $list;
- }
- /**
- * Parse a selector or selector list
- *
- * @api
- *
- * @param string $buffer
- * @param string $out
- *
- * @return boolean
- */
- public function parseSelector($buffer, &$out)
- {
- $this->count = 0;
- $this->env = null;
- $this->inParens = false;
- $this->eatWhiteDefault = true;
- $this->buffer = (string) $buffer;
- $this->saveEncoding();
- $selector = $this->selectors($out);
- $this->restoreEncoding();
- return $selector;
- }
- /**
- * 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
- * parsed.
- *
- * 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 Compiler::keyword(). (All parse functions are
- * structured the same.)
- *
- * 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 Compiler::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, Compiler::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.
- *
- * @return boolean
- */
- protected function parseChunk()
- {
- $s = $this->seek();
- // the directives
- if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
- if ($this->literal('@at-root') && ($this->selectors($selector) || true) && ($this->map($with) || true) && $this->literal('{')) {
- $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
- $atRoot->selector = $selector;
- $atRoot->with = $with;
- return true;
- }
- $this->seek($s);
- if ($this->literal('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) {
- $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
- $media->queryList = $mediaQueryList[2];
- return true;
- }
- $this->seek($s);
- if ($this->literal('@mixin') && $this->keyword($mixinName) && ($this->argumentDef($args) || true) && $this->literal('{')) {
- $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
- $mixin->name = $mixinName;
- $mixin->args = $args;
- return true;
- }
- $this->seek($s);
- if ($this->literal('@include') && $this->keyword($mixinName) && ($this->literal('(') && ($this->argValues($argValues) || true) && $this->literal(')') || true) && ($this->end() || $this->literal('{') && ($hasBlock = true))) {
- $child = array(Type::T_INCLUDE, $mixinName, isset($argValues) ? $argValues : null, null);
- if (!empty($hasBlock)) {
- $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s);
- $include->child = $child;
- } else {
- $this->append($child, $s);
- }
- return true;
- }
- $this->seek($s);
- if ($this->literal('@scssphp-import-once') && $this->valueList($importPath) && $this->end()) {
- $this->append(array(Type::T_SCSSPHP_IMPORT_ONCE, $importPath), $s);
- return true;
- }
- $this->seek($s);
- if ($this->literal('@import') && $this->valueList($importPath) && $this->end()) {
- $this->append(array(Type::T_IMPORT, $importPath), $s);
- return true;
- }
- $this->seek($s);
- if ($this->literal('@import') && $this->url($importPath) && $this->end()) {
- $this->append(array(Type::T_IMPORT, $importPath), $s);
- return true;
- }
- $this->seek($s);
- if ($this->literal('@extend') && $this->selectors($selectors) && $this->end()) {
- // check for '!flag'
- $optional = $this->stripOptionalFlag($selectors);
- $this->append(array(Type::T_EXTEND, $selectors, $optional), $s);
- return true;
- }
- $this->seek($s);
- if ($this->literal('@function') && $this->keyword($fnName) && $this->argumentDef($args) && $this->literal('{')) {
- $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
- $func->name = $fnName;
- $func->args = $args;
- return true;
- }
- $this->seek($s);
- if ($this->literal('@break') && $this->end()) {
- $this->append(array(Type::T_BREAK), $s);
- return true;
- }
- $this->seek($s);
- if ($this->literal('@continue') && $this->end()) {
- $this->append(array(Type::T_CONTINUE), $s);
- return true;
- }
- $this->seek($s);
- if ($this->literal('@return') && ($this->valueList($retVal) || true) && $this->end()) {
- $this->append(array(Type::T_RETURN, isset($retVal) ? $retVal : array(Type::T_NULL)), $s);
- return true;
- }
- $this->seek($s);
- if ($this->literal('@each') && $this->genericList($varNames, 'variable', ',', false) && $this->literal('in') && $this->valueList($list) && $this->literal('{')) {
- $each = $this->pushSpecialBlock(Type::T_EACH, $s);
- foreach ($varNames[2] as $varName) {
- $each->vars[] = $varName[1];
- }
- $each->list = $list;
- return true;
- }
- $this->seek($s);
- if ($this->literal('@while') && $this->expression($cond) && $this->literal('{')) {
- $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
- $while->cond = $cond;
- return true;
- }
- $this->seek($s);
- if ($this->literal('@for') && $this->variable($varName) && $this->literal('from') && $this->expression($start) && ($this->literal('through') || ($forUntil = true && $this->literal('to'))) && $this->expression($end) && $this->literal('{')) {
- $for = $this->pushSpecialBlock(Type::T_FOR, $s);
- $for->var = $varName[1];
- $for->start = $start;
- $for->end = $end;
- $for->until = isset($forUntil);
- return true;
- }
- $this->seek($s);
- if ($this->literal('@if') && $this->valueList($cond) && $this->literal('{')) {
- $if = $this->pushSpecialBlock(Type::T_IF, $s);
- $if->cond = $cond;
- $if->cases = array();
- return true;
- }
- $this->seek($s);
- if ($this->literal('@debug') && $this->valueList($value) && $this->end()) {
- $this->append(array(Type::T_DEBUG, $value), $s);
- return true;
- }
- $this->seek($s);
- if ($this->literal('@warn') && $this->valueList($value) && $this->end()) {
- $this->append(array(Type::T_WARN, $value), $s);
- return true;
- }
- $this->seek($s);
- if ($this->literal('@error') && $this->valueList($value) && $this->end()) {
- $this->append(array(Type::T_ERROR, $value), $s);
- return true;
- }
- $this->seek($s);
- if ($this->literal('@content') && $this->end()) {
- $this->append(array(Type::T_MIXIN_CONTENT), $s);
- return true;
- }
- $this->seek($s);
- $last = $this->last();
- if (isset($last) && $last[0] === Type::T_IF) {
- list(, $if) = $last;
- if ($this->literal('@else')) {
- if ($this->literal('{')) {
- $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
- } elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) {
- $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
- $else->cond = $cond;
- }
- if (isset($else)) {
- $else->dontAppend = true;
- $if->cases[] = $else;
- return true;
- }
- }
- $this->seek($s);
- }
- // only retain the first @charset directive encountered
- if ($this->literal('@charset') && $this->valueList($charset) && $this->end()) {
- if (!isset($this->charset)) {
- $statement = array(Type::T_CHARSET, $charset);
- list($line, $column) = $this->getSourcePosition($s);
- $statement[self::SOURCE_LINE] = $line;
- $statement[self::SOURCE_COLUMN] = $column;
- $statement[self::SOURCE_INDEX] = $this->sourceIndex;
- $this->charset = $statement;
- }
- return true;
- }
- $this->seek($s);
- // doesn't match built in directive, do generic one
- if ($this->literal('@', false) && $this->keyword($dirName) && ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) && $this->literal('{')) {
- if ($dirName === 'media') {
- $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
- } else {
- $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
- $directive->name = $dirName;
- }
- if (isset($dirValue)) {
- $directive->value = $dirValue;
- }
- return true;
- }
- $this->seek($s);
- return false;
- }
- // property shortcut
- // captures most properties before having to parse a selector
- if ($this->keyword($name, false) && $this->literal(': ') && $this->valueList($value) && $this->end()) {
- $name = array(Type::T_STRING, '', array($name));
- $this->append(array(Type::T_ASSIGN, $name, $value), $s);
- return true;
- }
- $this->seek($s);
- // variable assigns
- if ($this->variable($name) && $this->literal(':') && $this->valueList($value) && $this->end()) {
- // check for '!flag'
- $assignmentFlags = $this->stripAssignmentFlags($value);
- $this->append(array(Type::T_ASSIGN, $name, $value, $assignmentFlags), $s);
- return true;
- }
- $this->seek($s);
- // misc
- if ($this->literal('-->')) {
- return true;
- }
- // opening css block
- if ($this->selectors($selectors) && $this->literal('{')) {
- $this->pushBlock($selectors, $s);
- return true;
- }
- $this->seek($s);
- // property assign, or nested assign
- if ($this->propertyName($name) && $this->literal(':')) {
- $foundSomething = false;
- if ($this->valueList($value)) {
- $this->append(array(Type::T_ASSIGN, $name, $value), $s);
- $foundSomething = true;
- }
- if ($this->literal('{')) {
- $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
- $propBlock->prefix = $name;
- $foundSomething = true;
- } elseif ($foundSomething) {
- $foundSomething = $this->end();
- }
- if ($foundSomething) {
- return true;
- }
- }
- $this->seek($s);
- // closing a block
- if ($this->literal('}')) {
- $block = $this->popBlock();
- if (isset($block->type) && $block->type === Type::T_INCLUDE) {
- $include = $block->child;
- unset($block->child);
- $include[3] = $block;
- $this->append($include, $s);
- } elseif (empty($block->dontAppend)) {
- $type = isset($block->type) ? $block->type : Type::T_BLOCK;
- $this->append(array($type, $block), $s);
- }
- return true;
- }
- // extra stuff
- if ($this->literal(';') || $this->literal('<!--')) {
- return true;
- }
- return false;
- }
- /**
- * Push block onto parse tree
- *
- * @param array $selectors
- * @param integer $pos
- *
- * @return \Leafo\ScssPhp\Block
- */
- protected function pushBlock($selectors, $pos = 0)
- {
- list($line, $column) = $this->getSourcePosition($pos);
- $b = new Block();
- $b->sourceLine = $line;
- $b->sourceColumn = $column;
- $b->sourceIndex = $this->sourceIndex;
- $b->selectors = $selectors;
- $b->comments = array();
- $b->parent = $this->env;
- if (!$this->env) {
- $b->children = array();
- } elseif (empty($this->env->children)) {
- $this->env->children = $this->env->comments;
- $b->children = array();
- $this->env->comments = array();
- } else {
- $b->children = $this->env->comments;
- $this->env->comments = array();
- }
- $this->env = $b;
- return $b;
- }
- /**
- * Push special (named) block onto parse tree
- *
- * @param string $type
- * @param integer $pos
- *
- * @return \Leafo\ScssPhp\Block
- */
- protected function pushSpecialBlock($type, $pos)
- {
- $block = $this->pushBlock(null, $pos);
- $block->type = $type;
- return $block;
- }
- /**
- * Pop scope and return last block
- *
- * @return \Leafo\ScssPhp\Block
- *
- * @throws \Exception
- */
- protected function popBlock()
- {
- $block = $this->env;
- if (empty($block->parent)) {
- $this->throwParseError('unexpected }');
- }
- $this->env = $block->parent;
- unset($block->parent);
- $comments = $block->comments;
- if (count($comments)) {
- $this->env->comments = $comments;
- unset($block->comments);
- }
- return $block;
- }
- /**
- * Peek input stream
- *
- * @param string $regex
- * @param array $out
- * @param integer $from
- *
- * @return integer
- */
- protected function peek($regex, &$out, $from = null)
- {
- if (!isset($from)) {
- $from = $this->count;
- }
- $r = '/' . $regex . '/' . $this->patternModifiers;
- $result = preg_match($r, $this->buffer, $out, null, $from);
- return $result;
- }
- /**
- * Seek to position in input stream (or return current position in input stream)
- *
- * @param integer $where
- *
- * @return integer
- */
- protected function seek($where = null)
- {
- if ($where === null) {
- return $this->count;
- }
- $this->count = $where;
- return true;
- }
- /**
- * Match string looking for either ending delim, escape, or string interpolation
- *
- * {@internal This is a workaround for preg_match's 250K string match limit. }}
- *
- * @param array $m Matches (passed by reference)
- * @param string $delim Delimeter
- *
- * @return boolean True if match; false otherwise
- */
- protected function matchString(&$m, $delim)
- {
- $token = null;
- $end = strlen($this->buffer);
- // look for either ending delim, escape, or string interpolation
- foreach (array('#{', '\\', $delim) as $lookahead) {
- $pos = strpos($this->buffer, $lookahead, $this->count);
- if ($pos !== false && $pos < $end) {
- $end = $pos;
- $token = $lookahead;
- }
- }
- if (!isset($token)) {
- return false;
- }
- $match = substr($this->buffer, $this->count, $end - $this->count);
- $m = array($match . $token, $match, $token);
- $this->count = $end + strlen($token);
- return true;
- }
- /**
- * Try to match something on head of buffer
- *
- * @param string $regex
- * @param array $out
- * @param boolean $eatWhitespace
- *
- * @return boolean
- */
- protected function match($regex, &$out, $eatWhitespace = null)
- {
- if (!isset($eatWhitespace)) {
- $eatWhitespace = $this->eatWhiteDefault;
- }
- $r = '/' . $regex . '/' . $this->patternModifiers;
- if (preg_match($r, $this->buffer, $out, null, $this->count)) {
- $this->count += strlen($out[0]);
- if ($eatWhitespace) {
- $this->whitespace();
- }
- return true;
- }
- return false;
- }
- /**
- * Match literal string
- *
- * @param string $what
- * @param boolean $eatWhitespace
- *
- * @return boolean
- */
- protected function literal($what, $eatWhitespace = null)
- {
- if (!isset($eatWhitespace)) {
- $eatWhitespace = $this->eatWhiteDefault;
- }
- $len = strlen($what);
- if (strcasecmp(substr($this->buffer, $this->count, $len), $what) === 0) {
- $this->count += $len;
- if ($eatWhitespace) {
- $this->whitespace();
- }
- return true;
- }
- return false;
- }
- /**
- * Match some whitespace
- *
- * @return boolean
- */
- protected function whitespace()
- {
- $gotWhite = false;
- while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
- if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
- $this->appendComment(array(Type::T_COMMENT, $m[1]));
- $this->commentsSeen[$this->count] = true;
- }
- $this->count += strlen($m[0]);
- $gotWhite = true;
- }
- return $gotWhite;
- }
- /**
- * Append comment to current block
- *
- * @param array $comment
- */
- protected function appendComment($comment)
- {
- $comment[1] = substr(preg_replace(array('/^\\s+/m', '/^(.)/m'), array('', ' \\1'), $comment[1]), 1);
- $this->env->comments[] = $comment;
- }
- /**
- * Append statement to current block
- *
- * @param array $statement
- * @param integer $pos
- */
- protected function append($statement, $pos = null)
- {
- if ($pos !== null) {
- list($line, $column) = $this->getSourcePosition($pos);
- $statement[self::SOURCE_LINE] = $line;
- $statement[self::SOURCE_COLUMN] = $column;
- $statement[self::SOURCE_INDEX] = $this->sourceIndex;
- }
- $this->env->children[] = $statement;
- $comments = $this->env->comments;
- if (count($comments)) {
- $this->env->children = array_merge($this->env->children, $comments);
- $this->env->comments = array();
- }
- }
- /**
- * Returns last child was appended
- *
- * @return array|null
- */
- protected function last()
- {
- $i = count($this->env->children) - 1;
- if (isset($this->env->children[$i])) {
- return $this->env->children[$i];
- }
- }
- /**
- * Parse media query list
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function mediaQueryList(&$out)
- {
- return $this->genericList($out, 'mediaQuery', ',', false);
- }
- /**
- * Parse media query
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function mediaQuery(&$out)
- {
- $expressions = null;
- $parts = array();
- if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) && $this->mixedKeyword($mediaType)) {
- $prop = array(Type::T_MEDIA_TYPE);
- if (isset($only)) {
- $prop[] = array(Type::T_KEYWORD, 'only');
- }
- if (isset($not)) {
- $prop[] = array(Type::T_KEYWORD, 'not');
- }
- $media = array(Type::T_LIST, '', array());
- foreach ((array) $mediaType as $type) {
- if (is_array($type)) {
- $media[2][] = $type;
- } else {
- $media[2][] = array(Type::T_KEYWORD, $type);
- }
- }
- $prop[] = $media;
- $parts[] = $prop;
- }
- if (empty($parts) || $this->literal('and')) {
- $this->genericList($expressions, 'mediaExpression', 'and', false);
- if (is_array($expressions)) {
- $parts = array_merge($parts, $expressions[2]);
- }
- }
- $out = $parts;
- return true;
- }
- /**
- * Parse media expression
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function mediaExpression(&$out)
- {
- $s = $this->seek();
- $value = null;
- if ($this->literal('(') && $this->expression($feature) && ($this->literal(':') && $this->expression($value) || true) && $this->literal(')')) {
- $out = array(Type::T_MEDIA_EXPRESSION, $feature);
- if ($value) {
- $out[] = $value;
- }
- return true;
- }
- $this->seek($s);
- return false;
- }
- /**
- * Parse argument values
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function argValues(&$out)
- {
- if ($this->genericList($list, 'argValue', ',', false)) {
- $out = $list[2];
- return true;
- }
- return false;
- }
- /**
- * Parse argument value
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function argValue(&$out)
- {
- $s = $this->seek();
- $keyword = null;
- if (!$this->variable($keyword) || !$this->literal(':')) {
- $this->seek($s);
- $keyword = null;
- }
- if ($this->genericList($value, 'expression')) {
- $out = array($keyword, $value, false);
- $s = $this->seek();
- if ($this->literal('...')) {
- $out[2] = true;
- } else {
- $this->seek($s);
- }
- return true;
- }
- return false;
- }
- /**
- * Parse comma separated value list
- *
- * @param string $out
- *
- * @return boolean
- */
- protected function valueList(&$out)
- {
- return $this->genericList($out, 'spaceList', ',');
- }
- /**
- * Parse space separated value list
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function spaceList(&$out)
- {
- return $this->genericList($out, 'expression');
- }
- /**
- * Parse generic list
- *
- * @param array $out
- * @param callable $parseItem
- * @param string $delim
- * @param boolean $flatten
- *
- * @return boolean
- */
- protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
- {
- $s = $this->seek();
- $items = array();
- while ($this->{$parseItem}($value)) {
- $items[] = $value;
- if ($delim) {
- if (!$this->literal($delim)) {
- break;
- }
- }
- }
- if (count($items) === 0) {
- $this->seek($s);
- return false;
- }
- if ($flatten && count($items) === 1) {
- $out = $items[0];
- } else {
- $out = array(Type::T_LIST, $delim, $items);
- }
- return true;
- }
- /**
- * Parse expression
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function expression(&$out)
- {
- $s = $this->seek();
- if ($this->literal('(')) {
- if ($this->literal(')')) {
- $out = array(Type::T_LIST, '', array());
- return true;
- }
- if ($this->valueList($out) && $this->literal(')') && $out[0] === Type::T_LIST) {
- return true;
- }
- $this->seek($s);
- if ($this->map($out)) {
- return true;
- }
- $this->seek($s);
- }
- if ($this->value($lhs)) {
- $out = $this->expHelper($lhs, 0);
- return true;
- }
- return false;
- }
- /**
- * Parse left-hand side of subexpression
- *
- * @param array $lhs
- * @param integer $minP
- *
- * @return array
- */
- protected function expHelper($lhs, $minP)
- {
- $operators = self::$operatorPattern;
- $ss = $this->seek();
- $whiteBefore = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]);
- while ($this->match($operators, $m, false) && self::$precedence[$m[1]] >= $minP) {
- $whiteAfter = isset($this->buffer[$this->count]) && ctype_space($this->buffer[$this->count]);
- $varAfter = isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '$';
- $this->whitespace();
- $op = $m[1];
- // don't turn negative numbers into expressions
- if ($op === '-' && $whiteBefore && !$whiteAfter && !$varAfter) {
- break;
- }
- if (!$this->value($rhs)) {
- break;
- }
- // peek and see if rhs belongs to next operator
- if ($this->peek($operators, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) {
- $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
- }
- $lhs = array(Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter);
- $ss = $this->seek();
- $whiteBefore = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]);
- }
- $this->seek($ss);
- return $lhs;
- }
- /**
- * Parse value
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function value(&$out)
- {
- $s = $this->seek();
- if ($this->literal('not', false) && $this->whitespace() && $this->value($inner)) {
- $out = array(Type::T_UNARY, 'not', $inner, $this->inParens);
- return true;
- }
- $this->seek($s);
- if ($this->literal('not', false) && $this->parenValue($inner)) {
- $out = array(Type::T_UNARY, 'not', $inner, $this->inParens);
- return true;
- }
- $this->seek($s);
- if ($this->literal('+') && $this->value($inner)) {
- $out = array(Type::T_UNARY, '+', $inner, $this->inParens);
- return true;
- }
- $this->seek($s);
- // negation
- if ($this->literal('-', false) && ($this->variable($inner) || $this->unit($inner) || $this->parenValue($inner))) {
- $out = array(Type::T_UNARY, '-', $inner, $this->inParens);
- return true;
- }
- $this->seek($s);
- if ($this->parenValue($out) || $this->interpolation($out) || $this->variable($out) || $this->color($out) || $this->unit($out) || $this->string($out) || $this->func($out) || $this->progid($out)) {
- return true;
- }
- if ($this->keyword($keyword)) {
- if ($keyword === 'null') {
- $out = array(Type::T_NULL);
- } else {
- $out = array(Type::T_KEYWORD, $keyword);
- }
- return true;
- }
- return false;
- }
- /**
- * Parse parenthesized value
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function parenValue(&$out)
- {
- $s = $this->seek();
- $inParens = $this->inParens;
- if ($this->literal('(')) {
- if ($this->literal(')')) {
- $out = array(Type::T_LIST, '', array());
- return true;
- }
- $this->inParens = true;
- if ($this->expression($exp) && $this->literal(')')) {
- $out = $exp;
- $this->inParens = $inParens;
- return true;
- }
- }
- $this->inParens = $inParens;
- $this->seek($s);
- return false;
- }
- /**
- * Parse "progid:"
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function progid(&$out)
- {
- $s = $this->seek();
- if ($this->literal('progid:', false) && $this->openString('(', $fn) && $this->literal('(')) {
- $this->openString(')', $args, '(');
- if ($this->literal(')')) {
- $out = array(Type::T_STRING, '', array('progid:', $fn, '(', $args, ')'));
- return true;
- }
- }
- $this->seek($s);
- return false;
- }
- /**
- * Parse function call
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function func(&$func)
- {
- $s = $this->seek();
- if ($this->keyword($name, false) && $this->literal('(')) {
- if ($name === 'alpha' && $this->argumentList($args)) {
- $func = array(Type::T_FUNCTION, $name, array(Type::T_STRING, '', $args));
- return true;
- }
- if ($name !== 'expression' && !preg_match('/^(-[a-z]+-)?calc$/', $name)) {
- $ss = $this->seek();
- if ($this->argValues($args) && $this->literal(')')) {
- $func = array(Type::T_FUNCTION_CALL, $name, $args);
- return true;
- }
- $this->seek($ss);
- }
- if (($this->openString(')', $str, '(') || true) && $this->literal(')')) {
- $args = array();
- if (!empty($str)) {
- $args[] = array(null, array(Type::T_STRING, '', array($str)));
- }
- $func = array(Type::T_FUNCTION_CALL, $name, $args);
- return true;
- }
- }
- $this->seek($s);
- return false;
- }
- /**
- * Parse function call argument list
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function argumentList(&$out)
- {
- $s = $this->seek();
- $this->literal('(');
- $args = array();
- while ($this->keyword($var)) {
- if ($this->literal('=') && $this->expression($exp)) {
- $args[] = array(Type::T_STRING, '', array($var . '='));
- $arg = $exp;
- } else {
- break;
- }
- $args[] = $arg;
- if (!$this->literal(',')) {
- break;
- }
- $args[] = array(Type::T_STRING, '', array(', '));
- }
- if (!$this->literal(')') || !count($args)) {
- $this->seek($s);
- return false;
- }
- $out = $args;
- return true;
- }
- /**
- * Parse mixin/function definition argument list
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function argumentDef(&$out)
- {
- $s = $this->seek();
- $this->literal('(');
- $args = array();
- while ($this->variable($var)) {
- $arg = array($var[1], null, false);
- $ss = $this->seek();
- if ($this->literal(':') && $this->genericList($defaultVal, 'expression')) {
- $arg[1] = $defaultVal;
- } else {
- $this->seek($ss);
- }
- $ss = $this->seek();
- if ($this->literal('...')) {
- $sss = $this->seek();
- if (!$this->literal(')')) {
- $this->throwParseError('... has to be after the final argument');
- }
- $arg[2] = true;
- $this->seek($sss);
- } else {
- $this->seek($ss);
- }
- $args[] = $arg;
- if (!$this->literal(',')) {
- break;
- }
- }
- if (!$this->literal(')')) {
- $this->seek($s);
- return false;
- }
- $out = $args;
- return true;
- }
- /**
- * Parse map
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function map(&$out)
- {
- $s = $this->seek();
- if (!$this->literal('(')) {
- return false;
- }
- $keys = array();
- $values = array();
- while ($this->genericList($key, 'expression') && $this->literal(':') && $this->genericList($value, 'expression')) {
- $keys[] = $key;
- $values[] = $value;
- if (!$this->literal(',')) {
- break;
- }
- }
- if (!count($keys) || !$this->literal(')')) {
- $this->seek($s);
- return false;
- }
- $out = array(Type::T_MAP, $keys, $values);
- return true;
- }
- /**
- * Parse color
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function color(&$out)
- {
- $color = array(Type::T_COLOR);
- if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
- if (isset($m[3])) {
- $num = hexdec($m[3]);
- foreach (array(3, 2, 1) as $i) {
- $t = $num & 15;
- $color[$i] = $t << 4 | $t;
- $num >>= 4;
- }
- } else {
- $num = hexdec($m[2]);
- foreach (array(3, 2, 1) as $i) {
- $color[$i] = $num & 255;
- $num >>= 8;
- }
- }
- $out = $color;
- return true;
- }
- return false;
- }
- /**
- * Parse number with unit
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function unit(&$unit)
- {
- if ($this->match('([0-9]*(\\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
- $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
- return true;
- }
- return false;
- }
- /**
- * Parse string
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function string(&$out)
- {
- $s = $this->seek();
- if ($this->literal('"', false)) {
- $delim = '"';
- } elseif ($this->literal('\'', false)) {
- $delim = '\'';
- } else {
- return false;
- }
- $content = array();
- $oldWhite = $this->eatWhiteDefault;
- $this->eatWhiteDefault = false;
- $hasInterpolation = false;
- while ($this->matchString($m, $delim)) {
- if ($m[1] !== '') {
- $content[] = $m[1];
- }
- if ($m[2] === '#{') {
- $this->count -= strlen($m[2]);
- if ($this->interpolation($inter, false)) {
- $content[] = $inter;
- $hasInterpolation = true;
- } else {
- $this->count += strlen($m[2]);
- $content[] = '#{';
- }
- } elseif ($m[2] === '\\') {
- if ($this->literal('"', false)) {
- $content[] = $m[2] . '"';
- } elseif ($this->literal('\'', false)) {
- $content[] = $m[2] . '\'';
- } else {
- $content[] = $m[2];
- }
- } else {
- $this->count -= strlen($delim);
- break;
- }
- }
- $this->eatWhiteDefault = $oldWhite;
- if ($this->literal($delim)) {
- if ($hasInterpolation) {
- $delim = '"';
- foreach ($content as &$string) {
- if ($string === '\\\'') {
- $string = '\'';
- } elseif ($string === '\\"') {
- $string = '"';
- }
- }
- }
- $out = array(Type::T_STRING, $delim, $content);
- return true;
- }
- $this->seek($s);
- return false;
- }
- /**
- * Parse keyword or interpolation
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function mixedKeyword(&$out)
- {
- $parts = array();
- $oldWhite = $this->eatWhiteDefault;
- $this->eatWhiteDefault = false;
- for (;;) {
- if ($this->keyword($key)) {
- $parts[] = $key;
- continue;
- }
- if ($this->interpolation($inter)) {
- $parts[] = $inter;
- continue;
- }
- break;
- }
- $this->eatWhiteDefault = $oldWhite;
- if (count($parts) === 0) {
- return false;
- }
- if ($this->eatWhiteDefault) {
- $this->whitespace();
- }
- $out = $parts;
- return true;
- }
- /**
- * Parse an unbounded string stopped by $end
- *
- * @param string $end
- * @param array $out
- * @param string $nestingOpen
- *
- * @return boolean
- */
- protected function openString($end, &$out, $nestingOpen = null)
- {
- $oldWhite = $this->eatWhiteDefault;
- $this->eatWhiteDefault = false;
- $patt = '(.*?)([\'"]|#\\{|' . $this->pregQuote($end) . '|' . self::$commentPattern . ')';
- $nestingLevel = 0;
- $content = array();
- while ($this->match($patt, $m, false)) {
- if (isset($m[1]) && $m[1] !== '') {
- $content[] = $m[1];
- if ($nestingOpen) {
- $nestingLevel += substr_count($m[1], $nestingOpen);
- }
- }
- $tok = $m[2];
- $this->count -= strlen($tok);
- if ($tok === $end && !$nestingLevel--) {
- break;
- }
- if (($tok === '\'' || $tok === '"') && $this->string($str)) {
- $content[] = $str;
- continue;
- }
- if ($tok === '#{' && $this->interpolation($inter)) {
- $content[] = $inter;
- continue;
- }
- $content[] = $tok;
- $this->count += strlen($tok);
- }
- $this->eatWhiteDefault = $oldWhite;
- if (count($content) === 0) {
- return false;
- }
- // trim the end
- if (is_string(end($content))) {
- $content[count($content) - 1] = rtrim(end($content));
- }
- $out = array(Type::T_STRING, '', $content);
- return true;
- }
- /**
- * Parser interpolation
- *
- * @param array $out
- * @param boolean $lookWhite save information about whitespace before and after
- *
- * @return boolean
- */
- protected function interpolation(&$out, $lookWhite = true)
- {
- $oldWhite = $this->eatWhiteDefault;
- $this->eatWhiteDefault = true;
- $s = $this->seek();
- if ($this->literal('#{') && $this->valueList($value) && $this->literal('}', false)) {
- if ($lookWhite) {
- $left = preg_match('/\\s/', $this->buffer[$s - 1]) ? ' ' : '';
- $right = preg_match('/\\s/', $this->buffer[$this->count]) ? ' ' : '';
- } else {
- $left = $right = false;
- }
- $out = array(Type::T_INTERPOLATE, $value, $left, $right);
- $this->eatWhiteDefault = $oldWhite;
- if ($this->eatWhiteDefault) {
- $this->whitespace();
- }
- return true;
- }
- $this->seek($s);
- $this->eatWhiteDefault = $oldWhite;
- return false;
- }
- /**
- * Parse property name (as an array of parts or a string)
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function propertyName(&$out)
- {
- $parts = array();
- $oldWhite = $this->eatWhiteDefault;
- $this->eatWhiteDefault = false;
- for (;;) {
- if ($this->interpolation($inter)) {
- $parts[] = $inter;
- continue;
- }
- if ($this->keyword($text)) {
- $parts[] = $text;
- continue;
- }
- if (count($parts) === 0 && $this->match('[:.#]', $m, false)) {
- // css hacks
- $parts[] = $m[0];
- continue;
- }
- break;
- }
- $this->eatWhiteDefault = $oldWhite;
- if (count($parts) === 0) {
- return false;
- }
- // match comment hack
- if (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
- if (!empty($m[0])) {
- $parts[] = $m[0];
- $this->count += strlen($m[0]);
- }
- }
- $this->whitespace();
- // get any extra whitespace
- $out = array(Type::T_STRING, '', $parts);
- return true;
- }
- /**
- * Parse comma separated selector list
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function selectors(&$out)
- {
- $s = $this->seek();
- $selectors = array();
- while ($this->selector($sel)) {
- $selectors[] = $sel;
- if (!$this->literal(',')) {
- break;
- }
- while ($this->literal(',')) {
- }
- }
- if (count($selectors) === 0) {
- $this->seek($s);
- return false;
- }
- $out = $selectors;
- return true;
- }
- /**
- * Parse whitespace separated selector list
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function selector(&$out)
- {
- $selector = array();
- for (;;) {
- if ($this->match('[>+~]+', $m)) {
- $selector[] = array($m[0]);
- continue;
- }
- if ($this->selectorSingle($part)) {
- $selector[] = $part;
- $this->match('\\s+', $m);
- continue;
- }
- if ($this->match('\\/[^\\/]+\\/', $m)) {
- $selector[] = array($m[0]);
- continue;
- }
- break;
- }
- if (count($selector) === 0) {
- return false;
- }
- $out = $selector;
- return true;
- }
- /**
- * Parse the parts that make up a selector
- *
- * {@internal
- * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
- * }}
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function selectorSingle(&$out)
- {
- $oldWhite = $this->eatWhiteDefault;
- $this->eatWhiteDefault = false;
- $parts = array();
- if ($this->literal('*', false)) {
- $parts[] = '*';
- }
- for (;;) {
- // see if we can stop early
- if ($this->match('\\s*[{,]', $m)) {
- $this->count--;
- break;
- }
- $s = $this->seek();
- // self
- if ($this->literal('&', false)) {
- $parts[] = Compiler::$selfSelector;
- continue;
- }
- if ($this->literal('.', false)) {
- $parts[] = '.';
- continue;
- }
- if ($this->literal('|', false)) {
- $parts[] = '|';
- continue;
- }
- if ($this->match('\\\\\\S', $m)) {
- $parts[] = $m[0];
- continue;
- }
- // for keyframes
- if ($this->unit($unit)) {
- $parts[] = $unit;
- continue;
- }
- if ($this->keyword($name)) {
- $parts[] = $name;
- continue;
- }
- if ($this->interpolation($inter)) {
- $parts[] = $inter;
- continue;
- }
- if ($this->literal('%', false) && $this->placeholder($placeholder)) {
- $parts[] = '%';
- $parts[] = $placeholder;
- continue;
- }
- if ($this->literal('#', false)) {
- $parts[] = '#';
- continue;
- }
- // a pseudo selector
- if ($this->match('::?', $m) && $this->mixedKeyword($nameParts)) {
- $parts[] = $m[0];
- foreach ($nameParts as $sub) {
- $parts[] = $sub;
- }
- $ss = $this->seek();
- if ($this->literal('(') && ($this->openString(')', $str, '(') || true) && $this->literal(')')) {
- $parts[] = '(';
- if (!empty($str)) {
- $parts[] = $str;
- }
- $parts[] = ')';
- } else {
- $this->seek($ss);
- }
- continue;
- }
- $this->seek($s);
- // attribute selector
- if ($this->literal('[') && ($this->openString(']', $str, '[') || true) && $this->literal(']')) {
- $parts[] = '[';
- if (!empty($str)) {
- $parts[] = $str;
- }
- $parts[] = ']';
- continue;
- }
- $this->seek($s);
- break;
- }
- $this->eatWhiteDefault = $oldWhite;
- if (count($parts) === 0) {
- return false;
- }
- $out = $parts;
- return true;
- }
- /**
- * Parse a variable
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function variable(&$out)
- {
- $s = $this->seek();
- if ($this->literal('$', false) && $this->keyword($name)) {
- $out = array(Type::T_VARIABLE, $name);
- return true;
- }
- $this->seek($s);
- return false;
- }
- /**
- * Parse a keyword
- *
- * @param string $word
- * @param boolean $eatWhitespace
- *
- * @return boolean
- */
- protected function keyword(&$word, $eatWhitespace = null)
- {
- if ($this->match($this->utf8 ? '(([\\pL\\w_\\-\\*!"\']|[\\\\].)([\\pL\\w\\-_"\']|[\\\\].)*)' : '(([\\w_\\-\\*!"\']|[\\\\].)([\\w\\-_"\']|[\\\\].)*)', $m, $eatWhitespace)) {
- $word = $m[1];
- return true;
- }
- return false;
- }
- /**
- * Parse a placeholder
- *
- * @param string $placeholder
- *
- * @return boolean
- */
- protected function placeholder(&$placeholder)
- {
- if ($this->match($this->utf8 ? '([\\pL\\w\\-_]+|#[{][$][\\pL\\w\\-_]+[}])' : '([\\w\\-_]+|#[{][$][\\w\\-_]+[}])', $m)) {
- $placeholder = $m[1];
- return true;
- }
- return false;
- }
- /**
- * Parse a url
- *
- * @param array $out
- *
- * @return boolean
- */
- protected function url(&$out)
- {
- if ($this->match('(url\\(\\s*(["\']?)([^)]+)\\2\\s*\\))', $m)) {
- $out = array(Type::T_STRING, '', array('url(' . $m[2] . $m[3] . $m[2] . ')'));
- return true;
- }
- return false;
- }
- /**
- * Consume an end of statement delimiter
- *
- * @return boolean
- */
- protected function end()
- {
- if ($this->literal(';')) {
- return true;
- }
- if ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
- // if there is end of file or a closing block next then we don't need a ;
- return true;
- }
- return false;
- }
- /**
- * Strip assignment flag from the list
- *
- * @param array $value
- *
- * @return array
- */
- protected function stripAssignmentFlags(&$value)
- {
- $flags = array();
- for ($token =& $value; $token[0] === Type::T_LIST && ($s = count($token[2])); $token =& $lastNode) {
- $lastNode =& $token[2][$s - 1];
- while ($lastNode[0] === Type::T_KEYWORD && in_array($lastNode[1], array('!default', '!global'))) {
- array_pop($token[2]);
- $node = end($token[2]);
- $token = $this->flattenList($token);
- $flags[] = $lastNode[1];
- $lastNode = $node;
- }
- }
- return $flags;
- }
- /**
- * Strip optional flag from selector list
- *
- * @param array $selectors
- *
- * @return string
- */
- protected function stripOptionalFlag(&$selectors)
- {
- $optional = false;
- $selector = end($selectors);
- $part = end($selector);
- if ($part === array('!optional')) {
- array_pop($selectors[count($selectors) - 1]);
- $optional = true;
- }
- return $optional;
- }
- /**
- * Turn list of length 1 into value type
- *
- * @param array $value
- *
- * @return array
- */
- protected function flattenList($value)
- {
- if ($value[0] === Type::T_LIST && count($value[2]) === 1) {
- return $this->flattenList($value[2][0]);
- }
- return $value;
- }
- /**
- * @deprecated
- *
- * {@internal
- * advance counter to next occurrence of $what
- * $until - don't include $what in advance
- * $allowNewline, if string, will be used as valid char set
- * }}
- */
- protected function to($what, &$out, $until = false, $allowNewline = false)
- {
- if (is_string($allowNewline)) {
- $validChars = $allowNewline;
- } else {
- $validChars = $allowNewline ? '.' : '[^
- ]';
- }
- if (!$this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, !$until)) {
- return false;
- }
- if ($until) {
- $this->count -= strlen($what);
- }
- $out = $m[1];
- return true;
- }
- /**
- * @deprecated
- */
- protected function show()
- {
- if ($this->peek('(.*?)(
- |$)', $m, $this->count)) {
- return $m[1];
- }
- return '';
- }
- /**
- * Quote regular expression
- *
- * @param string $what
- *
- * @return string
- */
- private function pregQuote($what)
- {
- return preg_quote($what, '/');
- }
- /**
- * Extract line numbers from buffer
- *
- * @param string $buffer
- */
- private function extractLineNumbers($buffer)
- {
- $this->sourcePositions = array(0 => 0);
- $prev = 0;
- while (($pos = strpos($buffer, '
- ', $prev)) !== false) {
- $this->sourcePositions[] = $pos;
- $prev = $pos + 1;
- }
- $this->sourcePositions[] = strlen($buffer);
- if (substr($buffer, -1) !== '
- ') {
- $this->sourcePositions[] = strlen($buffer) + 1;
- }
- }
- /**
- * Get source line number and column (given character position in the buffer)
- *
- * @param integer $pos
- *
- * @return integer
- */
- private function getSourcePosition($pos)
- {
- $low = 0;
- $high = count($this->sourcePositions);
- while ($low < $high) {
- $mid = (int) (($high + $low) / 2);
- if ($pos < $this->sourcePositions[$mid]) {
- $high = $mid - 1;
- continue;
- }
- if ($pos >= $this->sourcePositions[$mid + 1]) {
- $low = $mid + 1;
- continue;
- }
- return array($mid + 1, $pos - $this->sourcePositions[$mid]);
- }
- return array($low + 1, $pos - $this->sourcePositions[$low]);
- }
- /**
- * Save internal encoding
- */
- private function saveEncoding()
- {
- if (ini_get('mbstring.func_overload') & 2) {
- $this->encoding = mb_internal_encoding();
- mb_internal_encoding('iso-8859-1');
- }
- }
- /**
- * Restore internal encoding
- */
- private function restoreEncoding()
- {
- if ($this->encoding) {
- mb_internal_encoding($this->encoding);
- }
- }
- }
|