lessc_parser

The Jetpack by WordPress.com lessc parser class.

Defined (1)

The class is defined in the following location(s).

/modules/custom-css/custom-css/preprocessors/lessc.inc.php  
  1. class lessc_parser { 
  2. static protected $nextBlockId = 0; // used to uniquely identify blocks 
  3.  
  4. static protected $precedence = array( 
  5. '=<' => 0,  
  6. '>=' => 0,  
  7. '=' => 0,  
  8. '<' => 0,  
  9. '>' => 0,  
  10.  
  11. '+' => 1,  
  12. '-' => 1,  
  13. '*' => 2,  
  14. '/' => 2,  
  15. '%' => 2,  
  16. ); 
  17.  
  18. static protected $whitePattern; 
  19. static protected $commentMulti; 
  20.  
  21. static protected $commentSingle = "//"; 
  22. static protected $commentMultiLeft = "/*"; 
  23. static protected $commentMultiRight = "*/"; 
  24.  
  25. // regex string to match any of the operators 
  26. static protected $operatorString; 
  27.  
  28. // these properties will supress division unless it's inside parenthases 
  29. static protected $supressDivisionProps = 
  30. array('/border-radius$/i', '/^font$/i'); 
  31.  
  32. protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport"); 
  33. protected $lineDirectives = array("charset"); 
  34.  
  35. /** 
  36. * if we are in parens we can be more liberal with whitespace around 
  37. * operators because it must evaluate to a single value and thus is less 
  38. * ambiguous. 
  39. * Consider: 
  40. * property1: 10 -5; // is two numbers, 10 and -5 
  41. * property2: (10 -5); // should evaluate to 5 
  42. */ 
  43. protected $inParens = false; 
  44.  
  45. // caches preg escaped literals 
  46. static protected $literalCache = array(); 
  47.  
  48. public function __construct($lessc, $sourceName = null) { 
  49. $this->eatWhiteDefault = true; 
  50. // reference to less needed for vPrefix, mPrefix, and parentSelector 
  51. $this->lessc = $lessc; 
  52.  
  53. $this->sourceName = $sourceName; // name used for error messages 
  54.  
  55. $this->writeComments = false; 
  56.  
  57. if (!self::$operatorString) { 
  58. self::$operatorString = 
  59. '('.implode('|', array_map(array('lessc', 'preg_quote'),  
  60. array_keys(self::$precedence))).')'; 
  61.  
  62. $commentSingle = lessc::preg_quote(self::$commentSingle); 
  63. $commentMultiLeft = lessc::preg_quote(self::$commentMultiLeft); 
  64. $commentMultiRight = lessc::preg_quote(self::$commentMultiRight); 
  65.  
  66. self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight; 
  67. self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais'; 
  68.  
  69. public function parse($buffer) { 
  70. $this->count = 0; 
  71. $this->line = 1; 
  72.  
  73. $this->env = null; // block stack 
  74. $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer); 
  75. $this->pushSpecialBlock("root"); 
  76. $this->eatWhiteDefault = true; 
  77. $this->seenComments = array(); 
  78.  
  79. // trim whitespace on head 
  80. // if (preg_match('/^\s+/', $this->buffer, $m)) { 
  81. // $this->line += substr_count($m[0], "\n"); 
  82. // $this->buffer = ltrim($this->buffer); 
  83. // } 
  84. $this->whitespace(); 
  85.  
  86. // parse the entire file 
  87. while (false !== $this->parseChunk()); 
  88.  
  89. if ($this->count != strlen($this->buffer)) 
  90. $this->throwError(); 
  91.  
  92. // TODO report where the block was opened 
  93. if ( !property_exists($this->env, 'parent') || !is_null($this->env->parent) ) 
  94. throw new exception('parse error: unclosed block'); 
  95.  
  96. return $this->env; 
  97.  
  98. /** 
  99. * Parse a single chunk off the head of the buffer and append it to the 
  100. * current parse environment. 
  101. * Returns false when the buffer is empty, or when there is an error. 
  102. * This function is called repeatedly until the entire document is 
  103. * parsed. 
  104. * This parser is most similar to a recursive descent parser. Single 
  105. * functions represent discrete grammatical rules for the language, and 
  106. * they are able to capture the text that represents those rules. 
  107. * Consider the function lessc::keyword(). (all parse functions are 
  108. * structured the same) 
  109. * The function takes a single reference argument. When calling the 
  110. * function it will attempt to match a keyword on the head of the buffer. 
  111. * If it is successful, it will place the keyword in the referenced 
  112. * argument, advance the position in the buffer, and return true. If it 
  113. * fails then it won't advance the buffer and it will return false. 
  114. * All of these parse functions are powered by lessc::match(), which behaves 
  115. * the same way, but takes a literal regular expression. Sometimes it is 
  116. * more convenient to use match instead of creating a new function. 
  117. * Because of the format of the functions, to parse an entire string of 
  118. * grammatical rules, you can chain them together using &&. 
  119. * But, if some of the rules in the chain succeed before one fails, then 
  120. * the buffer position will be left at an invalid state. In order to 
  121. * avoid this, lessc::seek() is used to remember and set buffer positions. 
  122. * Before parsing a chain, use $s = $this->seek() to remember the current 
  123. * position into $s. Then if a chain fails, use $this->seek($s) to 
  124. * go back where we started. 
  125. */ 
  126. protected function parseChunk() { 
  127. if (empty($this->buffer)) return false; 
  128. $s = $this->seek(); 
  129.  
  130. if ($this->whitespace()) { 
  131. return true; 
  132.  
  133. // setting a property 
  134. if ($this->keyword($key) && $this->assign() && 
  135. $this->propertyValue($value, $key) && $this->end()) 
  136. $this->append(array('assign', $key, $value), $s); 
  137. return true; 
  138. } else { 
  139. $this->seek($s); 
  140.  
  141.  
  142. // look for special css blocks 
  143. if ($this->literal('@', false)) { 
  144. $this->count--; 
  145.  
  146. // media 
  147. if ($this->literal('@media')) { 
  148. if (($this->mediaQueryList($mediaQueries) || true) 
  149. && $this->literal('{')) 
  150. $media = $this->pushSpecialBlock("media"); 
  151. $media->queries = is_null($mediaQueries) ? array() : $mediaQueries; 
  152. return true; 
  153. } else { 
  154. $this->seek($s); 
  155. return false; 
  156.  
  157. if ($this->literal("@", false) && $this->keyword($dirName)) { 
  158. if ($this->isDirective($dirName, $this->blockDirectives)) { 
  159. if (($this->openString("{", $dirValue, null, array(";")) || true) && 
  160. $this->literal("{")) 
  161. $dir = $this->pushSpecialBlock("directive"); 
  162. $dir->name = $dirName; 
  163. if (isset($dirValue)) $dir->value = $dirValue; 
  164. return true; 
  165. } elseif ($this->isDirective($dirName, $this->lineDirectives)) { 
  166. if ($this->propertyValue($dirValue) && $this->end()) { 
  167. $this->append(array("directive", $dirName, $dirValue)); 
  168. return true; 
  169.  
  170. $this->seek($s); 
  171.  
  172. // setting a variable 
  173. if ($this->variable($var) && $this->assign() && 
  174. $this->propertyValue($value) && $this->end()) 
  175. $this->append(array('assign', $var, $value), $s); 
  176. return true; 
  177. } else { 
  178. $this->seek($s); 
  179.  
  180. if ($this->import($importValue)) { 
  181. $this->append($importValue, $s); 
  182. return true; 
  183.  
  184. // opening parametric mixin 
  185. if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) && 
  186. ($this->guards($guards) || true) && 
  187. $this->literal('{')) 
  188. $block = $this->pushBlock($this->fixTags(array($tag))); 
  189. $block->args = $args; 
  190. $block->isVararg = $isVararg; 
  191. if (!empty($guards)) $block->guards = $guards; 
  192. return true; 
  193. } else { 
  194. $this->seek($s); 
  195.  
  196. // opening a simple block 
  197. if ($this->tags($tags) && $this->literal('{', false)) { 
  198. $tags = $this->fixTags($tags); 
  199. $this->pushBlock($tags); 
  200. return true; 
  201. } else { 
  202. $this->seek($s); 
  203.  
  204. // closing a block 
  205. if ($this->literal('}', false)) { 
  206. try { 
  207. $block = $this->pop(); 
  208. } catch (exception $e) { 
  209. $this->seek($s); 
  210. $this->throwError($e->getMessage()); 
  211.  
  212. $hidden = false; 
  213. if (is_null($block->type)) { 
  214. $hidden = true; 
  215. if (!isset($block->args)) { 
  216. foreach ($block->tags as $tag) { 
  217. if (!is_string($tag) || $tag{0} != $this->lessc->mPrefix) { 
  218. $hidden = false; 
  219. break; 
  220.  
  221. foreach ($block->tags as $tag) { 
  222. if (is_string($tag)) { 
  223. $this->env->children[$tag][] = $block; 
  224.  
  225. if (!$hidden) { 
  226. $this->append(array('block', $block), $s); 
  227.  
  228. // this is done here so comments aren't bundled into he block that 
  229. // was just closed 
  230. $this->whitespace(); 
  231. return true; 
  232.  
  233. // mixin 
  234. if ($this->mixinTags($tags) && 
  235. ($this->argumentDef($argv, $isVararg) || true) && 
  236. ($this->keyword($suffix) || true) && $this->end()) 
  237. $tags = $this->fixTags($tags); 
  238. $this->append(array('mixin', $tags, $argv, $suffix), $s); 
  239. return true; 
  240. } else { 
  241. $this->seek($s); 
  242.  
  243. // spare ; 
  244. if ($this->literal(';')) return true; 
  245.  
  246. return false; // got nothing, throw error 
  247.  
  248. protected function isDirective($dirname, $directives) { 
  249. // TODO: cache pattern in parser 
  250. $pattern = implode("|",  
  251. array_map(array("lessc", "preg_quote"), $directives)); 
  252. $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i'; 
  253.  
  254. return preg_match($pattern, $dirname); 
  255.  
  256. protected function fixTags($tags) { 
  257. // move @ tags out of variable namespace 
  258. foreach ($tags as &$tag) { 
  259. if ($tag{0} == $this->lessc->vPrefix) 
  260. $tag[0] = $this->lessc->mPrefix; 
  261. return $tags; 
  262.  
  263. // a list of expressions 
  264. protected function expressionList(&$exps) { 
  265. $values = array(); 
  266.  
  267. while ($this->expression($exp)) { 
  268. $values[] = $exp; 
  269.  
  270. if (count($values) == 0) return false; 
  271.  
  272. $exps = lessc::compressList($values, ' '); 
  273. return true; 
  274.  
  275. /** 
  276. * Attempt to consume an expression. 
  277. * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code 
  278. */ 
  279. protected function expression(&$out) { 
  280. if ($this->value($lhs)) { 
  281. $out = $this->expHelper($lhs, 0); 
  282.  
  283. // look for / shorthand 
  284. if (!empty($this->env->supressedDivision)) { 
  285. unset($this->env->supressedDivision); 
  286. $s = $this->seek(); 
  287. if ($this->literal("/") && $this->value($rhs)) { 
  288. $out = array("list", "",  
  289. array($out, array("keyword", "/"), $rhs)); 
  290. } else { 
  291. $this->seek($s); 
  292.  
  293. return true; 
  294. return false; 
  295.  
  296. /** 
  297. * recursively parse infix equation with $lhs at precedence $minP 
  298. */ 
  299. protected function expHelper($lhs, $minP) { 
  300. $this->inExp = true; 
  301. $ss = $this->seek(); 
  302.  
  303. while (true) { 
  304. $whiteBefore = isset($this->buffer[$this->count - 1]) && 
  305. ctype_space($this->buffer[$this->count - 1]); 
  306.  
  307. // If there is whitespace before the operator, then we require 
  308. // whitespace after the operator for it to be an expression 
  309. $needWhite = $whiteBefore && !$this->inParens; 
  310.  
  311. if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) { 
  312. if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) { 
  313. foreach (self::$supressDivisionProps as $pattern) { 
  314. if (preg_match($pattern, $this->env->currentProperty)) { 
  315. $this->env->supressedDivision = true; 
  316. break 2; 
  317.  
  318.  
  319. $whiteAfter = isset($this->buffer[$this->count - 1]) && 
  320. ctype_space($this->buffer[$this->count - 1]); 
  321.  
  322. if (!$this->value($rhs)) break; 
  323.  
  324. // peek for next operator to see what to do with rhs 
  325. if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) { 
  326. $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]); 
  327.  
  328. $lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter); 
  329. $ss = $this->seek(); 
  330.  
  331. continue; 
  332.  
  333. break; 
  334.  
  335. $this->seek($ss); 
  336.  
  337. return $lhs; 
  338.  
  339. // consume a list of values for a property 
  340. public function propertyValue(&$value, $keyName = null) { 
  341. $values = array(); 
  342.  
  343. if ($keyName !== null) $this->env->currentProperty = $keyName; 
  344.  
  345. $s = null; 
  346. while ($this->expressionList($v)) { 
  347. $values[] = $v; 
  348. $s = $this->seek(); 
  349. if (!$this->literal(', ')) break; 
  350.  
  351. if ($s) $this->seek($s); 
  352.  
  353. if ($keyName !== null) unset($this->env->currentProperty); 
  354.  
  355. if (count($values) == 0) return false; 
  356.  
  357. $value = lessc::compressList($values, ', '); 
  358. return true; 
  359.  
  360. protected function parenValue(&$out) { 
  361. $s = $this->seek(); 
  362.  
  363. // speed shortcut 
  364. if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") { 
  365. return false; 
  366.  
  367. $inParens = $this->inParens; 
  368. if ($this->literal("(") && 
  369. ($this->inParens = true) && $this->expression($exp) && 
  370. $this->literal(")")) 
  371. $out = $exp; 
  372. $this->inParens = $inParens; 
  373. return true; 
  374. } else { 
  375. $this->inParens = $inParens; 
  376. $this->seek($s); 
  377.  
  378. return false; 
  379.  
  380. // a single value 
  381. protected function value(&$value) { 
  382. $s = $this->seek(); 
  383.  
  384. // speed shortcut 
  385. if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") { 
  386. // negation 
  387. if ($this->literal("-", false) && 
  388. (($this->variable($inner) && $inner = array("variable", $inner)) || 
  389. $this->unit($inner) || 
  390. $this->parenValue($inner))) 
  391. $value = array("unary", "-", $inner); 
  392. return true; 
  393. } else { 
  394. $this->seek($s); 
  395.  
  396. if ($this->parenValue($value)) return true; 
  397. if ($this->unit($value)) return true; 
  398. if ($this->color($value)) return true; 
  399. if ($this->func($value)) return true; 
  400. if ($this->string($value)) return true; 
  401.  
  402. if ($this->keyword($word)) { 
  403. $value = array('keyword', $word); 
  404. return true; 
  405.  
  406. // try a variable 
  407. if ($this->variable($var)) { 
  408. $value = array('variable', $var); 
  409. return true; 
  410.  
  411. // unquote string (should this work on any type? 
  412. if ($this->literal("~") && $this->string($str)) { 
  413. $value = array("escape", $str); 
  414. return true; 
  415. } else { 
  416. $this->seek($s); 
  417.  
  418. // css hack: \0 
  419. if ($this->literal('\\') && $this->match('([0-9]+)', $m)) { 
  420. $value = array('keyword', '\\'.$m[1]); 
  421. return true; 
  422. } else { 
  423. $this->seek($s); 
  424.  
  425. return false; 
  426.  
  427. // an import statement 
  428. protected function import(&$out) { 
  429. if (!$this->literal('@import')) return false; 
  430.  
  431. // @import "something.css" media; 
  432. // @import url("something.css") media; 
  433. // @import url(something.css) media; 
  434.  
  435. if ($this->propertyValue($value)) { 
  436. $out = array("import", $value); 
  437. return true; 
  438.  
  439. protected function mediaQueryList(&$out) { 
  440. if ($this->genericList($list, "mediaQuery", ", ", false)) { 
  441. $out = $list[2]; 
  442. return true; 
  443. return false; 
  444.  
  445. protected function mediaQuery(&$out) { 
  446. $s = $this->seek(); 
  447.  
  448. $expressions = null; 
  449. $parts = array(); 
  450.  
  451. if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) { 
  452. $prop = array("mediaType"); 
  453. if (isset($only)) $prop[] = "only"; 
  454. if (isset($not)) $prop[] = "not"; 
  455. $prop[] = $mediaType; 
  456. $parts[] = $prop; 
  457. } else { 
  458. $this->seek($s); 
  459.  
  460.  
  461. if (!empty($mediaType) && !$this->literal("and")) { 
  462. // ~ 
  463. } else { 
  464. $this->genericList($expressions, "mediaExpression", "and", false); 
  465. if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]); 
  466.  
  467. if (count($parts) == 0) { 
  468. $this->seek($s); 
  469. return false; 
  470.  
  471. $out = $parts; 
  472. return true; 
  473.  
  474. protected function mediaExpression(&$out) { 
  475. $s = $this->seek(); 
  476. $value = null; 
  477. if ($this->literal("(") && 
  478. $this->keyword($feature) && 
  479. ($this->literal(":") && $this->expression($value) || true) && 
  480. $this->literal(")")) 
  481. $out = array("mediaExp", $feature); 
  482. if ($value) $out[] = $value; 
  483. return true; 
  484. } elseif ($this->variable($variable)) { 
  485. $out = array('variable', $variable); 
  486. return true; 
  487.  
  488. $this->seek($s); 
  489. return false; 
  490.  
  491. // an unbounded string stopped by $end 
  492. protected function openString($end, &$out, $nestingOpen=null, $rejectStrs = null) { 
  493. $oldWhite = $this->eatWhiteDefault; 
  494. $this->eatWhiteDefault = false; 
  495.  
  496. $stop = array("'", '"', "@{", $end); 
  497. $stop = array_map(array("lessc", "preg_quote"), $stop); 
  498. // $stop[] = self::$commentMulti; 
  499.  
  500. if (!is_null($rejectStrs)) { 
  501. $stop = array_merge($stop, $rejectStrs); 
  502.  
  503. $patt = '(.*?)('.implode("|", $stop).')'; 
  504.  
  505. $nestingLevel = 0; 
  506.  
  507. $content = array(); 
  508. while ($this->match($patt, $m, false)) { 
  509. if (!empty($m[1])) { 
  510. $content[] = $m[1]; 
  511. if ($nestingOpen) { 
  512. $nestingLevel += substr_count($m[1], $nestingOpen); 
  513.  
  514. $tok = $m[2]; 
  515.  
  516. $this->count-= strlen($tok); 
  517. if ($tok == $end) { 
  518. if ($nestingLevel == 0) { 
  519. break; 
  520. } else { 
  521. $nestingLevel--; 
  522.  
  523. if (($tok == "'" || $tok == '"') && $this->string($str)) { 
  524. $content[] = $str; 
  525. continue; 
  526.  
  527. if ($tok == "@{" && $this->interpolation($inter)) { 
  528. $content[] = $inter; 
  529. continue; 
  530.  
  531. if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) { 
  532. break; 
  533.  
  534. $content[] = $tok; 
  535. $this->count+= strlen($tok); 
  536.  
  537. $this->eatWhiteDefault = $oldWhite; 
  538.  
  539. if (count($content) == 0) return false; 
  540.  
  541. // trim the end 
  542. if (is_string(end($content))) { 
  543. $content[count($content) - 1] = rtrim(end($content)); 
  544.  
  545. $out = array("string", "", $content); 
  546. return true; 
  547.  
  548. protected function string(&$out) { 
  549. $s = $this->seek(); 
  550. if ($this->literal('"', false)) { 
  551. $delim = '"'; 
  552. } elseif ($this->literal("'", false)) { 
  553. $delim = "'"; 
  554. } else { 
  555. return false; 
  556.  
  557. $content = array(); 
  558.  
  559. // look for either ending delim , escape, or string interpolation 
  560. $patt = '([^\n]*?)(@\{|\\\\|' . 
  561. lessc::preg_quote($delim).')'; 
  562.  
  563. $oldWhite = $this->eatWhiteDefault; 
  564. $this->eatWhiteDefault = false; 
  565.  
  566. while ($this->match($patt, $m, false)) { 
  567. $content[] = $m[1]; 
  568. if ($m[2] == "@{") { 
  569. $this->count -= strlen($m[2]); 
  570. if ($this->interpolation($inter, false)) { 
  571. $content[] = $inter; 
  572. } else { 
  573. $this->count += strlen($m[2]); 
  574. $content[] = "@{"; // ignore it 
  575. } elseif ($m[2] == '\\') { 
  576. $content[] = $m[2]; 
  577. if ($this->literal($delim, false)) { 
  578. $content[] = $delim; 
  579. } else { 
  580. $this->count -= strlen($delim); 
  581. break; // delim 
  582.  
  583. $this->eatWhiteDefault = $oldWhite; 
  584.  
  585. if ($this->literal($delim)) { 
  586. $out = array("string", $delim, $content); 
  587. return true; 
  588.  
  589. $this->seek($s); 
  590. return false; 
  591.  
  592. protected function interpolation(&$out) { 
  593. $oldWhite = $this->eatWhiteDefault; 
  594. $this->eatWhiteDefault = true; 
  595.  
  596. $s = $this->seek(); 
  597. if ($this->literal("@{") && 
  598. $this->openString("}", $interp, null, array("'", '"', ";")) && 
  599. $this->literal("}", false)) 
  600. $out = array("interpolate", $interp); 
  601. $this->eatWhiteDefault = $oldWhite; 
  602. if ($this->eatWhiteDefault) $this->whitespace(); 
  603. return true; 
  604.  
  605. $this->eatWhiteDefault = $oldWhite; 
  606. $this->seek($s); 
  607. return false; 
  608.  
  609. protected function unit(&$unit) { 
  610. // speed shortcut 
  611. if (isset($this->buffer[$this->count])) { 
  612. $char = $this->buffer[$this->count]; 
  613. if (!ctype_digit($char) && $char != ".") return false; 
  614.  
  615. if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) { 
  616. $unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]); 
  617. return true; 
  618. return false; 
  619.  
  620. // a # color 
  621. protected function color(&$out) { 
  622. if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) { 
  623. if (strlen($m[1]) > 7) { 
  624. $out = array("string", "", array($m[1])); 
  625. } else { 
  626. $out = array("raw_color", $m[1]); 
  627. return true; 
  628.  
  629. return false; 
  630.  
  631. // consume an argument definition list surrounded by () 
  632. // each argument is a variable name with optional value 
  633. // or at the end a ... or a variable named followed by ... 
  634. // arguments are separated by , unless a ; is in the list, then ; is the 
  635. // delimiter. 
  636. protected function argumentDef(&$args, &$isVararg) { 
  637. $s = $this->seek(); 
  638. if (!$this->literal('(')) return false; 
  639.  
  640. $values = array(); 
  641. $delim = ", "; 
  642. $method = "expressionList"; 
  643.  
  644. $isVararg = false; 
  645. while (true) { 
  646. if ($this->literal("...")) { 
  647. $isVararg = true; 
  648. break; 
  649.  
  650. if ($this->$method($value)) { 
  651. if ($value[0] == "variable") { 
  652. $arg = array("arg", $value[1]); 
  653. $ss = $this->seek(); 
  654.  
  655. if ($this->assign() && $this->$method($rhs)) { 
  656. $arg[] = $rhs; 
  657. } else { 
  658. $this->seek($ss); 
  659. if ($this->literal("...")) { 
  660. $arg[0] = "rest"; 
  661. $isVararg = true; 
  662.  
  663. $values[] = $arg; 
  664. if ($isVararg) break; 
  665. continue; 
  666. } else { 
  667. $values[] = array("lit", $value); 
  668.  
  669.  
  670. if (!$this->literal($delim)) { 
  671. if ($delim == ", " && $this->literal(";")) { 
  672. // found new delim, convert existing args 
  673. $delim = ";"; 
  674. $method = "propertyValue"; 
  675.  
  676. // transform arg list 
  677. if (isset($values[1])) { // 2 items 
  678. $newList = array(); 
  679. foreach ($values as $i => $arg) { 
  680. switch($arg[0]) { 
  681. case "arg": 
  682. if ($i) { 
  683. $this->throwError("Cannot mix ; and , as delimiter types"); 
  684. $newList[] = $arg[2]; 
  685. break; 
  686. case "lit": 
  687. $newList[] = $arg[1]; 
  688. break; 
  689. case "rest": 
  690. $this->throwError("Unexpected rest before semicolon"); 
  691.  
  692. $newList = array("list", ", ", $newList); 
  693.  
  694. switch ($values[0][0]) { 
  695. case "arg": 
  696. $newArg = array("arg", $values[0][1], $newList); 
  697. break; 
  698. case "lit": 
  699. $newArg = array("lit", $newList); 
  700. break; 
  701.  
  702. } elseif ($values) { // 1 item 
  703. $newArg = $values[0]; 
  704.  
  705. if ($newArg) { 
  706. $values = array($newArg); 
  707. } else { 
  708. break; 
  709.  
  710. if (!$this->literal(')')) { 
  711. $this->seek($s); 
  712. return false; 
  713.  
  714. $args = $values; 
  715.  
  716. return true; 
  717.  
  718. // consume a list of tags 
  719. // this accepts a hanging delimiter 
  720. protected function tags(&$tags, $simple = false, $delim = ', ') { 
  721. $tags = array(); 
  722. while ($this->tag($tt, $simple)) { 
  723. $tags[] = $tt; 
  724. if (!$this->literal($delim)) break; 
  725. if (count($tags) == 0) return false; 
  726.  
  727. return true; 
  728.  
  729. // list of tags of specifying mixin path 
  730. // optionally separated by > (lazy, accepts extra >) 
  731. protected function mixinTags(&$tags) { 
  732. $tags = array(); 
  733. while ($this->tag($tt, true)) { 
  734. $tags[] = $tt; 
  735. $this->literal(">"); 
  736.  
  737. if (count($tags) == 0) return false; 
  738.  
  739. return true; 
  740.  
  741. // a bracketed value (contained within in a tag definition) 
  742. protected function tagBracket(&$parts, &$hasExpression) { 
  743. // speed shortcut 
  744. if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") { 
  745. return false; 
  746.  
  747. $s = $this->seek(); 
  748.  
  749. $hasInterpolation = false; 
  750.  
  751. if ($this->literal("[", false)) { 
  752. $attrParts = array("["); 
  753. // keyword, string, operator 
  754. while (true) { 
  755. if ($this->literal("]", false)) { 
  756. $this->count--; 
  757. break; // get out early 
  758.  
  759. if ($this->match('\s+', $m)) { 
  760. $attrParts[] = " "; 
  761. continue; 
  762. if ($this->string($str)) { 
  763. // escape parent selector, (yuck) 
  764. foreach ($str[2] as &$chunk) { 
  765. $chunk = str_replace($this->lessc->parentSelector, "$&$", $chunk); 
  766.  
  767. $attrParts[] = $str; 
  768. $hasInterpolation = true; 
  769. continue; 
  770.  
  771. if ($this->keyword($word)) { 
  772. $attrParts[] = $word; 
  773. continue; 
  774.  
  775. if ($this->interpolation($inter, false)) { 
  776. $attrParts[] = $inter; 
  777. $hasInterpolation = true; 
  778. continue; 
  779.  
  780. // operator, handles attr namespace too 
  781. if ($this->match('[|-~\$\*\^=]+', $m)) { 
  782. $attrParts[] = $m[0]; 
  783. continue; 
  784.  
  785. break; 
  786.  
  787. if ($this->literal("]", false)) { 
  788. $attrParts[] = "]"; 
  789. foreach ($attrParts as $part) { 
  790. $parts[] = $part; 
  791. $hasExpression = $hasExpression || $hasInterpolation; 
  792. return true; 
  793. $this->seek($s); 
  794.  
  795. $this->seek($s); 
  796. return false; 
  797.  
  798. // a space separated list of selectors 
  799. protected function tag(&$tag, $simple = false) { 
  800. if ($simple) 
  801. $chars = '^@, :;{}\][>\(\) "\''; 
  802. else 
  803. $chars = '^@, ;{}["\''; 
  804.  
  805. $s = $this->seek(); 
  806.  
  807. $hasExpression = false; 
  808. $parts = array(); 
  809. while ($this->tagBracket($parts, $hasExpression)); 
  810.  
  811. $oldWhite = $this->eatWhiteDefault; 
  812. $this->eatWhiteDefault = false; 
  813.  
  814. while (true) { 
  815. if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) { 
  816. $parts[] = $m[1]; 
  817. if ($simple) break; 
  818.  
  819. while ($this->tagBracket($parts, $hasExpression)); 
  820. continue; 
  821.  
  822. if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") { 
  823. if ($this->interpolation($interp)) { 
  824. $hasExpression = true; 
  825. $interp[2] = true; // don't unescape 
  826. $parts[] = $interp; 
  827. continue; 
  828.  
  829. if ($this->literal("@")) { 
  830. $parts[] = "@"; 
  831. continue; 
  832.  
  833. if ($this->unit($unit)) { // for keyframes 
  834. $parts[] = $unit[1]; 
  835. $parts[] = $unit[2]; 
  836. continue; 
  837.  
  838. break; 
  839.  
  840. $this->eatWhiteDefault = $oldWhite; 
  841. if (!$parts) { 
  842. $this->seek($s); 
  843. return false; 
  844.  
  845. if ($hasExpression) { 
  846. $tag = array("exp", array("string", "", $parts)); 
  847. } else { 
  848. $tag = trim(implode($parts)); 
  849.  
  850. $this->whitespace(); 
  851. return true; 
  852.  
  853. // a css function 
  854. protected function func(&$func) { 
  855. $s = $this->seek(); 
  856.  
  857. if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) { 
  858. $fname = $m[1]; 
  859.  
  860. $sPreArgs = $this->seek(); 
  861.  
  862. $args = array(); 
  863. while (true) { 
  864. $ss = $this->seek(); 
  865. // this ugly nonsense is for ie filter properties 
  866. if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) { 
  867. $args[] = array("string", "", array($name, "=", $value)); 
  868. } else { 
  869. $this->seek($ss); 
  870. if ($this->expressionList($value)) { 
  871. $args[] = $value; 
  872.  
  873. if (!$this->literal(', ')) break; 
  874. $args = array('list', ', ', $args); 
  875.  
  876. if ($this->literal(')')) { 
  877. $func = array('function', $fname, $args); 
  878. return true; 
  879. } elseif ($fname == 'url') { 
  880. // couldn't parse and in url? treat as string 
  881. $this->seek($sPreArgs); 
  882. if ($this->openString(")", $string) && $this->literal(")")) { 
  883. $func = array('function', $fname, $string); 
  884. return true; 
  885.  
  886. $this->seek($s); 
  887. return false; 
  888.  
  889. // consume a less variable 
  890. protected function variable(&$name) { 
  891. $s = $this->seek(); 
  892. if ($this->literal($this->lessc->vPrefix, false) && 
  893. ($this->variable($sub) || $this->keyword($name))) 
  894. if (!empty($sub)) { 
  895. $name = array('variable', $sub); 
  896. } else { 
  897. $name = $this->lessc->vPrefix.$name; 
  898. return true; 
  899.  
  900. $name = null; 
  901. $this->seek($s); 
  902. return false; 
  903.  
  904. /** 
  905. * Consume an assignment operator 
  906. * Can optionally take a name that will be set to the current property name 
  907. */ 
  908. protected function assign($name = null) { 
  909. if ($name) $this->currentProperty = $name; 
  910. return $this->literal(':') || $this->literal('='); 
  911.  
  912. // consume a keyword 
  913. protected function keyword(&$word) { 
  914. if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) { 
  915. $word = $m[1]; 
  916. return true; 
  917. return false; 
  918.  
  919. // consume an end of statement delimiter 
  920. protected function end() { 
  921. if ($this->literal(';', false)) { 
  922. return true; 
  923. } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') { 
  924. // if there is end of file or a closing block next then we don't need a ; 
  925. return true; 
  926. return false; 
  927.  
  928. protected function guards(&$guards) { 
  929. $s = $this->seek(); 
  930.  
  931. if (!$this->literal("when")) { 
  932. $this->seek($s); 
  933. return false; 
  934.  
  935. $guards = array(); 
  936.  
  937. while ($this->guardGroup($g)) { 
  938. $guards[] = $g; 
  939. if (!$this->literal(", ")) break; 
  940.  
  941. if (count($guards) == 0) { 
  942. $guards = null; 
  943. $this->seek($s); 
  944. return false; 
  945.  
  946. return true; 
  947.  
  948. // a bunch of guards that are and'd together 
  949. // TODO rename to guardGroup 
  950. protected function guardGroup(&$guardGroup) { 
  951. $s = $this->seek(); 
  952. $guardGroup = array(); 
  953. while ($this->guard($guard)) { 
  954. $guardGroup[] = $guard; 
  955. if (!$this->literal("and")) break; 
  956.  
  957. if (count($guardGroup) == 0) { 
  958. $guardGroup = null; 
  959. $this->seek($s); 
  960. return false; 
  961.  
  962. return true; 
  963.  
  964. protected function guard(&$guard) { 
  965. $s = $this->seek(); 
  966. $negate = $this->literal("not"); 
  967.  
  968. if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) { 
  969. $guard = $exp; 
  970. if ($negate) $guard = array("negate", $guard); 
  971. return true; 
  972.  
  973. $this->seek($s); 
  974. return false; 
  975.  
  976. /** raw parsing functions */ 
  977.  
  978. protected function literal($what, $eatWhitespace = null) { 
  979. if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; 
  980.  
  981. // shortcut on single letter 
  982. if (!isset($what[1]) && isset($this->buffer[$this->count])) { 
  983. if ($this->buffer[$this->count] == $what) { 
  984. if (!$eatWhitespace) { 
  985. $this->count++; 
  986. return true; 
  987. // goes below... 
  988. } else { 
  989. return false; 
  990.  
  991. if (!isset(self::$literalCache[$what])) { 
  992. self::$literalCache[$what] = lessc::preg_quote($what); 
  993.  
  994. return $this->match(self::$literalCache[$what], $m, $eatWhitespace); 
  995.  
  996. protected function genericList(&$out, $parseItem, $delim="", $flatten=true) { 
  997. $s = $this->seek(); 
  998. $items = array(); 
  999. while ($this->$parseItem($value)) { 
  1000. $items[] = $value; 
  1001. if ($delim) { 
  1002. if (!$this->literal($delim)) break; 
  1003.  
  1004. if (count($items) == 0) { 
  1005. $this->seek($s); 
  1006. return false; 
  1007.  
  1008. if ($flatten && count($items) == 1) { 
  1009. $out = $items[0]; 
  1010. } else { 
  1011. $out = array("list", $delim, $items); 
  1012.  
  1013. return true; 
  1014.  
  1015.  
  1016. // advance counter to next occurrence of $what 
  1017. // $until - don't include $what in advance 
  1018. // $allowNewline, if string, will be used as valid char set 
  1019. protected function to($what, &$out, $until = false, $allowNewline = false) { 
  1020. if (is_string($allowNewline)) { 
  1021. $validChars = $allowNewline; 
  1022. } else { 
  1023. $validChars = $allowNewline ? "." : "[^\n]"; 
  1024. if (!$this->match('('.$validChars.'*?)'.lessc::preg_quote($what), $m, !$until)) return false; 
  1025. if ($until) $this->count -= strlen($what); // give back $what 
  1026. $out = $m[1]; 
  1027. return true; 
  1028.  
  1029. // try to match something on head of buffer 
  1030. protected function match($regex, &$out, $eatWhitespace = null) { 
  1031. if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; 
  1032.  
  1033. $r = '/'.$regex.($eatWhitespace && !$this->writeComments ? '\s*' : '').'/Ais'; 
  1034. if (preg_match($r, $this->buffer, $out, null, $this->count)) { 
  1035. $this->count += strlen($out[0]); 
  1036. if ($eatWhitespace && $this->writeComments) $this->whitespace(); 
  1037. return true; 
  1038. return false; 
  1039.  
  1040. // match some whitespace 
  1041. protected function whitespace() { 
  1042. if ($this->writeComments) { 
  1043. $gotWhite = false; 
  1044. while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) { 
  1045. if (isset($m[1]) && empty($this->seenComments[$this->count])) { 
  1046. $this->append(array("comment", $m[1])); 
  1047. $this->seenComments[$this->count] = true; 
  1048. $this->count += strlen($m[0]); 
  1049. $gotWhite = true; 
  1050. return $gotWhite; 
  1051. } else { 
  1052. $this->match("", $m); 
  1053. return strlen($m[0]) > 0; 
  1054.  
  1055. // match something without consuming it 
  1056. protected function peek($regex, &$out = null, $from=null) { 
  1057. if (is_null($from)) $from = $this->count; 
  1058. $r = '/'.$regex.'/Ais'; 
  1059. $result = preg_match($r, $this->buffer, $out, null, $from); 
  1060.  
  1061. return $result; 
  1062.  
  1063. // seek to a spot in the buffer or return where we are on no argument 
  1064. protected function seek($where = null) { 
  1065. if ($where === null) return $this->count; 
  1066. else $this->count = $where; 
  1067. return true; 
  1068.  
  1069. /** misc functions */ 
  1070.  
  1071. public function throwError($msg = "parse error", $count = null) { 
  1072. $count = is_null($count) ? $this->count : $count; 
  1073.  
  1074. $line = $this->line + 
  1075. substr_count(substr($this->buffer, 0, $count), "\n"); 
  1076.  
  1077. if (!empty($this->sourceName)) { 
  1078. $loc = "$this->sourceName on line $line"; 
  1079. } else { 
  1080. $loc = "line: $line"; 
  1081.  
  1082. // TODO this depends on $this->count 
  1083. if ($this->peek("(.*?)(\n|$)", $m, $count)) { 
  1084. throw new exception("$msg: failed at `$m[1]` $loc"); 
  1085. } else { 
  1086. throw new exception("$msg: $loc"); 
  1087.  
  1088. protected function pushBlock($selectors=null, $type=null) { 
  1089. $b = new stdclass; 
  1090. $b->parent = $this->env; 
  1091.  
  1092. $b->type = $type; 
  1093. $b->id = self::$nextBlockId++; 
  1094.  
  1095. $b->isVararg = false; // TODO: kill me from here 
  1096. $b->tags = $selectors; 
  1097.  
  1098. $b->props = array(); 
  1099. $b->children = array(); 
  1100.  
  1101. $this->env = $b; 
  1102. return $b; 
  1103.  
  1104. // push a block that doesn't multiply tags 
  1105. protected function pushSpecialBlock($type) { 
  1106. return $this->pushBlock(null, $type); 
  1107.  
  1108. // append a property to the current block 
  1109. protected function append($prop, $pos = null) { 
  1110. if ($pos !== null) $prop[-1] = $pos; 
  1111. $this->env->props[] = $prop; 
  1112.  
  1113. // pop something off the stack 
  1114. protected function pop() { 
  1115. $old = $this->env; 
  1116. $this->env = $this->env->parent; 
  1117. return $old; 
  1118.  
  1119. // remove comments from $text 
  1120. // todo: make it work for all functions, not just url 
  1121. protected function removeComments($text) { 
  1122. $look = array( 
  1123. 'url(', '//', '/*', '"', "'" 
  1124. ); 
  1125.  
  1126. $out = ''; 
  1127. $min = null; 
  1128. while (true) { 
  1129. // find the next item 
  1130. foreach ($look as $token) { 
  1131. $pos = strpos($text, $token); 
  1132. if ($pos !== false) { 
  1133. if (!isset($min) || $pos < $min[1]) $min = array($token, $pos); 
  1134.  
  1135. if (is_null($min)) break; 
  1136.  
  1137. $count = $min[1]; 
  1138. $skip = 0; 
  1139. $newlines = 0; 
  1140. switch ($min[0]) { 
  1141. case 'url(': 
  1142. if (preg_match('/url\(.*?\)/', $text, $m, 0, $count)) 
  1143. $count += strlen($m[0]) - strlen($min[0]); 
  1144. break; 
  1145. case '"': 
  1146. case "'": 
  1147. if (preg_match('/'.$min[0].'.*?(?<!\\\\)'.$min[0].'/', $text, $m, 0, $count)) 
  1148. $count += strlen($m[0]) - 1; 
  1149. break; 
  1150. case '//': 
  1151. $skip = strpos($text, "\n", $count); 
  1152. if ($skip === false) $skip = strlen($text) - $count; 
  1153. else $skip -= $count; 
  1154. break; 
  1155. case '/*': 
  1156. if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) { 
  1157. $skip = strlen($m[0]); 
  1158. $newlines = substr_count($m[0], "\n"); 
  1159. break; 
  1160.  
  1161. if ($skip == 0) $count += strlen($min[0]); 
  1162.  
  1163. $out .= substr($text, 0, $count).str_repeat("\n", $newlines); 
  1164. $text = substr($text, $count + $skip); 
  1165.  
  1166. $min = null; 
  1167.  
  1168. return $out.$text; 
  1169.