scss_parser

SCSS parser.

Defined (1)

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

/library/admin/redux-framework/inc/scssphp/scss.inc.php  
  1. class scss_parser { 
  2. static protected $precedence = array( 
  3. "or" => 0,  
  4. "and" => 1,  
  5.  
  6. '==' => 2,  
  7. '!=' => 2,  
  8. '<=' => 2,  
  9. '>=' => 2,  
  10. '=' => 2,  
  11. '<' => 3,  
  12. '>' => 2,  
  13.  
  14. '+' => 3,  
  15. '-' => 3,  
  16. '*' => 4,  
  17. '/' => 4,  
  18. '%' => 4,  
  19. ); 
  20.  
  21. static protected $operators = array("+", "-", "*", "/", "%",  
  22. "==", "!=", "<=", ">=", "<", ">", "and", "or"); 
  23.  
  24. static protected $operatorStr; 
  25. static protected $whitePattern; 
  26. static protected $commentMulti; 
  27.  
  28. static protected $commentSingle = "//"; 
  29. static protected $commentMultiLeft = "/*"; 
  30. static protected $commentMultiRight = "*/"; 
  31.  
  32. /** 
  33. * Constructor 
  34. * @param string $sourceName 
  35. * @param boolean $rootParser 
  36. */ 
  37. public function __construct($sourceName = null, $rootParser = true) { 
  38. $this->sourceName = $sourceName; 
  39. $this->rootParser = $rootParser; 
  40.  
  41. if (empty(self::$operatorStr)) { 
  42. self::$operatorStr = $this->makeOperatorStr(self::$operators); 
  43.  
  44. $commentSingle = $this->preg_quote(self::$commentSingle); 
  45. $commentMultiLeft = $this->preg_quote(self::$commentMultiLeft); 
  46. $commentMultiRight = $this->preg_quote(self::$commentMultiRight); 
  47. self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight; 
  48. self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais'; 
  49.  
  50. static protected function makeOperatorStr($operators) { 
  51. return '('.implode('|', array_map(array('scss_parser', 'preg_quote'),  
  52. $operators)).')'; 
  53.  
  54. /** 
  55. * Parser buffer 
  56. * @param string $buffer; 
  57. * @return \StdClass 
  58. */ 
  59. public function parse($buffer) 
  60. $this->count = 0; 
  61. $this->env = null; 
  62. $this->inParens = false; 
  63. $this->eatWhiteDefault = true; 
  64. $this->insertComments = true; 
  65. $this->buffer = $buffer; 
  66.  
  67. $this->pushBlock(null); // root block 
  68. $this->whitespace(); 
  69.  
  70. while (false !== $this->parseChunk()) 
  71.  
  72. if ($this->count != strlen($this->buffer)) { 
  73. $this->throwParseError(); 
  74.  
  75. if (!empty($this->env->parent)) { 
  76. $this->throwParseError("unclosed block"); 
  77.  
  78. $this->env->isRoot = true; 
  79.  
  80. return $this->env; 
  81.  
  82. /** 
  83. * Parse a single chunk off the head of the buffer and append it to the 
  84. * current parse environment. 
  85. * Returns false when the buffer is empty, or when there is an error. 
  86. * This function is called repeatedly until the entire document is 
  87. * parsed. 
  88. * This parser is most similar to a recursive descent parser. Single 
  89. * functions represent discrete grammatical rules for the language, and 
  90. * they are able to capture the text that represents those rules. 
  91. * Consider the function scssc::keyword(). (All parse functions are 
  92. * structured the same.) 
  93. * The function takes a single reference argument. When calling the 
  94. * function it will attempt to match a keyword on the head of the buffer. 
  95. * If it is successful, it will place the keyword in the referenced 
  96. * argument, advance the position in the buffer, and return true. If it 
  97. * fails then it won't advance the buffer and it will return false. 
  98. * All of these parse functions are powered by scssc::match(), which behaves 
  99. * the same way, but takes a literal regular expression. Sometimes it is 
  100. * more convenient to use match instead of creating a new function. 
  101. * Because of the format of the functions, to parse an entire string of 
  102. * grammatical rules, you can chain them together using &&. 
  103. * But, if some of the rules in the chain succeed before one fails, then 
  104. * the buffer position will be left at an invalid state. In order to 
  105. * avoid this, scssc::seek() is used to remember and set buffer positions. 
  106. * Before parsing a chain, use $s = $this->seek() to remember the current 
  107. * position into $s. Then if a chain fails, use $this->seek($s) to 
  108. * go back where we started. 
  109. * @return boolean 
  110. */ 
  111. protected function parseChunk() { 
  112. $s = $this->seek(); 
  113.  
  114. // the directives 
  115. if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") { 
  116. if ($this->literal("@media") && $this->mediaQueryList($mediaQueryList) && $this->literal("{")) { 
  117. $media = $this->pushSpecialBlock("media"); 
  118. $media->queryList = $mediaQueryList[2]; 
  119. return true; 
  120. } else { 
  121. $this->seek($s); 
  122.  
  123. if ($this->literal("@mixin") && 
  124. $this->keyword($mixinName) && 
  125. ($this->argumentDef($args) || true) && 
  126. $this->literal("{")) 
  127. $mixin = $this->pushSpecialBlock("mixin"); 
  128. $mixin->name = $mixinName; 
  129. $mixin->args = $args; 
  130. return true; 
  131. } else { 
  132. $this->seek($s); 
  133.  
  134. if ($this->literal("@include") && 
  135. $this->keyword($mixinName) && 
  136. ($this->literal("(") && 
  137. ($this->argValues($argValues) || true) && 
  138. $this->literal(")") || true) && 
  139. ($this->end() || 
  140. $this->literal("{") && $hasBlock = true)) 
  141. $child = array("include",  
  142. $mixinName, isset($argValues) ? $argValues : null, null); 
  143.  
  144. if (!empty($hasBlock)) { 
  145. $include = $this->pushSpecialBlock("include"); 
  146. $include->child = $child; 
  147. } else { 
  148. $this->append($child, $s); 
  149.  
  150. return true; 
  151. } else { 
  152. $this->seek($s); 
  153.  
  154. if ($this->literal("@import") && 
  155. $this->valueList($importPath) && 
  156. $this->end()) 
  157. $this->append(array("import", $importPath), $s); 
  158. return true; 
  159. } else { 
  160. $this->seek($s); 
  161.  
  162. if ($this->literal("@extend") && 
  163. $this->selectors($selector) && 
  164. $this->end()) 
  165. $this->append(array("extend", $selector), $s); 
  166. return true; 
  167. } else { 
  168. $this->seek($s); 
  169.  
  170. if ($this->literal("@function") && 
  171. $this->keyword($fnName) && 
  172. $this->argumentDef($args) && 
  173. $this->literal("{")) 
  174. $func = $this->pushSpecialBlock("function"); 
  175. $func->name = $fnName; 
  176. $func->args = $args; 
  177. return true; 
  178. } else { 
  179. $this->seek($s); 
  180.  
  181. if ($this->literal("@return") && $this->valueList($retVal) && $this->end()) { 
  182. $this->append(array("return", $retVal), $s); 
  183. return true; 
  184. } else { 
  185. $this->seek($s); 
  186.  
  187. if ($this->literal("@each") && 
  188. $this->variable($varName) && 
  189. $this->literal("in") && 
  190. $this->valueList($list) && 
  191. $this->literal("{")) 
  192. $each = $this->pushSpecialBlock("each"); 
  193. $each->var = $varName[1]; 
  194. $each->list = $list; 
  195. return true; 
  196. } else { 
  197. $this->seek($s); 
  198.  
  199. if ($this->literal("@while") && 
  200. $this->expression($cond) && 
  201. $this->literal("{")) 
  202. $while = $this->pushSpecialBlock("while"); 
  203. $while->cond = $cond; 
  204. return true; 
  205. } else { 
  206. $this->seek($s); 
  207.  
  208. if ($this->literal("@for") && 
  209. $this->variable($varName) && 
  210. $this->literal("from") && 
  211. $this->expression($start) && 
  212. ($this->literal("through") || 
  213. ($forUntil = true && $this->literal("to"))) && 
  214. $this->expression($end) && 
  215. $this->literal("{")) 
  216. $for = $this->pushSpecialBlock("for"); 
  217. $for->var = $varName[1]; 
  218. $for->start = $start; 
  219. $for->end = $end; 
  220. $for->until = isset($forUntil); 
  221. return true; 
  222. } else { 
  223. $this->seek($s); 
  224.  
  225. if ($this->literal("@if") && $this->valueList($cond) && $this->literal("{")) { 
  226. $if = $this->pushSpecialBlock("if"); 
  227. $if->cond = $cond; 
  228. $if->cases = array(); 
  229. return true; 
  230. } else { 
  231. $this->seek($s); 
  232.  
  233. if (($this->literal("@debug") || $this->literal("@warn")) && 
  234. $this->valueList($value) && 
  235. $this->end()) { 
  236. $this->append(array("debug", $value, $s), $s); 
  237. return true; 
  238. } else { 
  239. $this->seek($s); 
  240.  
  241. if ($this->literal("@content") && $this->end()) { 
  242. $this->append(array("mixin_content"), $s); 
  243. return true; 
  244. } else { 
  245. $this->seek($s); 
  246.  
  247. $last = $this->last(); 
  248. if (isset($last) && $last[0] == "if") { 
  249. list(, $if) = $last; 
  250. if ($this->literal("@else")) { 
  251. if ($this->literal("{")) { 
  252. $else = $this->pushSpecialBlock("else"); 
  253. } elseif ($this->literal("if") && $this->valueList($cond) && $this->literal("{")) { 
  254. $else = $this->pushSpecialBlock("elseif"); 
  255. $else->cond = $cond; 
  256.  
  257. if (isset($else)) { 
  258. $else->dontAppend = true; 
  259. $if->cases[] = $else; 
  260. return true; 
  261.  
  262. $this->seek($s); 
  263.  
  264. if ($this->literal("@charset") && 
  265. $this->valueList($charset) && $this->end()) 
  266. $this->append(array("charset", $charset), $s); 
  267. return true; 
  268. } else { 
  269. $this->seek($s); 
  270.  
  271. // doesn't match built in directive, do generic one 
  272. if ($this->literal("@", false) && $this->keyword($dirName) && 
  273. ($this->openString("{", $dirValue) || true) && 
  274. $this->literal("{")) 
  275. $directive = $this->pushSpecialBlock("directive"); 
  276. $directive->name = $dirName; 
  277. if (isset($dirValue)) $directive->value = $dirValue; 
  278. return true; 
  279.  
  280. $this->seek($s); 
  281. return false; 
  282.  
  283. // property shortcut 
  284. // captures most properties before having to parse a selector 
  285. if ($this->keyword($name, false) && 
  286. $this->literal(": ") && 
  287. $this->valueList($value) && 
  288. $this->end()) 
  289. $name = array("string", "", array($name)); 
  290. $this->append(array("assign", $name, $value), $s); 
  291. return true; 
  292. } else { 
  293. $this->seek($s); 
  294.  
  295. // variable assigns 
  296. if ($this->variable($name) && 
  297. $this->literal(":") && 
  298. $this->valueList($value) && $this->end()) 
  299. // check for !default 
  300. $defaultVar = $value[0] == "list" && $this->stripDefault($value); 
  301. $this->append(array("assign", $name, $value, $defaultVar), $s); 
  302. return true; 
  303. } else { 
  304. $this->seek($s); 
  305.  
  306. // misc 
  307. if ($this->literal("-->")) { 
  308. return true; 
  309.  
  310. // opening css block 
  311. $oldComments = $this->insertComments; 
  312. $this->insertComments = false; 
  313. if ($this->selectors($selectors) && $this->literal("{")) { 
  314. $this->pushBlock($selectors); 
  315. $this->insertComments = $oldComments; 
  316. return true; 
  317. } else { 
  318. $this->seek($s); 
  319. $this->insertComments = $oldComments; 
  320.  
  321. // property assign, or nested assign 
  322. if ($this->propertyName($name) && $this->literal(":")) { 
  323. $foundSomething = false; 
  324. if ($this->valueList($value)) { 
  325. $this->append(array("assign", $name, $value), $s); 
  326. $foundSomething = true; 
  327.  
  328. if ($this->literal("{")) { 
  329. $propBlock = $this->pushSpecialBlock("nestedprop"); 
  330. $propBlock->prefix = $name; 
  331. $foundSomething = true; 
  332. } elseif ($foundSomething) { 
  333. $foundSomething = $this->end(); 
  334.  
  335. if ($foundSomething) { 
  336. return true; 
  337.  
  338. $this->seek($s); 
  339. } else { 
  340. $this->seek($s); 
  341.  
  342. // closing a block 
  343. if ($this->literal("}")) { 
  344. $block = $this->popBlock(); 
  345. if (isset($block->type) && $block->type == "include") { 
  346. $include = $block->child; 
  347. unset($block->child); 
  348. $include[3] = $block; 
  349. $this->append($include, $s); 
  350. } elseif (empty($block->dontAppend)) { 
  351. $type = isset($block->type) ? $block->type : "block"; 
  352. $this->append(array($type, $block), $s); 
  353. return true; 
  354.  
  355. // extra stuff 
  356. if ($this->literal(";") || 
  357. $this->literal("<!--")) 
  358. return true; 
  359.  
  360. return false; 
  361.  
  362. protected function stripDefault(&$value) { 
  363. $def = end($value[2]); 
  364. if ($def[0] == "keyword" && $def[1] == "!default") { 
  365. array_pop($value[2]); 
  366. $value = $this->flattenList($value); 
  367. return true; 
  368.  
  369. if ($def[0] == "list") { 
  370. return $this->stripDefault($value[2][count($value[2]) - 1]); 
  371.  
  372. return false; 
  373.  
  374. protected function literal($what, $eatWhitespace = null) { 
  375. if (!isset($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault; 
  376.  
  377. // shortcut on single letter 
  378. if (!isset($what[1]) && isset($this->buffer[$this->count])) { 
  379. if ($this->buffer[$this->count] == $what) { 
  380. if (!$eatWhitespace) { 
  381. $this->count++; 
  382. return true; 
  383. // goes below... 
  384. } else { 
  385. return false; 
  386.  
  387. return $this->match($this->preg_quote($what), $m, $eatWhitespace); 
  388.  
  389. // tree builders 
  390.  
  391. protected function pushBlock($selectors) { 
  392. $b = new stdClass; 
  393. $b->parent = $this->env; // not sure if we need this yet 
  394.  
  395. $b->selectors = $selectors; 
  396. $b->children = array(); 
  397.  
  398. $this->env = $b; 
  399. return $b; 
  400.  
  401. protected function pushSpecialBlock($type) { 
  402. $block = $this->pushBlock(null); 
  403. $block->type = $type; 
  404. return $block; 
  405.  
  406. protected function popBlock() { 
  407. if (empty($this->env->parent)) { 
  408. $this->throwParseError("unexpected }"); 
  409.  
  410. $old = $this->env; 
  411. $this->env = $this->env->parent; 
  412. unset($old->parent); 
  413. return $old; 
  414.  
  415. protected function append($statement, $pos=null) { 
  416. if ($pos !== null) { 
  417. $statement[-1] = $pos; 
  418. if (!$this->rootParser) $statement[-2] = $this; 
  419. $this->env->children[] = $statement; 
  420.  
  421. // last child that was appended 
  422. protected function last() { 
  423. $i = count($this->env->children) - 1; 
  424. if (isset($this->env->children[$i])) 
  425. return $this->env->children[$i]; 
  426.  
  427. // high level parsers (they return parts of ast) 
  428.  
  429. protected function mediaQueryList(&$out) { 
  430. return $this->genericList($out, "mediaQuery", ", ", false); 
  431.  
  432. protected function mediaQuery(&$out) { 
  433. $s = $this->seek(); 
  434.  
  435. $expressions = null; 
  436. $parts = array(); 
  437.  
  438. if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->mixedKeyword($mediaType)) { 
  439. $prop = array("mediaType"); 
  440. if (isset($only)) $prop[] = array("keyword", "only"); 
  441. if (isset($not)) $prop[] = array("keyword", "not"); 
  442. $media = array("list", "", array()); 
  443. foreach ((array)$mediaType as $type) { 
  444. if (is_array($type)) { 
  445. $media[2][] = $type; 
  446. } else { 
  447. $media[2][] = array("keyword", $type); 
  448. $prop[] = $media; 
  449. $parts[] = $prop; 
  450.  
  451. if (empty($parts) || $this->literal("and")) { 
  452. $this->genericList($expressions, "mediaExpression", "and", false); 
  453. if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]); 
  454.  
  455. $out = $parts; 
  456. return true; 
  457.  
  458. protected function mediaExpression(&$out) { 
  459. $s = $this->seek(); 
  460. $value = null; 
  461. if ($this->literal("(") && 
  462. $this->expression($feature) && 
  463. ($this->literal(":") && $this->expression($value) || true) && 
  464. $this->literal(")")) 
  465. $out = array("mediaExp", $feature); 
  466. if ($value) $out[] = $value; 
  467. return true; 
  468.  
  469. $this->seek($s); 
  470. return false; 
  471.  
  472. protected function argValues(&$out) { 
  473. if ($this->genericList($list, "argValue", ", ", false)) { 
  474. $out = $list[2]; 
  475. return true; 
  476. return false; 
  477.  
  478. protected function argValue(&$out) { 
  479. $s = $this->seek(); 
  480.  
  481. $keyword = null; 
  482. if (!$this->variable($keyword) || !$this->literal(":")) { 
  483. $this->seek($s); 
  484. $keyword = null; 
  485.  
  486. if ($this->genericList($value, "expression")) { 
  487. $out = array($keyword, $value, false); 
  488. $s = $this->seek(); 
  489. if ($this->literal("...")) { 
  490. $out[2] = true; 
  491. } else { 
  492. $this->seek($s); 
  493. return true; 
  494.  
  495. return false; 
  496.  
  497. /** 
  498. * Parse list 
  499. * @param string $out 
  500. * @return boolean 
  501. */ 
  502. public function valueList(&$out) 
  503. return $this->genericList($out, 'spaceList', ', '); 
  504.  
  505. protected function spaceList(&$out) 
  506. return $this->genericList($out, 'expression'); 
  507.  
  508. protected function genericList(&$out, $parseItem, $delim="", $flatten=true) { 
  509. $s = $this->seek(); 
  510. $items = array(); 
  511. while ($this->$parseItem($value)) { 
  512. $items[] = $value; 
  513. if ($delim) { 
  514. if (!$this->literal($delim)) break; 
  515.  
  516. if (count($items) == 0) { 
  517. $this->seek($s); 
  518. return false; 
  519.  
  520. if ($flatten && count($items) == 1) { 
  521. $out = $items[0]; 
  522. } else { 
  523. $out = array("list", $delim, $items); 
  524.  
  525. return true; 
  526.  
  527. protected function expression(&$out) { 
  528. $s = $this->seek(); 
  529.  
  530. if ($this->literal("(")) { 
  531. if ($this->literal(")")) { 
  532. $out = array("list", "", array()); 
  533. return true; 
  534.  
  535. if ($this->valueList($out) && $this->literal(')') && $out[0] == "list") { 
  536. return true; 
  537.  
  538. $this->seek($s); 
  539.  
  540. if ($this->value($lhs)) { 
  541. $out = $this->expHelper($lhs, 0); 
  542. return true; 
  543.  
  544. return false; 
  545.  
  546. protected function expHelper($lhs, $minP) { 
  547. $opstr = self::$operatorStr; 
  548.  
  549. $ss = $this->seek(); 
  550. $whiteBefore = isset($this->buffer[$this->count - 1]) && 
  551. ctype_space($this->buffer[$this->count - 1]); 
  552. while ($this->match($opstr, $m) && self::$precedence[$m[1]] >= $minP) { 
  553. $whiteAfter = isset($this->buffer[$this->count - 1]) && 
  554. ctype_space($this->buffer[$this->count - 1]); 
  555.  
  556. $op = $m[1]; 
  557.  
  558. // don't turn negative numbers into expressions 
  559. if ($op == "-" && $whiteBefore) { 
  560. if (!$whiteAfter) break; 
  561.  
  562. if (!$this->value($rhs)) break; 
  563.  
  564. // peek and see if rhs belongs to next operator 
  565. if ($this->peek($opstr, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) { 
  566. $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]); 
  567.  
  568. $lhs = array("exp", $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter); 
  569. $ss = $this->seek(); 
  570. $whiteBefore = isset($this->buffer[$this->count - 1]) && 
  571. ctype_space($this->buffer[$this->count - 1]); 
  572.  
  573. $this->seek($ss); 
  574. return $lhs; 
  575.  
  576. protected function value(&$out) { 
  577. $s = $this->seek(); 
  578.  
  579. if ($this->literal("not", false) && $this->whitespace() && $this->value($inner)) { 
  580. $out = array("unary", "not", $inner, $this->inParens); 
  581. return true; 
  582. } else { 
  583. $this->seek($s); 
  584.  
  585. if ($this->literal("+") && $this->value($inner)) { 
  586. $out = array("unary", "+", $inner, $this->inParens); 
  587. return true; 
  588. } else { 
  589. $this->seek($s); 
  590.  
  591. // negation 
  592. if ($this->literal("-", false) && 
  593. ($this->variable($inner) || 
  594. $this->unit($inner) || 
  595. $this->parenValue($inner))) 
  596. $out = array("unary", "-", $inner, $this->inParens); 
  597. return true; 
  598. } else { 
  599. $this->seek($s); 
  600.  
  601. if ($this->parenValue($out)) return true; 
  602. if ($this->interpolation($out)) return true; 
  603. if ($this->variable($out)) return true; 
  604. if ($this->color($out)) return true; 
  605. if ($this->unit($out)) return true; 
  606. if ($this->string($out)) return true; 
  607. if ($this->func($out)) return true; 
  608. if ($this->progid($out)) return true; 
  609.  
  610. if ($this->keyword($keyword)) { 
  611. if ($keyword == "null") { 
  612. $out = array("null"); 
  613. } else { 
  614. $out = array("keyword", $keyword); 
  615. return true; 
  616.  
  617. return false; 
  618.  
  619. // value wrappen in parentheses 
  620. protected function parenValue(&$out) { 
  621. $s = $this->seek(); 
  622.  
  623. $inParens = $this->inParens; 
  624. if ($this->literal("(") && 
  625. ($this->inParens = true) && $this->expression($exp) && 
  626. $this->literal(")")) 
  627. $out = $exp; 
  628. $this->inParens = $inParens; 
  629. return true; 
  630. } else { 
  631. $this->inParens = $inParens; 
  632. $this->seek($s); 
  633.  
  634. return false; 
  635.  
  636. protected function progid(&$out) { 
  637. $s = $this->seek(); 
  638. if ($this->literal("progid:", false) && 
  639. $this->openString("(", $fn) && 
  640. $this->literal("(")) 
  641. $this->openString(")", $args, "("); 
  642. if ($this->literal(")")) { 
  643. $out = array("string", "", array( 
  644. "progid:", $fn, "(", $args, ")" 
  645. )); 
  646. return true; 
  647.  
  648. $this->seek($s); 
  649. return false; 
  650.  
  651. protected function func(&$func) { 
  652. $s = $this->seek(); 
  653.  
  654. if ($this->keyword($name, false) && 
  655. $this->literal("(")) 
  656. if ($name == "alpha" && $this->argumentList($args)) { 
  657. $func = array("function", $name, array("string", "", $args)); 
  658. return true; 
  659.  
  660. if ($name != "expression" && !preg_match("/^(-[a-z]+-)?calc$/", $name)) { 
  661. $ss = $this->seek(); 
  662. if ($this->argValues($args) && $this->literal(")")) { 
  663. $func = array("fncall", $name, $args); 
  664. return true; 
  665. $this->seek($ss); 
  666.  
  667. if (($this->openString(")", $str, "(") || true ) && 
  668. $this->literal(")")) 
  669. $args = array(); 
  670. if (!empty($str)) { 
  671. $args[] = array(null, array("string", "", array($str))); 
  672.  
  673. $func = array("fncall", $name, $args); 
  674. return true; 
  675.  
  676. $this->seek($s); 
  677. return false; 
  678.  
  679. protected function argumentList(&$out) { 
  680. $s = $this->seek(); 
  681. $this->literal("("); 
  682.  
  683. $args = array(); 
  684. while ($this->keyword($var)) { 
  685. $ss = $this->seek(); 
  686.  
  687. if ($this->literal("=") && $this->expression($exp)) { 
  688. $args[] = array("string", "", array($var."=")); 
  689. $arg = $exp; 
  690. } else { 
  691. break; 
  692.  
  693. $args[] = $arg; 
  694.  
  695. if (!$this->literal(", ")) break; 
  696.  
  697. $args[] = array("string", "", array(", ")); 
  698.  
  699. if (!$this->literal(")") || !count($args)) { 
  700. $this->seek($s); 
  701. return false; 
  702.  
  703. $out = $args; 
  704. return true; 
  705.  
  706. protected function argumentDef(&$out) { 
  707. $s = $this->seek(); 
  708. $this->literal("("); 
  709.  
  710. $args = array(); 
  711. while ($this->variable($var)) { 
  712. $arg = array($var[1], null, false); 
  713.  
  714. $ss = $this->seek(); 
  715. if ($this->literal(":") && $this->genericList($defaultVal, "expression")) { 
  716. $arg[1] = $defaultVal; 
  717. } else { 
  718. $this->seek($ss); 
  719.  
  720. $ss = $this->seek(); 
  721. if ($this->literal("...")) { 
  722. $sss = $this->seek(); 
  723. if (!$this->literal(")")) { 
  724. $this->throwParseError("... has to be after the final argument"); 
  725. $arg[2] = true; 
  726. $this->seek($sss); 
  727. } else { 
  728. $this->seek($ss); 
  729.  
  730. $args[] = $arg; 
  731. if (!$this->literal(", ")) break; 
  732.  
  733. if (!$this->literal(")")) { 
  734. $this->seek($s); 
  735. return false; 
  736.  
  737. $out = $args; 
  738. return true; 
  739.  
  740. protected function color(&$out) { 
  741. $color = array('color'); 
  742.  
  743. if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) { 
  744. if (isset($m[3])) { 
  745. $num = $m[3]; 
  746. $width = 16; 
  747. } else { 
  748. $num = $m[2]; 
  749. $width = 256; 
  750.  
  751. $num = hexdec($num); 
  752. foreach (array(3, 2, 1) as $i) { 
  753. $t = $num % $width; 
  754. $num /= $width; 
  755.  
  756. $color[$i] = $t * (256/$width) + $t * floor(16/$width); 
  757.  
  758. $out = $color; 
  759. return true; 
  760.  
  761. return false; 
  762.  
  763. protected function unit(&$unit) { 
  764. if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) { 
  765. $unit = array("number", $m[1], empty($m[3]) ? "" : $m[3]); 
  766. return true; 
  767. return false; 
  768.  
  769. protected function string(&$out) { 
  770. $s = $this->seek(); 
  771. if ($this->literal('"', false)) { 
  772. $delim = '"'; 
  773. } elseif ($this->literal("'", false)) { 
  774. $delim = "'"; 
  775. } else { 
  776. return false; 
  777.  
  778. $content = array(); 
  779. $oldWhite = $this->eatWhiteDefault; 
  780. $this->eatWhiteDefault = false; 
  781.  
  782. while ($this->matchString($m, $delim)) { 
  783. $content[] = $m[1]; 
  784. if ($m[2] == "#{") { 
  785. $this->count -= strlen($m[2]); 
  786. if ($this->interpolation($inter, false)) { 
  787. $content[] = $inter; 
  788. } else { 
  789. $this->count += strlen($m[2]); 
  790. $content[] = "#{"; // ignore it 
  791. } elseif ($m[2] == '\\') { 
  792. $content[] = $m[2]; 
  793. if ($this->literal($delim, false)) { 
  794. $content[] = $delim; 
  795. } else { 
  796. $this->count -= strlen($delim); 
  797. break; // delim 
  798.  
  799. $this->eatWhiteDefault = $oldWhite; 
  800.  
  801. if ($this->literal($delim)) { 
  802. $out = array("string", $delim, $content); 
  803. return true; 
  804.  
  805. $this->seek($s); 
  806. return false; 
  807.  
  808. protected function mixedKeyword(&$out) { 
  809. $s = $this->seek(); 
  810.  
  811. $parts = array(); 
  812.  
  813. $oldWhite = $this->eatWhiteDefault; 
  814. $this->eatWhiteDefault = false; 
  815.  
  816. while (true) { 
  817. if ($this->keyword($key)) { 
  818. $parts[] = $key; 
  819. continue; 
  820.  
  821. if ($this->interpolation($inter)) { 
  822. $parts[] = $inter; 
  823. continue; 
  824.  
  825. break; 
  826.  
  827. $this->eatWhiteDefault = $oldWhite; 
  828.  
  829. if (count($parts) == 0) return false; 
  830.  
  831. if ($this->eatWhiteDefault) { 
  832. $this->whitespace(); 
  833.  
  834. $out = $parts; 
  835. return true; 
  836.  
  837. // an unbounded string stopped by $end 
  838. protected function openString($end, &$out, $nestingOpen=null) { 
  839. $oldWhite = $this->eatWhiteDefault; 
  840. $this->eatWhiteDefault = false; 
  841.  
  842. $stop = array("'", '"', "#{", $end); 
  843. $stop = array_map(array($this, "preg_quote"), $stop); 
  844. $stop[] = self::$commentMulti; 
  845.  
  846. $patt = '(.*?)('.implode("|", $stop).')'; 
  847.  
  848. $nestingLevel = 0; 
  849.  
  850. $content = array(); 
  851. while ($this->match($patt, $m, false)) { 
  852. if (isset($m[1]) && $m[1] !== '') { 
  853. $content[] = $m[1]; 
  854. if ($nestingOpen) { 
  855. $nestingLevel += substr_count($m[1], $nestingOpen); 
  856.  
  857. $tok = $m[2]; 
  858.  
  859. $this->count-= strlen($tok); 
  860. if ($tok == $end) { 
  861. if ($nestingLevel == 0) { 
  862. break; 
  863. } else { 
  864. $nestingLevel--; 
  865.  
  866. if (($tok == "'" || $tok == '"') && $this->string($str)) { 
  867. $content[] = $str; 
  868. continue; 
  869.  
  870. if ($tok == "#{" && $this->interpolation($inter)) { 
  871. $content[] = $inter; 
  872. continue; 
  873.  
  874. $content[] = $tok; 
  875. $this->count+= strlen($tok); 
  876.  
  877. $this->eatWhiteDefault = $oldWhite; 
  878.  
  879. if (count($content) == 0) return false; 
  880.  
  881. // trim the end 
  882. if (is_string(end($content))) { 
  883. $content[count($content) - 1] = rtrim(end($content)); 
  884.  
  885. $out = array("string", "", $content); 
  886. return true; 
  887.  
  888. // $lookWhite: save information about whitespace before and after 
  889. protected function interpolation(&$out, $lookWhite=true) { 
  890. $oldWhite = $this->eatWhiteDefault; 
  891. $this->eatWhiteDefault = true; 
  892.  
  893. $s = $this->seek(); 
  894. if ($this->literal("#{") && $this->valueList($value) && $this->literal("}", false)) { 
  895.  
  896. // TODO: don't error if out of bounds 
  897.  
  898. if ($lookWhite) { 
  899. $left = preg_match('/\s/', $this->buffer[$s - 1]) ? " " : ""; 
  900. $right = preg_match('/\s/', $this->buffer[$this->count]) ? " ": ""; 
  901. } else { 
  902. $left = $right = false; 
  903.  
  904. $out = array("interpolate", $value, $left, $right); 
  905. $this->eatWhiteDefault = $oldWhite; 
  906. if ($this->eatWhiteDefault) $this->whitespace(); 
  907. return true; 
  908.  
  909. $this->seek($s); 
  910. $this->eatWhiteDefault = $oldWhite; 
  911. return false; 
  912.  
  913. // low level parsers 
  914.  
  915. // returns an array of parts or a string 
  916. protected function propertyName(&$out) { 
  917. $s = $this->seek(); 
  918. $parts = array(); 
  919.  
  920. $oldWhite = $this->eatWhiteDefault; 
  921. $this->eatWhiteDefault = false; 
  922.  
  923. while (true) { 
  924. if ($this->interpolation($inter)) { 
  925. $parts[] = $inter; 
  926. } elseif ($this->keyword($text)) { 
  927. $parts[] = $text; 
  928. } elseif (count($parts) == 0 && $this->match('[:.#]', $m, false)) { 
  929. // css hacks 
  930. $parts[] = $m[0]; 
  931. } else { 
  932. break; 
  933.  
  934. $this->eatWhiteDefault = $oldWhite; 
  935. if (count($parts) == 0) return false; 
  936.  
  937. // match comment hack 
  938. if (preg_match(self::$whitePattern,  
  939. $this->buffer, $m, null, $this->count)) 
  940. if (!empty($m[0])) { 
  941. $parts[] = $m[0]; 
  942. $this->count += strlen($m[0]); 
  943.  
  944. $this->whitespace(); // get any extra whitespace 
  945.  
  946. $out = array("string", "", $parts); 
  947. return true; 
  948.  
  949. // comma separated list of selectors 
  950. protected function selectors(&$out) { 
  951. $s = $this->seek(); 
  952. $selectors = array(); 
  953. while ($this->selector($sel)) { 
  954. $selectors[] = $sel; 
  955. if (!$this->literal(", ")) break; 
  956. while ($this->literal(", ")); // ignore extra 
  957.  
  958. if (count($selectors) == 0) { 
  959. $this->seek($s); 
  960. return false; 
  961.  
  962. $out = $selectors; 
  963. return true; 
  964.  
  965. // whitespace separated list of selectorSingle 
  966. protected function selector(&$out) { 
  967. $selector = array(); 
  968.  
  969. while (true) { 
  970. if ($this->match('[>+~]+', $m)) { 
  971. $selector[] = array($m[0]); 
  972. } elseif ($this->selectorSingle($part)) { 
  973. $selector[] = $part; 
  974. $this->whitespace(); 
  975. } elseif ($this->match('\/[^\/]+\/', $m)) { 
  976. $selector[] = array($m[0]); 
  977. } else { 
  978. break; 
  979.  
  980.  
  981. if (count($selector) == 0) { 
  982. return false; 
  983.  
  984. $out = $selector; 
  985. return true; 
  986.  
  987. // the parts that make up 
  988. // div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder 
  989. protected function selectorSingle(&$out) { 
  990. $oldWhite = $this->eatWhiteDefault; 
  991. $this->eatWhiteDefault = false; 
  992.  
  993. $parts = array(); 
  994.  
  995. if ($this->literal("*", false)) { 
  996. $parts[] = "*"; 
  997.  
  998. while (true) { 
  999. // see if we can stop early 
  1000. if ($this->match("\s*[{, ]", $m)) { 
  1001. $this->count--; 
  1002. break; 
  1003.  
  1004. $s = $this->seek(); 
  1005. // self 
  1006. if ($this->literal("&", false)) { 
  1007. $parts[] = scssc::$selfSelector; 
  1008. continue; 
  1009.  
  1010. if ($this->literal(".", false)) { 
  1011. $parts[] = "."; 
  1012. continue; 
  1013.  
  1014. if ($this->literal("|", false)) { 
  1015. $parts[] = "|"; 
  1016. continue; 
  1017.  
  1018. // for keyframes 
  1019. if ($this->unit($unit)) { 
  1020. $parts[] = $unit; 
  1021. continue; 
  1022.  
  1023. if ($this->keyword($name)) { 
  1024. $parts[] = $name; 
  1025. continue; 
  1026.  
  1027. if ($this->interpolation($inter)) { 
  1028. $parts[] = $inter; 
  1029. continue; 
  1030.  
  1031. if ($this->literal('%', false) && $this->placeholder($placeholder)) { 
  1032. $parts[] = '%'; 
  1033. $parts[] = $placeholder; 
  1034. continue; 
  1035.  
  1036. if ($this->literal("#", false)) { 
  1037. $parts[] = "#"; 
  1038. continue; 
  1039.  
  1040. // a pseudo selector 
  1041. if ($this->match("::?", $m) && $this->mixedKeyword($nameParts)) { 
  1042. $parts[] = $m[0]; 
  1043. foreach ($nameParts as $sub) { 
  1044. $parts[] = $sub; 
  1045.  
  1046. $ss = $this->seek(); 
  1047. if ($this->literal("(") && 
  1048. ($this->openString(")", $str, "(") || true ) && 
  1049. $this->literal(")")) 
  1050. $parts[] = "("; 
  1051. if (!empty($str)) $parts[] = $str; 
  1052. $parts[] = ")"; 
  1053. } else { 
  1054. $this->seek($ss); 
  1055.  
  1056. continue; 
  1057. } else { 
  1058. $this->seek($s); 
  1059.  
  1060. // attribute selector 
  1061. // TODO: replace with open string? 
  1062. if ($this->literal("[", false)) { 
  1063. $attrParts = array("["); 
  1064. // keyword, string, operator 
  1065. while (true) { 
  1066. if ($this->literal("]", false)) { 
  1067. $this->count--; 
  1068. break; // get out early 
  1069.  
  1070. if ($this->match('\s+', $m)) { 
  1071. $attrParts[] = " "; 
  1072. continue; 
  1073. if ($this->string($str)) { 
  1074. $attrParts[] = $str; 
  1075. continue; 
  1076.  
  1077. if ($this->keyword($word)) { 
  1078. $attrParts[] = $word; 
  1079. continue; 
  1080.  
  1081. if ($this->interpolation($inter, false)) { 
  1082. $attrParts[] = $inter; 
  1083. continue; 
  1084.  
  1085. // operator, handles attr namespace too 
  1086. if ($this->match('[|-~\$\*\^=]+', $m)) { 
  1087. $attrParts[] = $m[0]; 
  1088. continue; 
  1089.  
  1090. break; 
  1091.  
  1092. if ($this->literal("]", false)) { 
  1093. $attrParts[] = "]"; 
  1094. foreach ($attrParts as $part) { 
  1095. $parts[] = $part; 
  1096. continue; 
  1097. $this->seek($s); 
  1098. // should just break here? 
  1099.  
  1100. break; 
  1101.  
  1102. $this->eatWhiteDefault = $oldWhite; 
  1103.  
  1104. if (count($parts) == 0) return false; 
  1105.  
  1106. $out = $parts; 
  1107. return true; 
  1108.  
  1109. protected function variable(&$out) { 
  1110. $s = $this->seek(); 
  1111. if ($this->literal("$", false) && $this->keyword($name)) { 
  1112. $out = array("var", $name); 
  1113. return true; 
  1114. $this->seek($s); 
  1115. return false; 
  1116.  
  1117. protected function keyword(&$word, $eatWhitespace = null) { 
  1118. if ($this->match('([\w_\-\*!"\'\\\\][\w\-_"\'\\\\]*)',  
  1119. $m, $eatWhitespace)) 
  1120. $word = $m[1]; 
  1121. return true; 
  1122. return false; 
  1123.  
  1124. protected function placeholder(&$placeholder) { 
  1125. if ($this->match('([\w\-_]+)', $m)) { 
  1126. $placeholder = $m[1]; 
  1127. return true; 
  1128. return false; 
  1129.  
  1130. // consume an end of statement delimiter 
  1131. protected function end() { 
  1132. if ($this->literal(';')) { 
  1133. return true; 
  1134. } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') { 
  1135. // if there is end of file or a closing block next then we don't need a ; 
  1136. return true; 
  1137. return false; 
  1138.  
  1139. // advance counter to next occurrence of $what 
  1140. // $until - don't include $what in advance 
  1141. // $allowNewline, if string, will be used as valid char set 
  1142. protected function to($what, &$out, $until = false, $allowNewline = false) { 
  1143. if (is_string($allowNewline)) { 
  1144. $validChars = $allowNewline; 
  1145. } else { 
  1146. $validChars = $allowNewline ? "." : "[^\n]"; 
  1147. if (!$this->match('('.$validChars.'*?)'.$this->preg_quote($what), $m, !$until)) return false; 
  1148. if ($until) $this->count -= strlen($what); // give back $what 
  1149. $out = $m[1]; 
  1150. return true; 
  1151.  
  1152. public function throwParseError($msg = "parse error", $count = null) { 
  1153. $count = !isset($count) ? $this->count : $count; 
  1154.  
  1155. $line = $this->getLineNo($count); 
  1156.  
  1157. if (!empty($this->sourceName)) { 
  1158. $loc = "$this->sourceName on line $line"; 
  1159. } else { 
  1160. $loc = "line: $line"; 
  1161.  
  1162. if ($this->peek("(.*?)(\n|$)", $m, $count)) { 
  1163. throw new Exception("$msg: failed at `$m[1]` $loc"); 
  1164. } else { 
  1165. throw new Exception("$msg: $loc"); 
  1166.  
  1167. public function getLineNo($pos) { 
  1168. return 1 + substr_count(substr($this->buffer, 0, $pos), "\n"); 
  1169.  
  1170. /** 
  1171. * Match string looking for either ending delim, escape, or string interpolation 
  1172. * {@internal This is a workaround for preg_match's 250K string match limit. }} 
  1173. * @param array $m Matches (passed by reference) 
  1174. * @param string $delim Delimeter 
  1175. * @return boolean True if match; false otherwise 
  1176. */ 
  1177. protected function matchString(&$m, $delim) { 
  1178. $token = null; 
  1179.  
  1180. $end = strpos($this->buffer, "\n", $this->count); 
  1181. if ($end === false || $this->buffer[$end - 1] == '\\' || $this->buffer[$end - 2] == '\\' && $this->buffer[$end - 1] == "\r") { 
  1182. $end = strlen($this->buffer); 
  1183.  
  1184. // look for either ending delim, escape, or string interpolation 
  1185. foreach (array('#{', '\\', $delim) as $lookahead) { 
  1186. $pos = strpos($this->buffer, $lookahead, $this->count); 
  1187. if ($pos !== false && $pos < $end) { 
  1188. $end = $pos; 
  1189. $token = $lookahead; 
  1190.  
  1191. if (!isset($token)) { 
  1192. return false; 
  1193.  
  1194. $match = substr($this->buffer, $this->count, $end - $this->count); 
  1195. $m = array( 
  1196. $match . $token,  
  1197. $match,  
  1198. $token 
  1199. ); 
  1200. $this->count = $end + strlen($token); 
  1201.  
  1202. return true; 
  1203.  
  1204. // try to match something on head of buffer 
  1205. protected function match($regex, &$out, $eatWhitespace = null) { 
  1206. if (!isset($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault; 
  1207.  
  1208. $r = '/'.$regex.'/Ais'; 
  1209. if (preg_match($r, $this->buffer, $out, null, $this->count)) { 
  1210. $this->count += strlen($out[0]); 
  1211. if ($eatWhitespace) $this->whitespace(); 
  1212. return true; 
  1213. return false; 
  1214.  
  1215. // match some whitespace 
  1216. protected function whitespace() { 
  1217. $gotWhite = false; 
  1218. while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) { 
  1219. if ($this->insertComments) { 
  1220. if (isset($m[1]) && empty($this->commentsSeen[$this->count])) { 
  1221. $this->append(array("comment", $m[1])); 
  1222. $this->commentsSeen[$this->count] = true; 
  1223. $this->count += strlen($m[0]); 
  1224. $gotWhite = true; 
  1225. return $gotWhite; 
  1226.  
  1227. protected function peek($regex, &$out, $from=null) { 
  1228. if (!isset($from)) $from = $this->count; 
  1229.  
  1230. $r = '/'.$regex.'/Ais'; 
  1231. $result = preg_match($r, $this->buffer, $out, null, $from); 
  1232.  
  1233. return $result; 
  1234.  
  1235. protected function seek($where = null) { 
  1236. if ($where === null) return $this->count; 
  1237. else $this->count = $where; 
  1238. return true; 
  1239.  
  1240. static function preg_quote($what) { 
  1241. return preg_quote($what, '/'); 
  1242.  
  1243. protected function show() { 
  1244. if ($this->peek("(.*?)(\n|$)", $m, $this->count)) { 
  1245. return $m[1]; 
  1246. return ""; 
  1247.  
  1248. // turn list of length 1 into value type 
  1249. protected function flattenList($value) { 
  1250. if ($value[0] == "list" && count($value[2]) == 1) { 
  1251. return $this->flattenList($value[2][0]); 
  1252. return $value;