a3_lessc_parser

The WooCommerce Predictive Search LITE a3 lessc parser class.

Defined (1)

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

/admin/less/lib/lessc.inc.php  
  1. class a3_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('a3_lessc', 'preg_quote'),  
  60. array_keys(self::$precedence))).')'; 
  61.  
  62. $commentSingle = a3_lessc::preg_quote(self::$commentSingle); 
  63. $commentMultiLeft = a3_lessc::preg_quote(self::$commentMultiLeft); 
  64. $commentMultiRight = a3_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. $lastCount = $this->count; 
  88. while (false !== $this->parseChunk()); 
  89.  
  90. if ($this->count != strlen($this->buffer)) 
  91. $this->throwError(); 
  92.  
  93. // TODO report where the block was opened 
  94. if (!is_null($this->env->parent)) 
  95. throw new exception('parse error: unclosed block'); 
  96.  
  97. return $this->env; 
  98.  
  99. /** 
  100. * Parse a single chunk off the head of the buffer and append it to the 
  101. * current parse environment. 
  102. * Returns false when the buffer is empty, or when there is an error. 
  103. * This function is called repeatedly until the entire document is 
  104. * parsed. 
  105. * This parser is most similar to a recursive descent parser. Single 
  106. * functions represent discrete grammatical rules for the language, and 
  107. * they are able to capture the text that represents those rules. 
  108. * Consider the function a3_lessc::keyword(). (all parse functions are 
  109. * structured the same) 
  110. * The function takes a single reference argument. When calling the 
  111. * function it will attempt to match a keyword on the head of the buffer. 
  112. * If it is successful, it will place the keyword in the referenced 
  113. * argument, advance the position in the buffer, and return true. If it 
  114. * fails then it won't advance the buffer and it will return false. 
  115. * All of these parse functions are powered by a3_lessc::match(), which behaves 
  116. * the same way, but takes a literal regular expression. Sometimes it is 
  117. * more convenient to use match instead of creating a new function. 
  118. * Because of the format of the functions, to parse an entire string of 
  119. * grammatical rules, you can chain them together using &&. 
  120. * But, if some of the rules in the chain succeed before one fails, then 
  121. * the buffer position will be left at an invalid state. In order to 
  122. * avoid this, a3_lessc::seek() is used to remember and set buffer positions. 
  123. * Before parsing a chain, use $s = $this->seek() to remember the current 
  124. * position into $s. Then if a chain fails, use $this->seek($s) to 
  125. * go back where we started. 
  126. */ 
  127. protected function parseChunk() { 
  128. if (empty($this->buffer)) return false; 
  129. $s = $this->seek(); 
  130.  
  131. // setting a property 
  132. if ($this->keyword($key) && $this->assign() && 
  133. $this->propertyValue($value, $key) && $this->end()) 
  134. $this->append(array('assign', $key, $value), $s); 
  135. return true; 
  136. } else { 
  137. $this->seek($s); 
  138.  
  139.  
  140. // look for special css blocks 
  141. if ($this->literal('@', false)) { 
  142. $this->count--; 
  143.  
  144. // media 
  145. if ($this->literal('@media')) { 
  146. if (($this->mediaQueryList($mediaQueries) || true) 
  147. && $this->literal('{')) 
  148. $media = $this->pushSpecialBlock("media"); 
  149. $media->queries = is_null($mediaQueries) ? array() : $mediaQueries; 
  150. return true; 
  151. } else { 
  152. $this->seek($s); 
  153. return false; 
  154.  
  155. if ($this->literal("@", false) && $this->keyword($dirName)) { 
  156. if ($this->isDirective($dirName, $this->blockDirectives)) { 
  157. if (($this->openString("{", $dirValue, null, array(";")) || true) && 
  158. $this->literal("{")) 
  159. $dir = $this->pushSpecialBlock("directive"); 
  160. $dir->name = $dirName; 
  161. if (isset($dirValue)) $dir->value = $dirValue; 
  162. return true; 
  163. } elseif ($this->isDirective($dirName, $this->lineDirectives)) { 
  164. if ($this->propertyValue($dirValue) && $this->end()) { 
  165. $this->append(array("directive", $dirName, $dirValue)); 
  166. return true; 
  167.  
  168. $this->seek($s); 
  169.  
  170. // setting a variable 
  171. if ($this->variable($var) && $this->assign() && 
  172. $this->propertyValue($value) && $this->end()) 
  173. $this->append(array('assign', $var, $value), $s); 
  174. return true; 
  175. } else { 
  176. $this->seek($s); 
  177.  
  178. if ($this->import($importValue)) { 
  179. $this->append($importValue, $s); 
  180. return true; 
  181.  
  182. // opening parametric mixin 
  183. if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) && 
  184. ($this->guards($guards) || true) && 
  185. $this->literal('{')) 
  186. $block = $this->pushBlock($this->fixTags(array($tag))); 
  187. $block->args = $args; 
  188. $block->isVararg = $isVararg; 
  189. if (!empty($guards)) $block->guards = $guards; 
  190. return true; 
  191. } else { 
  192. $this->seek($s); 
  193.  
  194. // opening a simple block 
  195. if ($this->tags($tags) && $this->literal('{')) { 
  196. $tags = $this->fixTags($tags); 
  197. $this->pushBlock($tags); 
  198. return true; 
  199. } else { 
  200. $this->seek($s); 
  201.  
  202. // closing a block 
  203. if ($this->literal('}', false)) { 
  204. try { 
  205. $block = $this->pop(); 
  206. } catch (exception $e) { 
  207. $this->seek($s); 
  208. $this->throwError($e->getMessage()); 
  209.  
  210. $hidden = false; 
  211. if (is_null($block->type)) { 
  212. $hidden = true; 
  213. if (!isset($block->args)) { 
  214. foreach ($block->tags as $tag) { 
  215. if (!is_string($tag) || $tag{0} != $this->lessc->mPrefix) { 
  216. $hidden = false; 
  217. break; 
  218.  
  219. foreach ($block->tags as $tag) { 
  220. if (is_string($tag)) { 
  221. $this->env->children[$tag][] = $block; 
  222.  
  223. if (!$hidden) { 
  224. $this->append(array('block', $block), $s); 
  225.  
  226. // this is done here so comments aren't bundled into he block that 
  227. // was just closed 
  228. $this->whitespace(); 
  229. return true; 
  230.  
  231. // mixin 
  232. if ($this->mixinTags($tags) && 
  233. ($this->argumentDef($argv, $isVararg) || true) && 
  234. ($this->keyword($suffix) || true) && $this->end()) 
  235. $tags = $this->fixTags($tags); 
  236. $this->append(array('mixin', $tags, $argv, $suffix), $s); 
  237. return true; 
  238. } else { 
  239. $this->seek($s); 
  240.  
  241. // spare ; 
  242. if ($this->literal(';')) return true; 
  243.  
  244. return false; // got nothing, throw error 
  245.  
  246. protected function isDirective($dirname, $directives) { 
  247. // TODO: cache pattern in parser 
  248. $pattern = implode("|",  
  249. array_map(array('a3_lessc', "preg_quote"), $directives)); 
  250. $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i'; 
  251.  
  252. return preg_match($pattern, $dirname); 
  253.  
  254. protected function fixTags($tags) { 
  255. // move @ tags out of variable namespace 
  256. foreach ($tags as &$tag) { 
  257. if ($tag{0} == $this->lessc->vPrefix) 
  258. $tag[0] = $this->lessc->mPrefix; 
  259. return $tags; 
  260.  
  261. // a list of expressions 
  262. protected function expressionList(&$exps) { 
  263. $values = array(); 
  264.  
  265. while ($this->expression($exp)) { 
  266. $values[] = $exp; 
  267.  
  268. if (count($values) == 0) return false; 
  269.  
  270. $exps = a3_lessc::compressList($values, ' '); 
  271. return true; 
  272.  
  273. /** 
  274. * Attempt to consume an expression. 
  275. * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code 
  276. */ 
  277. protected function expression(&$out) { 
  278. if ($this->value($lhs)) { 
  279. $out = $this->expHelper($lhs, 0); 
  280.  
  281. // look for / shorthand 
  282. if (!empty($this->env->supressedDivision)) { 
  283. unset($this->env->supressedDivision); 
  284. $s = $this->seek(); 
  285. if ($this->literal("/") && $this->value($rhs)) { 
  286. $out = array("list", "",  
  287. array($out, array("keyword", "/"), $rhs)); 
  288. } else { 
  289. $this->seek($s); 
  290.  
  291. return true; 
  292. return false; 
  293.  
  294. /** 
  295. * recursively parse infix equation with $lhs at precedence $minP 
  296. */ 
  297. protected function expHelper($lhs, $minP) { 
  298. $this->inExp = true; 
  299. $ss = $this->seek(); 
  300.  
  301. while (true) { 
  302. $whiteBefore = isset($this->buffer[$this->count - 1]) && 
  303. ctype_space($this->buffer[$this->count - 1]); 
  304.  
  305. // If there is whitespace before the operator, then we require 
  306. // whitespace after the operator for it to be an expression 
  307. $needWhite = $whiteBefore && !$this->inParens; 
  308.  
  309. if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) { 
  310. if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) { 
  311. foreach (self::$supressDivisionProps as $pattern) { 
  312. if (preg_match($pattern, $this->env->currentProperty)) { 
  313. $this->env->supressedDivision = true; 
  314. break 2; 
  315.  
  316.  
  317. $whiteAfter = isset($this->buffer[$this->count - 1]) && 
  318. ctype_space($this->buffer[$this->count - 1]); 
  319.  
  320. if (!$this->value($rhs)) break; 
  321.  
  322. // peek for next operator to see what to do with rhs 
  323. if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) { 
  324. $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]); 
  325.  
  326. $lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter); 
  327. $ss = $this->seek(); 
  328.  
  329. continue; 
  330.  
  331. break; 
  332.  
  333. $this->seek($ss); 
  334.  
  335. return $lhs; 
  336.  
  337. // consume a list of values for a property 
  338. public function propertyValue(&$value, $keyName = null) { 
  339. $values = array(); 
  340.  
  341. if ($keyName !== null) $this->env->currentProperty = $keyName; 
  342.  
  343. $s = null; 
  344. while ($this->expressionList($v)) { 
  345. $values[] = $v; 
  346. $s = $this->seek(); 
  347. if (!$this->literal(', ')) break; 
  348.  
  349. if ($s) $this->seek($s); 
  350.  
  351. if ($keyName !== null) unset($this->env->currentProperty); 
  352.  
  353. if (count($values) == 0) return false; 
  354.  
  355. $value = a3_lessc::compressList($values, ', '); 
  356. return true; 
  357.  
  358. protected function parenValue(&$out) { 
  359. $s = $this->seek(); 
  360.  
  361. // speed shortcut 
  362. if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") { 
  363. return false; 
  364.  
  365. $inParens = $this->inParens; 
  366. if ($this->literal("(") && 
  367. ($this->inParens = true) && $this->expression($exp) && 
  368. $this->literal(")")) 
  369. $out = $exp; 
  370. $this->inParens = $inParens; 
  371. return true; 
  372. } else { 
  373. $this->inParens = $inParens; 
  374. $this->seek($s); 
  375.  
  376. return false; 
  377.  
  378. // a single value 
  379. protected function value(&$value) { 
  380. $s = $this->seek(); 
  381.  
  382. // speed shortcut 
  383. if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") { 
  384. // negation 
  385. if ($this->literal("-", false) && 
  386. (($this->variable($inner) && $inner = array("variable", $inner)) || 
  387. $this->unit($inner) || 
  388. $this->parenValue($inner))) 
  389. $value = array("unary", "-", $inner); 
  390. return true; 
  391. } else { 
  392. $this->seek($s); 
  393.  
  394. if ($this->parenValue($value)) return true; 
  395. if ($this->unit($value)) return true; 
  396. if ($this->color($value)) return true; 
  397. if ($this->func($value)) return true; 
  398. if ($this->string($value)) return true; 
  399.  
  400. if ($this->keyword($word)) { 
  401. $value = array('keyword', $word); 
  402. return true; 
  403.  
  404. // try a variable 
  405. if ($this->variable($var)) { 
  406. $value = array('variable', $var); 
  407. return true; 
  408.  
  409. // unquote string (should this work on any type? 
  410. if ($this->literal("~") && $this->string($str)) { 
  411. $value = array("escape", $str); 
  412. return true; 
  413. } else { 
  414. $this->seek($s); 
  415.  
  416. // css hack: \0 
  417. if ($this->literal('\\') && $this->match('([0-9]+)', $m)) { 
  418. $value = array('keyword', '\\'.$m[1]); 
  419. return true; 
  420. } else { 
  421. $this->seek($s); 
  422.  
  423. return false; 
  424.  
  425. // an import statement 
  426. protected function import(&$out) { 
  427. $s = $this->seek(); 
  428. if (!$this->literal('@import')) return false; 
  429.  
  430. // @import "something.css" media; 
  431. // @import url("something.css") media; 
  432. // @import url(something.css) media; 
  433.  
  434. if ($this->propertyValue($value)) { 
  435. $out = array("import", $value); 
  436. return true; 
  437.  
  438. protected function mediaQueryList(&$out) { 
  439. if ($this->genericList($list, "mediaQuery", ", ", false)) { 
  440. $out = $list[2]; 
  441. return true; 
  442. return false; 
  443.  
  444. protected function mediaQuery(&$out) { 
  445. $s = $this->seek(); 
  446.  
  447. $expressions = null; 
  448. $parts = array(); 
  449.  
  450. if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) { 
  451. $prop = array("mediaType"); 
  452. if (isset($only)) $prop[] = "only"; 
  453. if (isset($not)) $prop[] = "not"; 
  454. $prop[] = $mediaType; 
  455. $parts[] = $prop; 
  456. } else { 
  457. $this->seek($s); 
  458.  
  459.  
  460. if (!empty($mediaType) && !$this->literal("and")) { 
  461. // ~ 
  462. } else { 
  463. $this->genericList($expressions, "mediaExpression", "and", false); 
  464. if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]); 
  465.  
  466. if (count($parts) == 0) { 
  467. $this->seek($s); 
  468. return false; 
  469.  
  470. $out = $parts; 
  471. return true; 
  472.  
  473. protected function mediaExpression(&$out) { 
  474. $s = $this->seek(); 
  475. $value = null; 
  476. if ($this->literal("(") && 
  477. $this->keyword($feature) && 
  478. ($this->literal(":") && $this->expression($value) || true) && 
  479. $this->literal(")")) 
  480. $out = array("mediaExp", $feature); 
  481. if ($value) $out[] = $value; 
  482. return true; 
  483. } elseif ($this->variable($variable)) { 
  484. $out = array('variable', $variable); 
  485. return true; 
  486.  
  487. $this->seek($s); 
  488. return false; 
  489.  
  490. // an unbounded string stopped by $end 
  491. protected function openString($end, &$out, $nestingOpen=null, $rejectStrs = null) { 
  492. $oldWhite = $this->eatWhiteDefault; 
  493. $this->eatWhiteDefault = false; 
  494.  
  495. $stop = array("'", '"', "@{", $end); 
  496. $stop = array_map(array('a3_lessc', "preg_quote"), $stop); 
  497. // $stop[] = self::$commentMulti; 
  498.  
  499. if (!is_null($rejectStrs)) { 
  500. $stop = array_merge($stop, $rejectStrs); 
  501.  
  502. $patt = '(.*?)('.implode("|", $stop).')'; 
  503.  
  504. $nestingLevel = 0; 
  505.  
  506. $content = array(); 
  507. while ($this->match($patt, $m, false)) { 
  508. if (!empty($m[1])) { 
  509. $content[] = $m[1]; 
  510. if ($nestingOpen) { 
  511. $nestingLevel += substr_count($m[1], $nestingOpen); 
  512.  
  513. $tok = $m[2]; 
  514.  
  515. $this->count-= strlen($tok); 
  516. if ($tok == $end) { 
  517. if ($nestingLevel == 0) { 
  518. break; 
  519. } else { 
  520. $nestingLevel--; 
  521.  
  522. if (($tok == "'" || $tok == '"') && $this->string($str)) { 
  523. $content[] = $str; 
  524. continue; 
  525.  
  526. if ($tok == "@{" && $this->interpolation($inter)) { 
  527. $content[] = $inter; 
  528. continue; 
  529.  
  530. if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) { 
  531. break; 
  532.  
  533. $content[] = $tok; 
  534. $this->count+= strlen($tok); 
  535.  
  536. $this->eatWhiteDefault = $oldWhite; 
  537.  
  538. if (count($content) == 0) return false; 
  539.  
  540. // trim the end 
  541. if (is_string(end($content))) { 
  542. $content[count($content) - 1] = rtrim(end($content)); 
  543.  
  544. $out = array("string", "", $content); 
  545. return true; 
  546.  
  547. protected function string(&$out) { 
  548. $s = $this->seek(); 
  549. if ($this->literal('"', false)) { 
  550. $delim = '"'; 
  551. } elseif ($this->literal("'", false)) { 
  552. $delim = "'"; 
  553. } else { 
  554. return false; 
  555.  
  556. $content = array(); 
  557.  
  558. // look for either ending delim , escape, or string interpolation 
  559. $patt = '([^\n]*?)(@\{|\\\\|' . 
  560. a3_lessc::preg_quote($delim).')'; 
  561.  
  562. $oldWhite = $this->eatWhiteDefault; 
  563. $this->eatWhiteDefault = false; 
  564.  
  565. while ($this->match($patt, $m, false)) { 
  566. $content[] = $m[1]; 
  567. if ($m[2] == "@{") { 
  568. $this->count -= strlen($m[2]); 
  569. if ($this->interpolation($inter, false)) { 
  570. $content[] = $inter; 
  571. } else { 
  572. $this->count += strlen($m[2]); 
  573. $content[] = "@{"; // ignore it 
  574. } elseif ($m[2] == '\\') { 
  575. $content[] = $m[2]; 
  576. if ($this->literal($delim, false)) { 
  577. $content[] = $delim; 
  578. } else { 
  579. $this->count -= strlen($delim); 
  580. break; // delim 
  581.  
  582. $this->eatWhiteDefault = $oldWhite; 
  583.  
  584. if ($this->literal($delim)) { 
  585. $out = array("string", $delim, $content); 
  586. return true; 
  587.  
  588. $this->seek($s); 
  589. return false; 
  590.  
  591. protected function interpolation(&$out) { 
  592. $oldWhite = $this->eatWhiteDefault; 
  593. $this->eatWhiteDefault = true; 
  594.  
  595. $s = $this->seek(); 
  596. if ($this->literal("@{") && 
  597. $this->openString("}", $interp, null, array("'", '"', ";")) && 
  598. $this->literal("}", false)) 
  599. $out = array("interpolate", $interp); 
  600. $this->eatWhiteDefault = $oldWhite; 
  601. if ($this->eatWhiteDefault) $this->whitespace(); 
  602. return true; 
  603.  
  604. $this->eatWhiteDefault = $oldWhite; 
  605. $this->seek($s); 
  606. return false; 
  607.  
  608. protected function unit(&$unit) { 
  609. // speed shortcut 
  610. if (isset($this->buffer[$this->count])) { 
  611. $char = $this->buffer[$this->count]; 
  612. if (!ctype_digit($char) && $char != ".") return false; 
  613.  
  614. if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) { 
  615. $unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]); 
  616. return true; 
  617. return false; 
  618.  
  619. // a # color 
  620. protected function color(&$out) { 
  621. if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) { 
  622. if (strlen($m[1]) > 7) { 
  623. $out = array("string", "", array($m[1])); 
  624. } else { 
  625. $out = array("raw_color", $m[1]); 
  626. return true; 
  627.  
  628. return false; 
  629.  
  630. // consume an argument definition list surrounded by () 
  631. // each argument is a variable name with optional value 
  632. // or at the end a ... or a variable named followed by ... 
  633. // arguments are separated by , unless a ; is in the list, then ; is the 
  634. // delimiter. 
  635. protected function argumentDef(&$args, &$isVararg) { 
  636. $s = $this->seek(); 
  637. if (!$this->literal('(')) return false; 
  638.  
  639. $values = array(); 
  640. $delim = ", "; 
  641. $method = "expressionList"; 
  642.  
  643. $isVararg = false; 
  644. while (true) { 
  645. if ($this->literal("...")) { 
  646. $isVararg = true; 
  647. break; 
  648.  
  649. if ($this->$method($value)) { 
  650. if ($value[0] == "variable") { 
  651. $arg = array("arg", $value[1]); 
  652. $ss = $this->seek(); 
  653.  
  654. if ($this->assign() && $this->$method($rhs)) { 
  655. $arg[] = $rhs; 
  656. } else { 
  657. $this->seek($ss); 
  658. if ($this->literal("...")) { 
  659. $arg[0] = "rest"; 
  660. $isVararg = true; 
  661.  
  662. $values[] = $arg; 
  663. if ($isVararg) break; 
  664. continue; 
  665. } else { 
  666. $values[] = array("lit", $value); 
  667.  
  668.  
  669. if (!$this->literal($delim)) { 
  670. if ($delim == ", " && $this->literal(";")) { 
  671. // found new delim, convert existing args 
  672. $delim = ";"; 
  673. $method = "propertyValue"; 
  674.  
  675. // transform arg list 
  676. if (isset($values[1])) { // 2 items 
  677. $newList = array(); 
  678. foreach ($values as $i => $arg) { 
  679. switch($arg[0]) { 
  680. case "arg": 
  681. if ($i) { 
  682. $this->throwError("Cannot mix ; and , as delimiter types"); 
  683. $newList[] = $arg[2]; 
  684. break; 
  685. case "lit": 
  686. $newList[] = $arg[1]; 
  687. break; 
  688. case "rest": 
  689. $this->throwError("Unexpected rest before semicolon"); 
  690.  
  691. $newList = array("list", ", ", $newList); 
  692.  
  693. switch ($values[0][0]) { 
  694. case "arg": 
  695. $newArg = array("arg", $values[0][1], $newList); 
  696. break; 
  697. case "lit": 
  698. $newArg = array("lit", $newList); 
  699. break; 
  700.  
  701. } elseif ($values) { // 1 item 
  702. $newArg = $values[0]; 
  703.  
  704. if ($newArg) { 
  705. $values = array($newArg); 
  706. } else { 
  707. break; 
  708.  
  709. if (!$this->literal(')')) { 
  710. $this->seek($s); 
  711. return false; 
  712.  
  713. $args = $values; 
  714.  
  715. return true; 
  716.  
  717. // consume a list of tags 
  718. // this accepts a hanging delimiter 
  719. protected function tags(&$tags, $simple = false, $delim = ', ') { 
  720. $tags = array(); 
  721. while ($this->tag($tt, $simple)) { 
  722. $tags[] = $tt; 
  723. if (!$this->literal($delim)) break; 
  724. if (count($tags) == 0) return false; 
  725.  
  726. return true; 
  727.  
  728. // list of tags of specifying mixin path 
  729. // optionally separated by > (lazy, accepts extra >) 
  730. protected function mixinTags(&$tags) { 
  731. $s = $this->seek(); 
  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(';')) { 
  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] = a3_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.'*?)'.a3_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->commentsSeen[$this->count])) { 
  1046. $this->append(array("comment", $m[1])); 
  1047. $this->commentsSeen[$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.