1
0

scss.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. <?php
  2. /**
  3. * The SCSS scanner is quite complex, having to deal with nested rules
  4. * and so forth and some disambiguation is non-trivial, so we are employing
  5. * a two-pass approach here - we first tokenize the source as normal with a
  6. * scanner, then we parse the token stream with a parser to figure out
  7. * what various things really are.
  8. */
  9. class LuminousSCSSScanner extends LuminousScanner {
  10. private $regexen = array();
  11. public $rule_tag_map = array(
  12. 'PROPERTY' => 'TYPE',
  13. 'COMMENT_SL' => 'COMMENT',
  14. 'COMMENT_ML' => 'COMMENT',
  15. 'ELEMENT_SELECTOR' => 'KEYWORD',
  16. 'STRING_S' => 'STRING',
  17. 'STRING_D' => 'STRING',
  18. 'CLASS_SELECTOR' => 'VARIABLE',
  19. 'ID_SELECTOR' => 'VARIABLE',
  20. 'PSEUDO_SELECTOR' => 'OPERATOR',
  21. 'ATTR_SELECTOR' => 'OPERATOR',
  22. 'WHITESPACE' => null,
  23. 'COLON' => 'OPERATOR',
  24. 'SEMICOLON' => 'OPERATOR',
  25. 'COMMA' => 'OPERATOR',
  26. 'R_BRACE' => 'OPERATOR',
  27. 'R_BRACKET' => 'OPERATOR',
  28. 'R_SQ_BRACKET' => 'OPERATOR',
  29. 'L_BRACE' => 'OPERATOR',
  30. 'L_BRACKET' => 'OPERATOR',
  31. 'L_SQ_BRACKET' => 'OPERATOR',
  32. 'OTHER_OPERATOR' => 'OPERATOR',
  33. 'GENERIC_IDENTIFIER' => null,
  34. 'AT_IDENTIFIER' => 'KEYWORD',
  35. 'IMPORTANT' => 'KEYWORD',
  36. );
  37. public function init() {
  38. $this->regexen = array(
  39. // For the first pass we just feed in a bunch of tokens.
  40. // Some of these are generic and will require disambiguation later
  41. 'COMMENT_SL' => LuminousTokenPresets::$C_COMMENT_SL,
  42. 'COMMENT_ML' => LuminousTokenPresets::$C_COMMENT_ML,
  43. 'STRING_S' => LuminousTokenPresets::$SINGLE_STR,
  44. 'STRING_D' => LuminousTokenPresets::$DOUBLE_STR,
  45. // TODO check var naming, is $1 a legal variable?
  46. 'VARIABLE' => '%\$[\-a-z_0-9]+ | \#\{\$[\-a-z_0-9]+\} %x',
  47. 'AT_IDENTIFIER' => '%@[a-zA-Z0-9]+%',
  48. // This is generic - it may be a selector fragment, a rule, or
  49. // even a hex colour.
  50. 'GENERIC_IDENTIFIER' => '@
  51. \\#[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?
  52. |
  53. [0-9]+(\.[0-9]+)?(\w+|%|in|cm|mm|em|ex|pt|pc|px|s)?
  54. |
  55. -?[a-zA-Z_\-0-9]+[a-zA-Z_\-0-9]*
  56. |&
  57. @x',
  58. 'IMPORTANT' => '/!important/',
  59. 'L_BRACE' => '/\{/',
  60. 'R_BRACE' => '/\}/',
  61. 'L_SQ_BRACKET' => '/\[/',
  62. 'R_SQ_BRACKET' => '/\]/',
  63. 'L_BRACKET' => '/\(/',
  64. 'R_BRACKET' => '/\)/',
  65. 'DOUBLE_COLON' => '/::/',
  66. 'COLON' => '/:/',
  67. 'SEMICOLON' => '/;/',
  68. 'DOT' => '/\./',
  69. 'HASH' => '/#/',
  70. 'COMMA' => '/,/',
  71. 'OTHER_OPERATOR' => '@[+\-*/%&>=!]@',
  72. 'WHITESPACE' => '/\s+/'
  73. );
  74. }
  75. public function main() {
  76. while (!$this->eos()) {
  77. $m = null;
  78. foreach($this->regexen as $token=>$pattern) {
  79. if ( ($m = $this->scan($pattern)) !== null) {
  80. $this->record($m, $token);
  81. break;
  82. }
  83. }
  84. if ($m === null) {
  85. $this->record($this->get(), null);
  86. }
  87. }
  88. $parser = new LuminousSASSParser();
  89. $parser->tokens = $this->tokens;
  90. $parser->parse();
  91. $this->tokens = $parser->tokens;
  92. }
  93. }
  94. /**
  95. * The parsing class
  96. */
  97. class LuminousSASSParser {
  98. public $tokens;
  99. public $index;
  100. public $stack;
  101. static $delete_token = 'delete';
  102. /**
  103. * Returns true if the next token is the given token name
  104. * optionally skipping whitespace
  105. */
  106. function next_is($token_name, $ignore_whitespace = false) {
  107. $i = $this->index+1;
  108. $len = count($this->tokens);
  109. while($i<$len) {
  110. $tok = $this->tokens[$i][0];
  111. if ($ignore_whitespace && $tok === 'WHITESPACE') {
  112. $i++;
  113. }
  114. else {
  115. return $tok === $token_name;
  116. }
  117. }
  118. return false;
  119. }
  120. /**
  121. * Returns the index of the next match of the sequence of tokens
  122. * given, optionally ignoring ertain tokens
  123. */
  124. function next_sequence($sequence, $ignore=array()) {
  125. $i = $this->index+1;
  126. $len = count($this->tokens);
  127. $seq_len = count($sequence);
  128. $seq = 0;
  129. $seq_start = 0;
  130. while ($i<$len) {
  131. $tok = $this->tokens[$i][0];
  132. if ($tok === $sequence[$seq]) {
  133. if ($seq === 0) $seq_start = $i;
  134. $seq++;
  135. $i++;
  136. if ($seq === $seq_len) {
  137. return $seq_start;
  138. }
  139. } else {
  140. if (in_array($tok, $ignore)) {}
  141. else {
  142. $seq = 0;
  143. }
  144. $i++;
  145. }
  146. }
  147. return $len;
  148. }
  149. /**
  150. * Returns the first token which occurs out of the set of given tokens
  151. */
  152. function next_of($token_names) {
  153. $i = $this->index+1;
  154. $len = count($this->tokens);
  155. while ($i<$len) {
  156. $tok = $this->tokens[$i][0];
  157. if (in_array($tok, $token_names)) {
  158. return $tok;
  159. }
  160. $i++;
  161. }
  162. return null;
  163. }
  164. /**
  165. * Returns the index of the next token with the given token name
  166. */
  167. function next_of_type($token_name) {
  168. $i = $this->index+1;
  169. $len = count($this->tokens);
  170. while($i<$len) {
  171. $tok = $this->tokens[$i][0];
  172. if ($tok === $token_name) {
  173. return $i;
  174. }
  175. $i++;
  176. }
  177. return $len;
  178. }
  179. private function _parse_identifier($token) {
  180. $val = $token[1];
  181. $c = isset($val[0])? $val[0] : '';
  182. if (ctype_digit($c) || $c === '#') {
  183. $token[0] = 'NUMERIC';
  184. }
  185. }
  186. /**
  187. * Parses a selector rule
  188. */
  189. private function _parse_rule() {
  190. $new_token = $this->tokens[$this->index];
  191. $set = false;
  192. if ($this->index > 0) {
  193. $prev_token = &$this->tokens[$this->index-1];
  194. $prev_token_type = &$prev_token[0];
  195. $prev_token_text = &$prev_token[1];
  196. $concat = false;
  197. $map = array(
  198. 'DOT' => 'CLASS_SELECTOR',
  199. 'HASH' => 'ID_SELECTOR',
  200. 'COLON' => 'PSEUDO_SELECTOR',
  201. 'DOUBLE_COLON' => 'PSEUDO_SELECTOR'
  202. );
  203. if (isset($map[$prev_token_type])) {
  204. // mark the prev token for deletion and concat into one.
  205. $new_token[0] = $map[$prev_token_type];
  206. $prev_token_type = self::$delete_token;
  207. $new_token[1] = $prev_token_text . $new_token[1];
  208. $set = true;
  209. }
  210. }
  211. if (!$set) {
  212. // must be an element
  213. $new_token[0] = 'ELEMENT_SELECTOR';
  214. }
  215. $this->tokens[$this->index] = $new_token;
  216. }
  217. /**
  218. * Cleans up the token stream by deleting any tokens marked for
  219. * deletion, and makes sure the array is continuous afterwards.
  220. */
  221. private function _cleanup() {
  222. foreach($this->tokens as $i=>$t) {
  223. if ($t[0] === self::$delete_token) {
  224. unset($this->tokens[$i]);
  225. }
  226. }
  227. $this->tokens = array_values($this->tokens);
  228. }
  229. /**
  230. * Main parsing function
  231. */
  232. public function parse() {
  233. $new_tokens = array();
  234. $len = count($this->tokens);
  235. $this->stack = array();
  236. $prop_value = 'PROPERTY';
  237. $pushes = array(
  238. 'L_BRACKET' => 'bracket',
  239. 'L_BRACE' => 'brace',
  240. 'AT_IDENTIFIER' => 'at',
  241. 'L_SQ_BRACKET' => 'square'
  242. );
  243. $pops = array(
  244. 'R_BRACKET' => 'bracket',
  245. 'R_BRACE' => 'brace',
  246. 'R_SQ_BRACKET' => 'square'
  247. );
  248. $this->index = 0;
  249. while($this->index < $len) {
  250. $token = &$this->tokens[$this->index];
  251. $stack_size = count($this->stack);
  252. $state = !$stack_size? null : $this->stack[$stack_size-1];
  253. $tok_name = &$token[0];
  254. $in_brace = in_array('brace', $this->stack);
  255. $in_bracket = in_array('bracket', $this->stack);
  256. $in_sq = in_array('square', $this->stack);
  257. $in_at = in_array('at', $this->stack);
  258. if ($tok_name === self::$delete_token) continue;
  259. if ($tok_name === 'L_BRACE') {
  260. if ($state === 'at') {
  261. array_pop($this->stack);
  262. }
  263. $this->stack[] = $pushes[$tok_name];
  264. $prop_value = 'PROPERTY';
  265. }
  266. elseif (isset($pushes[$tok_name])) {
  267. $this->stack[] = $pushes[$tok_name];
  268. } else if (isset($pops[$tok_name]) && $state === $pops[$tok_name]) {
  269. array_pop($this->stack);
  270. }
  271. elseif (!$in_bracket && $tok_name === 'COLON') {
  272. $prop_value = 'VALUE';
  273. }
  274. elseif ($tok_name === 'SEMICOLON') {
  275. $prop_value = 'PROPERTY';
  276. if ($state === 'at') array_pop($this->stack);
  277. }
  278. elseif ($tok_name === 'GENERIC_IDENTIFIER') {
  279. // this is where the fun starts.
  280. // we have to figure out exactly what this is
  281. // if we can look ahead and find a '{' before we find a
  282. // ';', then this is part of a selector.
  283. // Otherwise it's part of a property/value pair.
  284. // the exception is when we have something like:
  285. // font : { family : sans-serif; }
  286. // then we need to check for ':{'
  287. if ($in_sq) {
  288. $token[0] = 'ATTR_SELECTOR';
  289. }
  290. else if ($in_bracket) {
  291. $this->_parse_identifier($token);
  292. }
  293. elseif(!$in_at) {
  294. $semi = $this->next_of_type('SEMICOLON');
  295. $colon_brace = $this->next_sequence(array('COLON', 'L_BRACE'),
  296. array('WHITESPACE'));
  297. $brace = $this->next_of_type('L_BRACE');
  298. $rule_terminator = min($semi, $colon_brace);
  299. if ($brace < $rule_terminator) {
  300. $this->_parse_rule();
  301. $prop_value = 'PROPERTY';
  302. } else {
  303. $this->tokens[$this->index][0] = $prop_value;
  304. if ($prop_value === 'VALUE') {
  305. $this->_parse_identifier($token);
  306. }
  307. }
  308. }
  309. }
  310. $this->index++;
  311. }
  312. $this->_cleanup();
  313. }
  314. }