scss_parser

SCSS parser.

Defined (1)

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

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