Parser.php 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842
  1. <?php
  2. /**
  3. * SCSSPHP
  4. *
  5. * @copyright 2012-2015 Leaf Corcoran
  6. *
  7. * @license http://opensource.org/licenses/MIT MIT
  8. *
  9. * @link http://leafo.github.io/scssphp
  10. */
  11. namespace Leafo\ScssPhp;
  12. use Leafo\ScssPhp\Block;
  13. use Leafo\ScssPhp\Compiler;
  14. use Leafo\ScssPhp\Exception\ParserException;
  15. use Leafo\ScssPhp\Node;
  16. use Leafo\ScssPhp\Type;
  17. /**
  18. * Parser
  19. *
  20. * @author Leaf Corcoran <leafot@gmail.com>
  21. */
  22. class Parser
  23. {
  24. const SOURCE_INDEX = -1;
  25. const SOURCE_LINE = -2;
  26. const SOURCE_COLUMN = -3;
  27. /**
  28. * @var array
  29. */
  30. protected static $precedence = array('=' => 0, 'or' => 1, 'and' => 2, '==' => 3, '!=' => 3, '<=>' => 3, '<=' => 4, '>=' => 4, '<' => 4, '>' => 4, '+' => 5, '-' => 5, '*' => 6, '/' => 6, '%' => 6);
  31. protected static $commentPattern;
  32. protected static $operatorPattern;
  33. protected static $whitePattern;
  34. private $sourceName;
  35. private $sourceIndex;
  36. private $sourcePositions;
  37. private $charset;
  38. private $count;
  39. private $env;
  40. private $inParens;
  41. private $eatWhiteDefault;
  42. private $buffer;
  43. private $utf8;
  44. private $encoding;
  45. private $patternModifiers;
  46. /**
  47. * Constructor
  48. *
  49. * @api
  50. *
  51. * @param string $sourceName
  52. * @param integer $sourceIndex
  53. * @param string $encoding
  54. */
  55. public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8')
  56. {
  57. $this->sourceName = $sourceName ?: '(stdin)';
  58. $this->sourceIndex = $sourceIndex;
  59. $this->charset = null;
  60. $this->utf8 = !$encoding || strtolower($encoding) === 'utf-8';
  61. $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais';
  62. if (empty(self::$operatorPattern)) {
  63. self::$operatorPattern = '([*\\/%+-]|[!=]\\=|\\>\\=?|\\<\\=\\>|\\<\\=?|and|or)';
  64. $commentSingle = '\\/\\/';
  65. $commentMultiLeft = '\\/\\*';
  66. $commentMultiRight = '\\*\\/';
  67. self::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight;
  68. self::$whitePattern = $this->utf8 ? '/' . $commentSingle . '[^\\n]*\\s*|(' . self::$commentPattern . ')\\s*|\\s+/AisuS' : '/' . $commentSingle . '[^\\n]*\\s*|(' . self::$commentPattern . ')\\s*|\\s+/AisS';
  69. }
  70. }
  71. /**
  72. * Get source file name
  73. *
  74. * @api
  75. *
  76. * @return string
  77. */
  78. public function getSourceName()
  79. {
  80. return $this->sourceName;
  81. }
  82. /**
  83. * Throw parser error
  84. *
  85. * @api
  86. *
  87. * @param string $msg
  88. *
  89. * @throws \Leafo\ScssPhp\Exception\ParserException
  90. */
  91. public function throwParseError($msg = 'parse error')
  92. {
  93. list($line, ) = $this->getSourcePosition($this->count);
  94. $loc = empty($this->sourceName) ? "line: {$line}" : "{$this->sourceName} on line {$line}";
  95. if ($this->peek('(.*?)(
  96. |$)', $m, $this->count)) {
  97. throw new ParserException("{$msg}: failed at `{$m['1']}` {$loc}");
  98. }
  99. throw new ParserException("{$msg}: {$loc}");
  100. }
  101. /**
  102. * Parser buffer
  103. *
  104. * @api
  105. *
  106. * @param string $buffer
  107. *
  108. * @return \Leafo\ScssPhp\Block
  109. */
  110. public function parse($buffer)
  111. {
  112. $this->count = 0;
  113. $this->env = null;
  114. $this->inParens = false;
  115. $this->eatWhiteDefault = true;
  116. $this->buffer = rtrim($buffer, '�..');
  117. $this->saveEncoding();
  118. $this->extractLineNumbers($buffer);
  119. $this->pushBlock(null);
  120. // root block
  121. $this->whitespace();
  122. $this->pushBlock(null);
  123. $this->popBlock();
  124. while ($this->parseChunk()) {
  125. }
  126. if ($this->count !== strlen($this->buffer)) {
  127. $this->throwParseError();
  128. }
  129. if (!empty($this->env->parent)) {
  130. $this->throwParseError('unclosed block');
  131. }
  132. if ($this->charset) {
  133. array_unshift($this->env->children, $this->charset);
  134. }
  135. $this->env->isRoot = true;
  136. $this->restoreEncoding();
  137. return $this->env;
  138. }
  139. /**
  140. * Parse a value or value list
  141. *
  142. * @api
  143. *
  144. * @param string $buffer
  145. * @param string $out
  146. *
  147. * @return boolean
  148. */
  149. public function parseValue($buffer, &$out)
  150. {
  151. $this->count = 0;
  152. $this->env = null;
  153. $this->inParens = false;
  154. $this->eatWhiteDefault = true;
  155. $this->buffer = (string) $buffer;
  156. $this->saveEncoding();
  157. $list = $this->valueList($out);
  158. $this->restoreEncoding();
  159. return $list;
  160. }
  161. /**
  162. * Parse a selector or selector list
  163. *
  164. * @api
  165. *
  166. * @param string $buffer
  167. * @param string $out
  168. *
  169. * @return boolean
  170. */
  171. public function parseSelector($buffer, &$out)
  172. {
  173. $this->count = 0;
  174. $this->env = null;
  175. $this->inParens = false;
  176. $this->eatWhiteDefault = true;
  177. $this->buffer = (string) $buffer;
  178. $this->saveEncoding();
  179. $selector = $this->selectors($out);
  180. $this->restoreEncoding();
  181. return $selector;
  182. }
  183. /**
  184. * Parse a single chunk off the head of the buffer and append it to the
  185. * current parse environment.
  186. *
  187. * Returns false when the buffer is empty, or when there is an error.
  188. *
  189. * This function is called repeatedly until the entire document is
  190. * parsed.
  191. *
  192. * This parser is most similar to a recursive descent parser. Single
  193. * functions represent discrete grammatical rules for the language, and
  194. * they are able to capture the text that represents those rules.
  195. *
  196. * Consider the function Compiler::keyword(). (All parse functions are
  197. * structured the same.)
  198. *
  199. * The function takes a single reference argument. When calling the
  200. * function it will attempt to match a keyword on the head of the buffer.
  201. * If it is successful, it will place the keyword in the referenced
  202. * argument, advance the position in the buffer, and return true. If it
  203. * fails then it won't advance the buffer and it will return false.
  204. *
  205. * All of these parse functions are powered by Compiler::match(), which behaves
  206. * the same way, but takes a literal regular expression. Sometimes it is
  207. * more convenient to use match instead of creating a new function.
  208. *
  209. * Because of the format of the functions, to parse an entire string of
  210. * grammatical rules, you can chain them together using &&.
  211. *
  212. * But, if some of the rules in the chain succeed before one fails, then
  213. * the buffer position will be left at an invalid state. In order to
  214. * avoid this, Compiler::seek() is used to remember and set buffer positions.
  215. *
  216. * Before parsing a chain, use $s = $this->seek() to remember the current
  217. * position into $s. Then if a chain fails, use $this->seek($s) to
  218. * go back where we started.
  219. *
  220. * @return boolean
  221. */
  222. protected function parseChunk()
  223. {
  224. $s = $this->seek();
  225. // the directives
  226. if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
  227. if ($this->literal('@at-root') && ($this->selectors($selector) || true) && ($this->map($with) || true) && $this->literal('{')) {
  228. $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
  229. $atRoot->selector = $selector;
  230. $atRoot->with = $with;
  231. return true;
  232. }
  233. $this->seek($s);
  234. if ($this->literal('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) {
  235. $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
  236. $media->queryList = $mediaQueryList[2];
  237. return true;
  238. }
  239. $this->seek($s);
  240. if ($this->literal('@mixin') && $this->keyword($mixinName) && ($this->argumentDef($args) || true) && $this->literal('{')) {
  241. $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
  242. $mixin->name = $mixinName;
  243. $mixin->args = $args;
  244. return true;
  245. }
  246. $this->seek($s);
  247. if ($this->literal('@include') && $this->keyword($mixinName) && ($this->literal('(') && ($this->argValues($argValues) || true) && $this->literal(')') || true) && ($this->end() || $this->literal('{') && ($hasBlock = true))) {
  248. $child = array(Type::T_INCLUDE, $mixinName, isset($argValues) ? $argValues : null, null);
  249. if (!empty($hasBlock)) {
  250. $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s);
  251. $include->child = $child;
  252. } else {
  253. $this->append($child, $s);
  254. }
  255. return true;
  256. }
  257. $this->seek($s);
  258. if ($this->literal('@scssphp-import-once') && $this->valueList($importPath) && $this->end()) {
  259. $this->append(array(Type::T_SCSSPHP_IMPORT_ONCE, $importPath), $s);
  260. return true;
  261. }
  262. $this->seek($s);
  263. if ($this->literal('@import') && $this->valueList($importPath) && $this->end()) {
  264. $this->append(array(Type::T_IMPORT, $importPath), $s);
  265. return true;
  266. }
  267. $this->seek($s);
  268. if ($this->literal('@import') && $this->url($importPath) && $this->end()) {
  269. $this->append(array(Type::T_IMPORT, $importPath), $s);
  270. return true;
  271. }
  272. $this->seek($s);
  273. if ($this->literal('@extend') && $this->selectors($selectors) && $this->end()) {
  274. // check for '!flag'
  275. $optional = $this->stripOptionalFlag($selectors);
  276. $this->append(array(Type::T_EXTEND, $selectors, $optional), $s);
  277. return true;
  278. }
  279. $this->seek($s);
  280. if ($this->literal('@function') && $this->keyword($fnName) && $this->argumentDef($args) && $this->literal('{')) {
  281. $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
  282. $func->name = $fnName;
  283. $func->args = $args;
  284. return true;
  285. }
  286. $this->seek($s);
  287. if ($this->literal('@break') && $this->end()) {
  288. $this->append(array(Type::T_BREAK), $s);
  289. return true;
  290. }
  291. $this->seek($s);
  292. if ($this->literal('@continue') && $this->end()) {
  293. $this->append(array(Type::T_CONTINUE), $s);
  294. return true;
  295. }
  296. $this->seek($s);
  297. if ($this->literal('@return') && ($this->valueList($retVal) || true) && $this->end()) {
  298. $this->append(array(Type::T_RETURN, isset($retVal) ? $retVal : array(Type::T_NULL)), $s);
  299. return true;
  300. }
  301. $this->seek($s);
  302. if ($this->literal('@each') && $this->genericList($varNames, 'variable', ',', false) && $this->literal('in') && $this->valueList($list) && $this->literal('{')) {
  303. $each = $this->pushSpecialBlock(Type::T_EACH, $s);
  304. foreach ($varNames[2] as $varName) {
  305. $each->vars[] = $varName[1];
  306. }
  307. $each->list = $list;
  308. return true;
  309. }
  310. $this->seek($s);
  311. if ($this->literal('@while') && $this->expression($cond) && $this->literal('{')) {
  312. $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
  313. $while->cond = $cond;
  314. return true;
  315. }
  316. $this->seek($s);
  317. if ($this->literal('@for') && $this->variable($varName) && $this->literal('from') && $this->expression($start) && ($this->literal('through') || ($forUntil = true && $this->literal('to'))) && $this->expression($end) && $this->literal('{')) {
  318. $for = $this->pushSpecialBlock(Type::T_FOR, $s);
  319. $for->var = $varName[1];
  320. $for->start = $start;
  321. $for->end = $end;
  322. $for->until = isset($forUntil);
  323. return true;
  324. }
  325. $this->seek($s);
  326. if ($this->literal('@if') && $this->valueList($cond) && $this->literal('{')) {
  327. $if = $this->pushSpecialBlock(Type::T_IF, $s);
  328. $if->cond = $cond;
  329. $if->cases = array();
  330. return true;
  331. }
  332. $this->seek($s);
  333. if ($this->literal('@debug') && $this->valueList($value) && $this->end()) {
  334. $this->append(array(Type::T_DEBUG, $value), $s);
  335. return true;
  336. }
  337. $this->seek($s);
  338. if ($this->literal('@warn') && $this->valueList($value) && $this->end()) {
  339. $this->append(array(Type::T_WARN, $value), $s);
  340. return true;
  341. }
  342. $this->seek($s);
  343. if ($this->literal('@error') && $this->valueList($value) && $this->end()) {
  344. $this->append(array(Type::T_ERROR, $value), $s);
  345. return true;
  346. }
  347. $this->seek($s);
  348. if ($this->literal('@content') && $this->end()) {
  349. $this->append(array(Type::T_MIXIN_CONTENT), $s);
  350. return true;
  351. }
  352. $this->seek($s);
  353. $last = $this->last();
  354. if (isset($last) && $last[0] === Type::T_IF) {
  355. list(, $if) = $last;
  356. if ($this->literal('@else')) {
  357. if ($this->literal('{')) {
  358. $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
  359. } elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) {
  360. $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
  361. $else->cond = $cond;
  362. }
  363. if (isset($else)) {
  364. $else->dontAppend = true;
  365. $if->cases[] = $else;
  366. return true;
  367. }
  368. }
  369. $this->seek($s);
  370. }
  371. // only retain the first @charset directive encountered
  372. if ($this->literal('@charset') && $this->valueList($charset) && $this->end()) {
  373. if (!isset($this->charset)) {
  374. $statement = array(Type::T_CHARSET, $charset);
  375. list($line, $column) = $this->getSourcePosition($s);
  376. $statement[self::SOURCE_LINE] = $line;
  377. $statement[self::SOURCE_COLUMN] = $column;
  378. $statement[self::SOURCE_INDEX] = $this->sourceIndex;
  379. $this->charset = $statement;
  380. }
  381. return true;
  382. }
  383. $this->seek($s);
  384. // doesn't match built in directive, do generic one
  385. if ($this->literal('@', false) && $this->keyword($dirName) && ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) && $this->literal('{')) {
  386. if ($dirName === 'media') {
  387. $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
  388. } else {
  389. $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
  390. $directive->name = $dirName;
  391. }
  392. if (isset($dirValue)) {
  393. $directive->value = $dirValue;
  394. }
  395. return true;
  396. }
  397. $this->seek($s);
  398. return false;
  399. }
  400. // property shortcut
  401. // captures most properties before having to parse a selector
  402. if ($this->keyword($name, false) && $this->literal(': ') && $this->valueList($value) && $this->end()) {
  403. $name = array(Type::T_STRING, '', array($name));
  404. $this->append(array(Type::T_ASSIGN, $name, $value), $s);
  405. return true;
  406. }
  407. $this->seek($s);
  408. // variable assigns
  409. if ($this->variable($name) && $this->literal(':') && $this->valueList($value) && $this->end()) {
  410. // check for '!flag'
  411. $assignmentFlags = $this->stripAssignmentFlags($value);
  412. $this->append(array(Type::T_ASSIGN, $name, $value, $assignmentFlags), $s);
  413. return true;
  414. }
  415. $this->seek($s);
  416. // misc
  417. if ($this->literal('-->')) {
  418. return true;
  419. }
  420. // opening css block
  421. if ($this->selectors($selectors) && $this->literal('{')) {
  422. $this->pushBlock($selectors, $s);
  423. return true;
  424. }
  425. $this->seek($s);
  426. // property assign, or nested assign
  427. if ($this->propertyName($name) && $this->literal(':')) {
  428. $foundSomething = false;
  429. if ($this->valueList($value)) {
  430. $this->append(array(Type::T_ASSIGN, $name, $value), $s);
  431. $foundSomething = true;
  432. }
  433. if ($this->literal('{')) {
  434. $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
  435. $propBlock->prefix = $name;
  436. $foundSomething = true;
  437. } elseif ($foundSomething) {
  438. $foundSomething = $this->end();
  439. }
  440. if ($foundSomething) {
  441. return true;
  442. }
  443. }
  444. $this->seek($s);
  445. // closing a block
  446. if ($this->literal('}')) {
  447. $block = $this->popBlock();
  448. if (isset($block->type) && $block->type === Type::T_INCLUDE) {
  449. $include = $block->child;
  450. unset($block->child);
  451. $include[3] = $block;
  452. $this->append($include, $s);
  453. } elseif (empty($block->dontAppend)) {
  454. $type = isset($block->type) ? $block->type : Type::T_BLOCK;
  455. $this->append(array($type, $block), $s);
  456. }
  457. return true;
  458. }
  459. // extra stuff
  460. if ($this->literal(';') || $this->literal('<!--')) {
  461. return true;
  462. }
  463. return false;
  464. }
  465. /**
  466. * Push block onto parse tree
  467. *
  468. * @param array $selectors
  469. * @param integer $pos
  470. *
  471. * @return \Leafo\ScssPhp\Block
  472. */
  473. protected function pushBlock($selectors, $pos = 0)
  474. {
  475. list($line, $column) = $this->getSourcePosition($pos);
  476. $b = new Block();
  477. $b->sourceLine = $line;
  478. $b->sourceColumn = $column;
  479. $b->sourceIndex = $this->sourceIndex;
  480. $b->selectors = $selectors;
  481. $b->comments = array();
  482. $b->parent = $this->env;
  483. if (!$this->env) {
  484. $b->children = array();
  485. } elseif (empty($this->env->children)) {
  486. $this->env->children = $this->env->comments;
  487. $b->children = array();
  488. $this->env->comments = array();
  489. } else {
  490. $b->children = $this->env->comments;
  491. $this->env->comments = array();
  492. }
  493. $this->env = $b;
  494. return $b;
  495. }
  496. /**
  497. * Push special (named) block onto parse tree
  498. *
  499. * @param string $type
  500. * @param integer $pos
  501. *
  502. * @return \Leafo\ScssPhp\Block
  503. */
  504. protected function pushSpecialBlock($type, $pos)
  505. {
  506. $block = $this->pushBlock(null, $pos);
  507. $block->type = $type;
  508. return $block;
  509. }
  510. /**
  511. * Pop scope and return last block
  512. *
  513. * @return \Leafo\ScssPhp\Block
  514. *
  515. * @throws \Exception
  516. */
  517. protected function popBlock()
  518. {
  519. $block = $this->env;
  520. if (empty($block->parent)) {
  521. $this->throwParseError('unexpected }');
  522. }
  523. $this->env = $block->parent;
  524. unset($block->parent);
  525. $comments = $block->comments;
  526. if (count($comments)) {
  527. $this->env->comments = $comments;
  528. unset($block->comments);
  529. }
  530. return $block;
  531. }
  532. /**
  533. * Peek input stream
  534. *
  535. * @param string $regex
  536. * @param array $out
  537. * @param integer $from
  538. *
  539. * @return integer
  540. */
  541. protected function peek($regex, &$out, $from = null)
  542. {
  543. if (!isset($from)) {
  544. $from = $this->count;
  545. }
  546. $r = '/' . $regex . '/' . $this->patternModifiers;
  547. $result = preg_match($r, $this->buffer, $out, null, $from);
  548. return $result;
  549. }
  550. /**
  551. * Seek to position in input stream (or return current position in input stream)
  552. *
  553. * @param integer $where
  554. *
  555. * @return integer
  556. */
  557. protected function seek($where = null)
  558. {
  559. if ($where === null) {
  560. return $this->count;
  561. }
  562. $this->count = $where;
  563. return true;
  564. }
  565. /**
  566. * Match string looking for either ending delim, escape, or string interpolation
  567. *
  568. * {@internal This is a workaround for preg_match's 250K string match limit. }}
  569. *
  570. * @param array $m Matches (passed by reference)
  571. * @param string $delim Delimeter
  572. *
  573. * @return boolean True if match; false otherwise
  574. */
  575. protected function matchString(&$m, $delim)
  576. {
  577. $token = null;
  578. $end = strlen($this->buffer);
  579. // look for either ending delim, escape, or string interpolation
  580. foreach (array('#{', '\\', $delim) as $lookahead) {
  581. $pos = strpos($this->buffer, $lookahead, $this->count);
  582. if ($pos !== false && $pos < $end) {
  583. $end = $pos;
  584. $token = $lookahead;
  585. }
  586. }
  587. if (!isset($token)) {
  588. return false;
  589. }
  590. $match = substr($this->buffer, $this->count, $end - $this->count);
  591. $m = array($match . $token, $match, $token);
  592. $this->count = $end + strlen($token);
  593. return true;
  594. }
  595. /**
  596. * Try to match something on head of buffer
  597. *
  598. * @param string $regex
  599. * @param array $out
  600. * @param boolean $eatWhitespace
  601. *
  602. * @return boolean
  603. */
  604. protected function match($regex, &$out, $eatWhitespace = null)
  605. {
  606. if (!isset($eatWhitespace)) {
  607. $eatWhitespace = $this->eatWhiteDefault;
  608. }
  609. $r = '/' . $regex . '/' . $this->patternModifiers;
  610. if (preg_match($r, $this->buffer, $out, null, $this->count)) {
  611. $this->count += strlen($out[0]);
  612. if ($eatWhitespace) {
  613. $this->whitespace();
  614. }
  615. return true;
  616. }
  617. return false;
  618. }
  619. /**
  620. * Match literal string
  621. *
  622. * @param string $what
  623. * @param boolean $eatWhitespace
  624. *
  625. * @return boolean
  626. */
  627. protected function literal($what, $eatWhitespace = null)
  628. {
  629. if (!isset($eatWhitespace)) {
  630. $eatWhitespace = $this->eatWhiteDefault;
  631. }
  632. $len = strlen($what);
  633. if (strcasecmp(substr($this->buffer, $this->count, $len), $what) === 0) {
  634. $this->count += $len;
  635. if ($eatWhitespace) {
  636. $this->whitespace();
  637. }
  638. return true;
  639. }
  640. return false;
  641. }
  642. /**
  643. * Match some whitespace
  644. *
  645. * @return boolean
  646. */
  647. protected function whitespace()
  648. {
  649. $gotWhite = false;
  650. while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
  651. if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
  652. $this->appendComment(array(Type::T_COMMENT, $m[1]));
  653. $this->commentsSeen[$this->count] = true;
  654. }
  655. $this->count += strlen($m[0]);
  656. $gotWhite = true;
  657. }
  658. return $gotWhite;
  659. }
  660. /**
  661. * Append comment to current block
  662. *
  663. * @param array $comment
  664. */
  665. protected function appendComment($comment)
  666. {
  667. $comment[1] = substr(preg_replace(array('/^\\s+/m', '/^(.)/m'), array('', ' \\1'), $comment[1]), 1);
  668. $this->env->comments[] = $comment;
  669. }
  670. /**
  671. * Append statement to current block
  672. *
  673. * @param array $statement
  674. * @param integer $pos
  675. */
  676. protected function append($statement, $pos = null)
  677. {
  678. if ($pos !== null) {
  679. list($line, $column) = $this->getSourcePosition($pos);
  680. $statement[self::SOURCE_LINE] = $line;
  681. $statement[self::SOURCE_COLUMN] = $column;
  682. $statement[self::SOURCE_INDEX] = $this->sourceIndex;
  683. }
  684. $this->env->children[] = $statement;
  685. $comments = $this->env->comments;
  686. if (count($comments)) {
  687. $this->env->children = array_merge($this->env->children, $comments);
  688. $this->env->comments = array();
  689. }
  690. }
  691. /**
  692. * Returns last child was appended
  693. *
  694. * @return array|null
  695. */
  696. protected function last()
  697. {
  698. $i = count($this->env->children) - 1;
  699. if (isset($this->env->children[$i])) {
  700. return $this->env->children[$i];
  701. }
  702. }
  703. /**
  704. * Parse media query list
  705. *
  706. * @param array $out
  707. *
  708. * @return boolean
  709. */
  710. protected function mediaQueryList(&$out)
  711. {
  712. return $this->genericList($out, 'mediaQuery', ',', false);
  713. }
  714. /**
  715. * Parse media query
  716. *
  717. * @param array $out
  718. *
  719. * @return boolean
  720. */
  721. protected function mediaQuery(&$out)
  722. {
  723. $expressions = null;
  724. $parts = array();
  725. if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) && $this->mixedKeyword($mediaType)) {
  726. $prop = array(Type::T_MEDIA_TYPE);
  727. if (isset($only)) {
  728. $prop[] = array(Type::T_KEYWORD, 'only');
  729. }
  730. if (isset($not)) {
  731. $prop[] = array(Type::T_KEYWORD, 'not');
  732. }
  733. $media = array(Type::T_LIST, '', array());
  734. foreach ((array) $mediaType as $type) {
  735. if (is_array($type)) {
  736. $media[2][] = $type;
  737. } else {
  738. $media[2][] = array(Type::T_KEYWORD, $type);
  739. }
  740. }
  741. $prop[] = $media;
  742. $parts[] = $prop;
  743. }
  744. if (empty($parts) || $this->literal('and')) {
  745. $this->genericList($expressions, 'mediaExpression', 'and', false);
  746. if (is_array($expressions)) {
  747. $parts = array_merge($parts, $expressions[2]);
  748. }
  749. }
  750. $out = $parts;
  751. return true;
  752. }
  753. /**
  754. * Parse media expression
  755. *
  756. * @param array $out
  757. *
  758. * @return boolean
  759. */
  760. protected function mediaExpression(&$out)
  761. {
  762. $s = $this->seek();
  763. $value = null;
  764. if ($this->literal('(') && $this->expression($feature) && ($this->literal(':') && $this->expression($value) || true) && $this->literal(')')) {
  765. $out = array(Type::T_MEDIA_EXPRESSION, $feature);
  766. if ($value) {
  767. $out[] = $value;
  768. }
  769. return true;
  770. }
  771. $this->seek($s);
  772. return false;
  773. }
  774. /**
  775. * Parse argument values
  776. *
  777. * @param array $out
  778. *
  779. * @return boolean
  780. */
  781. protected function argValues(&$out)
  782. {
  783. if ($this->genericList($list, 'argValue', ',', false)) {
  784. $out = $list[2];
  785. return true;
  786. }
  787. return false;
  788. }
  789. /**
  790. * Parse argument value
  791. *
  792. * @param array $out
  793. *
  794. * @return boolean
  795. */
  796. protected function argValue(&$out)
  797. {
  798. $s = $this->seek();
  799. $keyword = null;
  800. if (!$this->variable($keyword) || !$this->literal(':')) {
  801. $this->seek($s);
  802. $keyword = null;
  803. }
  804. if ($this->genericList($value, 'expression')) {
  805. $out = array($keyword, $value, false);
  806. $s = $this->seek();
  807. if ($this->literal('...')) {
  808. $out[2] = true;
  809. } else {
  810. $this->seek($s);
  811. }
  812. return true;
  813. }
  814. return false;
  815. }
  816. /**
  817. * Parse comma separated value list
  818. *
  819. * @param string $out
  820. *
  821. * @return boolean
  822. */
  823. protected function valueList(&$out)
  824. {
  825. return $this->genericList($out, 'spaceList', ',');
  826. }
  827. /**
  828. * Parse space separated value list
  829. *
  830. * @param array $out
  831. *
  832. * @return boolean
  833. */
  834. protected function spaceList(&$out)
  835. {
  836. return $this->genericList($out, 'expression');
  837. }
  838. /**
  839. * Parse generic list
  840. *
  841. * @param array $out
  842. * @param callable $parseItem
  843. * @param string $delim
  844. * @param boolean $flatten
  845. *
  846. * @return boolean
  847. */
  848. protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
  849. {
  850. $s = $this->seek();
  851. $items = array();
  852. while ($this->{$parseItem}($value)) {
  853. $items[] = $value;
  854. if ($delim) {
  855. if (!$this->literal($delim)) {
  856. break;
  857. }
  858. }
  859. }
  860. if (count($items) === 0) {
  861. $this->seek($s);
  862. return false;
  863. }
  864. if ($flatten && count($items) === 1) {
  865. $out = $items[0];
  866. } else {
  867. $out = array(Type::T_LIST, $delim, $items);
  868. }
  869. return true;
  870. }
  871. /**
  872. * Parse expression
  873. *
  874. * @param array $out
  875. *
  876. * @return boolean
  877. */
  878. protected function expression(&$out)
  879. {
  880. $s = $this->seek();
  881. if ($this->literal('(')) {
  882. if ($this->literal(')')) {
  883. $out = array(Type::T_LIST, '', array());
  884. return true;
  885. }
  886. if ($this->valueList($out) && $this->literal(')') && $out[0] === Type::T_LIST) {
  887. return true;
  888. }
  889. $this->seek($s);
  890. if ($this->map($out)) {
  891. return true;
  892. }
  893. $this->seek($s);
  894. }
  895. if ($this->value($lhs)) {
  896. $out = $this->expHelper($lhs, 0);
  897. return true;
  898. }
  899. return false;
  900. }
  901. /**
  902. * Parse left-hand side of subexpression
  903. *
  904. * @param array $lhs
  905. * @param integer $minP
  906. *
  907. * @return array
  908. */
  909. protected function expHelper($lhs, $minP)
  910. {
  911. $operators = self::$operatorPattern;
  912. $ss = $this->seek();
  913. $whiteBefore = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]);
  914. while ($this->match($operators, $m, false) && self::$precedence[$m[1]] >= $minP) {
  915. $whiteAfter = isset($this->buffer[$this->count]) && ctype_space($this->buffer[$this->count]);
  916. $varAfter = isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '$';
  917. $this->whitespace();
  918. $op = $m[1];
  919. // don't turn negative numbers into expressions
  920. if ($op === '-' && $whiteBefore && !$whiteAfter && !$varAfter) {
  921. break;
  922. }
  923. if (!$this->value($rhs)) {
  924. break;
  925. }
  926. // peek and see if rhs belongs to next operator
  927. if ($this->peek($operators, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) {
  928. $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
  929. }
  930. $lhs = array(Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter);
  931. $ss = $this->seek();
  932. $whiteBefore = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]);
  933. }
  934. $this->seek($ss);
  935. return $lhs;
  936. }
  937. /**
  938. * Parse value
  939. *
  940. * @param array $out
  941. *
  942. * @return boolean
  943. */
  944. protected function value(&$out)
  945. {
  946. $s = $this->seek();
  947. if ($this->literal('not', false) && $this->whitespace() && $this->value($inner)) {
  948. $out = array(Type::T_UNARY, 'not', $inner, $this->inParens);
  949. return true;
  950. }
  951. $this->seek($s);
  952. if ($this->literal('not', false) && $this->parenValue($inner)) {
  953. $out = array(Type::T_UNARY, 'not', $inner, $this->inParens);
  954. return true;
  955. }
  956. $this->seek($s);
  957. if ($this->literal('+') && $this->value($inner)) {
  958. $out = array(Type::T_UNARY, '+', $inner, $this->inParens);
  959. return true;
  960. }
  961. $this->seek($s);
  962. // negation
  963. if ($this->literal('-', false) && ($this->variable($inner) || $this->unit($inner) || $this->parenValue($inner))) {
  964. $out = array(Type::T_UNARY, '-', $inner, $this->inParens);
  965. return true;
  966. }
  967. $this->seek($s);
  968. if ($this->parenValue($out) || $this->interpolation($out) || $this->variable($out) || $this->color($out) || $this->unit($out) || $this->string($out) || $this->func($out) || $this->progid($out)) {
  969. return true;
  970. }
  971. if ($this->keyword($keyword)) {
  972. if ($keyword === 'null') {
  973. $out = array(Type::T_NULL);
  974. } else {
  975. $out = array(Type::T_KEYWORD, $keyword);
  976. }
  977. return true;
  978. }
  979. return false;
  980. }
  981. /**
  982. * Parse parenthesized value
  983. *
  984. * @param array $out
  985. *
  986. * @return boolean
  987. */
  988. protected function parenValue(&$out)
  989. {
  990. $s = $this->seek();
  991. $inParens = $this->inParens;
  992. if ($this->literal('(')) {
  993. if ($this->literal(')')) {
  994. $out = array(Type::T_LIST, '', array());
  995. return true;
  996. }
  997. $this->inParens = true;
  998. if ($this->expression($exp) && $this->literal(')')) {
  999. $out = $exp;
  1000. $this->inParens = $inParens;
  1001. return true;
  1002. }
  1003. }
  1004. $this->inParens = $inParens;
  1005. $this->seek($s);
  1006. return false;
  1007. }
  1008. /**
  1009. * Parse "progid:"
  1010. *
  1011. * @param array $out
  1012. *
  1013. * @return boolean
  1014. */
  1015. protected function progid(&$out)
  1016. {
  1017. $s = $this->seek();
  1018. if ($this->literal('progid:', false) && $this->openString('(', $fn) && $this->literal('(')) {
  1019. $this->openString(')', $args, '(');
  1020. if ($this->literal(')')) {
  1021. $out = array(Type::T_STRING, '', array('progid:', $fn, '(', $args, ')'));
  1022. return true;
  1023. }
  1024. }
  1025. $this->seek($s);
  1026. return false;
  1027. }
  1028. /**
  1029. * Parse function call
  1030. *
  1031. * @param array $out
  1032. *
  1033. * @return boolean
  1034. */
  1035. protected function func(&$func)
  1036. {
  1037. $s = $this->seek();
  1038. if ($this->keyword($name, false) && $this->literal('(')) {
  1039. if ($name === 'alpha' && $this->argumentList($args)) {
  1040. $func = array(Type::T_FUNCTION, $name, array(Type::T_STRING, '', $args));
  1041. return true;
  1042. }
  1043. if ($name !== 'expression' && !preg_match('/^(-[a-z]+-)?calc$/', $name)) {
  1044. $ss = $this->seek();
  1045. if ($this->argValues($args) && $this->literal(')')) {
  1046. $func = array(Type::T_FUNCTION_CALL, $name, $args);
  1047. return true;
  1048. }
  1049. $this->seek($ss);
  1050. }
  1051. if (($this->openString(')', $str, '(') || true) && $this->literal(')')) {
  1052. $args = array();
  1053. if (!empty($str)) {
  1054. $args[] = array(null, array(Type::T_STRING, '', array($str)));
  1055. }
  1056. $func = array(Type::T_FUNCTION_CALL, $name, $args);
  1057. return true;
  1058. }
  1059. }
  1060. $this->seek($s);
  1061. return false;
  1062. }
  1063. /**
  1064. * Parse function call argument list
  1065. *
  1066. * @param array $out
  1067. *
  1068. * @return boolean
  1069. */
  1070. protected function argumentList(&$out)
  1071. {
  1072. $s = $this->seek();
  1073. $this->literal('(');
  1074. $args = array();
  1075. while ($this->keyword($var)) {
  1076. if ($this->literal('=') && $this->expression($exp)) {
  1077. $args[] = array(Type::T_STRING, '', array($var . '='));
  1078. $arg = $exp;
  1079. } else {
  1080. break;
  1081. }
  1082. $args[] = $arg;
  1083. if (!$this->literal(',')) {
  1084. break;
  1085. }
  1086. $args[] = array(Type::T_STRING, '', array(', '));
  1087. }
  1088. if (!$this->literal(')') || !count($args)) {
  1089. $this->seek($s);
  1090. return false;
  1091. }
  1092. $out = $args;
  1093. return true;
  1094. }
  1095. /**
  1096. * Parse mixin/function definition argument list
  1097. *
  1098. * @param array $out
  1099. *
  1100. * @return boolean
  1101. */
  1102. protected function argumentDef(&$out)
  1103. {
  1104. $s = $this->seek();
  1105. $this->literal('(');
  1106. $args = array();
  1107. while ($this->variable($var)) {
  1108. $arg = array($var[1], null, false);
  1109. $ss = $this->seek();
  1110. if ($this->literal(':') && $this->genericList($defaultVal, 'expression')) {
  1111. $arg[1] = $defaultVal;
  1112. } else {
  1113. $this->seek($ss);
  1114. }
  1115. $ss = $this->seek();
  1116. if ($this->literal('...')) {
  1117. $sss = $this->seek();
  1118. if (!$this->literal(')')) {
  1119. $this->throwParseError('... has to be after the final argument');
  1120. }
  1121. $arg[2] = true;
  1122. $this->seek($sss);
  1123. } else {
  1124. $this->seek($ss);
  1125. }
  1126. $args[] = $arg;
  1127. if (!$this->literal(',')) {
  1128. break;
  1129. }
  1130. }
  1131. if (!$this->literal(')')) {
  1132. $this->seek($s);
  1133. return false;
  1134. }
  1135. $out = $args;
  1136. return true;
  1137. }
  1138. /**
  1139. * Parse map
  1140. *
  1141. * @param array $out
  1142. *
  1143. * @return boolean
  1144. */
  1145. protected function map(&$out)
  1146. {
  1147. $s = $this->seek();
  1148. if (!$this->literal('(')) {
  1149. return false;
  1150. }
  1151. $keys = array();
  1152. $values = array();
  1153. while ($this->genericList($key, 'expression') && $this->literal(':') && $this->genericList($value, 'expression')) {
  1154. $keys[] = $key;
  1155. $values[] = $value;
  1156. if (!$this->literal(',')) {
  1157. break;
  1158. }
  1159. }
  1160. if (!count($keys) || !$this->literal(')')) {
  1161. $this->seek($s);
  1162. return false;
  1163. }
  1164. $out = array(Type::T_MAP, $keys, $values);
  1165. return true;
  1166. }
  1167. /**
  1168. * Parse color
  1169. *
  1170. * @param array $out
  1171. *
  1172. * @return boolean
  1173. */
  1174. protected function color(&$out)
  1175. {
  1176. $color = array(Type::T_COLOR);
  1177. if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
  1178. if (isset($m[3])) {
  1179. $num = hexdec($m[3]);
  1180. foreach (array(3, 2, 1) as $i) {
  1181. $t = $num & 15;
  1182. $color[$i] = $t << 4 | $t;
  1183. $num >>= 4;
  1184. }
  1185. } else {
  1186. $num = hexdec($m[2]);
  1187. foreach (array(3, 2, 1) as $i) {
  1188. $color[$i] = $num & 255;
  1189. $num >>= 8;
  1190. }
  1191. }
  1192. $out = $color;
  1193. return true;
  1194. }
  1195. return false;
  1196. }
  1197. /**
  1198. * Parse number with unit
  1199. *
  1200. * @param array $out
  1201. *
  1202. * @return boolean
  1203. */
  1204. protected function unit(&$unit)
  1205. {
  1206. if ($this->match('([0-9]*(\\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
  1207. $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
  1208. return true;
  1209. }
  1210. return false;
  1211. }
  1212. /**
  1213. * Parse string
  1214. *
  1215. * @param array $out
  1216. *
  1217. * @return boolean
  1218. */
  1219. protected function string(&$out)
  1220. {
  1221. $s = $this->seek();
  1222. if ($this->literal('"', false)) {
  1223. $delim = '"';
  1224. } elseif ($this->literal('\'', false)) {
  1225. $delim = '\'';
  1226. } else {
  1227. return false;
  1228. }
  1229. $content = array();
  1230. $oldWhite = $this->eatWhiteDefault;
  1231. $this->eatWhiteDefault = false;
  1232. $hasInterpolation = false;
  1233. while ($this->matchString($m, $delim)) {
  1234. if ($m[1] !== '') {
  1235. $content[] = $m[1];
  1236. }
  1237. if ($m[2] === '#{') {
  1238. $this->count -= strlen($m[2]);
  1239. if ($this->interpolation($inter, false)) {
  1240. $content[] = $inter;
  1241. $hasInterpolation = true;
  1242. } else {
  1243. $this->count += strlen($m[2]);
  1244. $content[] = '#{';
  1245. }
  1246. } elseif ($m[2] === '\\') {
  1247. if ($this->literal('"', false)) {
  1248. $content[] = $m[2] . '"';
  1249. } elseif ($this->literal('\'', false)) {
  1250. $content[] = $m[2] . '\'';
  1251. } else {
  1252. $content[] = $m[2];
  1253. }
  1254. } else {
  1255. $this->count -= strlen($delim);
  1256. break;
  1257. }
  1258. }
  1259. $this->eatWhiteDefault = $oldWhite;
  1260. if ($this->literal($delim)) {
  1261. if ($hasInterpolation) {
  1262. $delim = '"';
  1263. foreach ($content as &$string) {
  1264. if ($string === '\\\'') {
  1265. $string = '\'';
  1266. } elseif ($string === '\\"') {
  1267. $string = '"';
  1268. }
  1269. }
  1270. }
  1271. $out = array(Type::T_STRING, $delim, $content);
  1272. return true;
  1273. }
  1274. $this->seek($s);
  1275. return false;
  1276. }
  1277. /**
  1278. * Parse keyword or interpolation
  1279. *
  1280. * @param array $out
  1281. *
  1282. * @return boolean
  1283. */
  1284. protected function mixedKeyword(&$out)
  1285. {
  1286. $parts = array();
  1287. $oldWhite = $this->eatWhiteDefault;
  1288. $this->eatWhiteDefault = false;
  1289. for (;;) {
  1290. if ($this->keyword($key)) {
  1291. $parts[] = $key;
  1292. continue;
  1293. }
  1294. if ($this->interpolation($inter)) {
  1295. $parts[] = $inter;
  1296. continue;
  1297. }
  1298. break;
  1299. }
  1300. $this->eatWhiteDefault = $oldWhite;
  1301. if (count($parts) === 0) {
  1302. return false;
  1303. }
  1304. if ($this->eatWhiteDefault) {
  1305. $this->whitespace();
  1306. }
  1307. $out = $parts;
  1308. return true;
  1309. }
  1310. /**
  1311. * Parse an unbounded string stopped by $end
  1312. *
  1313. * @param string $end
  1314. * @param array $out
  1315. * @param string $nestingOpen
  1316. *
  1317. * @return boolean
  1318. */
  1319. protected function openString($end, &$out, $nestingOpen = null)
  1320. {
  1321. $oldWhite = $this->eatWhiteDefault;
  1322. $this->eatWhiteDefault = false;
  1323. $patt = '(.*?)([\'"]|#\\{|' . $this->pregQuote($end) . '|' . self::$commentPattern . ')';
  1324. $nestingLevel = 0;
  1325. $content = array();
  1326. while ($this->match($patt, $m, false)) {
  1327. if (isset($m[1]) && $m[1] !== '') {
  1328. $content[] = $m[1];
  1329. if ($nestingOpen) {
  1330. $nestingLevel += substr_count($m[1], $nestingOpen);
  1331. }
  1332. }
  1333. $tok = $m[2];
  1334. $this->count -= strlen($tok);
  1335. if ($tok === $end && !$nestingLevel--) {
  1336. break;
  1337. }
  1338. if (($tok === '\'' || $tok === '"') && $this->string($str)) {
  1339. $content[] = $str;
  1340. continue;
  1341. }
  1342. if ($tok === '#{' && $this->interpolation($inter)) {
  1343. $content[] = $inter;
  1344. continue;
  1345. }
  1346. $content[] = $tok;
  1347. $this->count += strlen($tok);
  1348. }
  1349. $this->eatWhiteDefault = $oldWhite;
  1350. if (count($content) === 0) {
  1351. return false;
  1352. }
  1353. // trim the end
  1354. if (is_string(end($content))) {
  1355. $content[count($content) - 1] = rtrim(end($content));
  1356. }
  1357. $out = array(Type::T_STRING, '', $content);
  1358. return true;
  1359. }
  1360. /**
  1361. * Parser interpolation
  1362. *
  1363. * @param array $out
  1364. * @param boolean $lookWhite save information about whitespace before and after
  1365. *
  1366. * @return boolean
  1367. */
  1368. protected function interpolation(&$out, $lookWhite = true)
  1369. {
  1370. $oldWhite = $this->eatWhiteDefault;
  1371. $this->eatWhiteDefault = true;
  1372. $s = $this->seek();
  1373. if ($this->literal('#{') && $this->valueList($value) && $this->literal('}', false)) {
  1374. if ($lookWhite) {
  1375. $left = preg_match('/\\s/', $this->buffer[$s - 1]) ? ' ' : '';
  1376. $right = preg_match('/\\s/', $this->buffer[$this->count]) ? ' ' : '';
  1377. } else {
  1378. $left = $right = false;
  1379. }
  1380. $out = array(Type::T_INTERPOLATE, $value, $left, $right);
  1381. $this->eatWhiteDefault = $oldWhite;
  1382. if ($this->eatWhiteDefault) {
  1383. $this->whitespace();
  1384. }
  1385. return true;
  1386. }
  1387. $this->seek($s);
  1388. $this->eatWhiteDefault = $oldWhite;
  1389. return false;
  1390. }
  1391. /**
  1392. * Parse property name (as an array of parts or a string)
  1393. *
  1394. * @param array $out
  1395. *
  1396. * @return boolean
  1397. */
  1398. protected function propertyName(&$out)
  1399. {
  1400. $parts = array();
  1401. $oldWhite = $this->eatWhiteDefault;
  1402. $this->eatWhiteDefault = false;
  1403. for (;;) {
  1404. if ($this->interpolation($inter)) {
  1405. $parts[] = $inter;
  1406. continue;
  1407. }
  1408. if ($this->keyword($text)) {
  1409. $parts[] = $text;
  1410. continue;
  1411. }
  1412. if (count($parts) === 0 && $this->match('[:.#]', $m, false)) {
  1413. // css hacks
  1414. $parts[] = $m[0];
  1415. continue;
  1416. }
  1417. break;
  1418. }
  1419. $this->eatWhiteDefault = $oldWhite;
  1420. if (count($parts) === 0) {
  1421. return false;
  1422. }
  1423. // match comment hack
  1424. if (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
  1425. if (!empty($m[0])) {
  1426. $parts[] = $m[0];
  1427. $this->count += strlen($m[0]);
  1428. }
  1429. }
  1430. $this->whitespace();
  1431. // get any extra whitespace
  1432. $out = array(Type::T_STRING, '', $parts);
  1433. return true;
  1434. }
  1435. /**
  1436. * Parse comma separated selector list
  1437. *
  1438. * @param array $out
  1439. *
  1440. * @return boolean
  1441. */
  1442. protected function selectors(&$out)
  1443. {
  1444. $s = $this->seek();
  1445. $selectors = array();
  1446. while ($this->selector($sel)) {
  1447. $selectors[] = $sel;
  1448. if (!$this->literal(',')) {
  1449. break;
  1450. }
  1451. while ($this->literal(',')) {
  1452. }
  1453. }
  1454. if (count($selectors) === 0) {
  1455. $this->seek($s);
  1456. return false;
  1457. }
  1458. $out = $selectors;
  1459. return true;
  1460. }
  1461. /**
  1462. * Parse whitespace separated selector list
  1463. *
  1464. * @param array $out
  1465. *
  1466. * @return boolean
  1467. */
  1468. protected function selector(&$out)
  1469. {
  1470. $selector = array();
  1471. for (;;) {
  1472. if ($this->match('[>+~]+', $m)) {
  1473. $selector[] = array($m[0]);
  1474. continue;
  1475. }
  1476. if ($this->selectorSingle($part)) {
  1477. $selector[] = $part;
  1478. $this->match('\\s+', $m);
  1479. continue;
  1480. }
  1481. if ($this->match('\\/[^\\/]+\\/', $m)) {
  1482. $selector[] = array($m[0]);
  1483. continue;
  1484. }
  1485. break;
  1486. }
  1487. if (count($selector) === 0) {
  1488. return false;
  1489. }
  1490. $out = $selector;
  1491. return true;
  1492. }
  1493. /**
  1494. * Parse the parts that make up a selector
  1495. *
  1496. * {@internal
  1497. * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
  1498. * }}
  1499. *
  1500. * @param array $out
  1501. *
  1502. * @return boolean
  1503. */
  1504. protected function selectorSingle(&$out)
  1505. {
  1506. $oldWhite = $this->eatWhiteDefault;
  1507. $this->eatWhiteDefault = false;
  1508. $parts = array();
  1509. if ($this->literal('*', false)) {
  1510. $parts[] = '*';
  1511. }
  1512. for (;;) {
  1513. // see if we can stop early
  1514. if ($this->match('\\s*[{,]', $m)) {
  1515. $this->count--;
  1516. break;
  1517. }
  1518. $s = $this->seek();
  1519. // self
  1520. if ($this->literal('&', false)) {
  1521. $parts[] = Compiler::$selfSelector;
  1522. continue;
  1523. }
  1524. if ($this->literal('.', false)) {
  1525. $parts[] = '.';
  1526. continue;
  1527. }
  1528. if ($this->literal('|', false)) {
  1529. $parts[] = '|';
  1530. continue;
  1531. }
  1532. if ($this->match('\\\\\\S', $m)) {
  1533. $parts[] = $m[0];
  1534. continue;
  1535. }
  1536. // for keyframes
  1537. if ($this->unit($unit)) {
  1538. $parts[] = $unit;
  1539. continue;
  1540. }
  1541. if ($this->keyword($name)) {
  1542. $parts[] = $name;
  1543. continue;
  1544. }
  1545. if ($this->interpolation($inter)) {
  1546. $parts[] = $inter;
  1547. continue;
  1548. }
  1549. if ($this->literal('%', false) && $this->placeholder($placeholder)) {
  1550. $parts[] = '%';
  1551. $parts[] = $placeholder;
  1552. continue;
  1553. }
  1554. if ($this->literal('#', false)) {
  1555. $parts[] = '#';
  1556. continue;
  1557. }
  1558. // a pseudo selector
  1559. if ($this->match('::?', $m) && $this->mixedKeyword($nameParts)) {
  1560. $parts[] = $m[0];
  1561. foreach ($nameParts as $sub) {
  1562. $parts[] = $sub;
  1563. }
  1564. $ss = $this->seek();
  1565. if ($this->literal('(') && ($this->openString(')', $str, '(') || true) && $this->literal(')')) {
  1566. $parts[] = '(';
  1567. if (!empty($str)) {
  1568. $parts[] = $str;
  1569. }
  1570. $parts[] = ')';
  1571. } else {
  1572. $this->seek($ss);
  1573. }
  1574. continue;
  1575. }
  1576. $this->seek($s);
  1577. // attribute selector
  1578. if ($this->literal('[') && ($this->openString(']', $str, '[') || true) && $this->literal(']')) {
  1579. $parts[] = '[';
  1580. if (!empty($str)) {
  1581. $parts[] = $str;
  1582. }
  1583. $parts[] = ']';
  1584. continue;
  1585. }
  1586. $this->seek($s);
  1587. break;
  1588. }
  1589. $this->eatWhiteDefault = $oldWhite;
  1590. if (count($parts) === 0) {
  1591. return false;
  1592. }
  1593. $out = $parts;
  1594. return true;
  1595. }
  1596. /**
  1597. * Parse a variable
  1598. *
  1599. * @param array $out
  1600. *
  1601. * @return boolean
  1602. */
  1603. protected function variable(&$out)
  1604. {
  1605. $s = $this->seek();
  1606. if ($this->literal('$', false) && $this->keyword($name)) {
  1607. $out = array(Type::T_VARIABLE, $name);
  1608. return true;
  1609. }
  1610. $this->seek($s);
  1611. return false;
  1612. }
  1613. /**
  1614. * Parse a keyword
  1615. *
  1616. * @param string $word
  1617. * @param boolean $eatWhitespace
  1618. *
  1619. * @return boolean
  1620. */
  1621. protected function keyword(&$word, $eatWhitespace = null)
  1622. {
  1623. if ($this->match($this->utf8 ? '(([\\pL\\w_\\-\\*!"\']|[\\\\].)([\\pL\\w\\-_"\']|[\\\\].)*)' : '(([\\w_\\-\\*!"\']|[\\\\].)([\\w\\-_"\']|[\\\\].)*)', $m, $eatWhitespace)) {
  1624. $word = $m[1];
  1625. return true;
  1626. }
  1627. return false;
  1628. }
  1629. /**
  1630. * Parse a placeholder
  1631. *
  1632. * @param string $placeholder
  1633. *
  1634. * @return boolean
  1635. */
  1636. protected function placeholder(&$placeholder)
  1637. {
  1638. if ($this->match($this->utf8 ? '([\\pL\\w\\-_]+|#[{][$][\\pL\\w\\-_]+[}])' : '([\\w\\-_]+|#[{][$][\\w\\-_]+[}])', $m)) {
  1639. $placeholder = $m[1];
  1640. return true;
  1641. }
  1642. return false;
  1643. }
  1644. /**
  1645. * Parse a url
  1646. *
  1647. * @param array $out
  1648. *
  1649. * @return boolean
  1650. */
  1651. protected function url(&$out)
  1652. {
  1653. if ($this->match('(url\\(\\s*(["\']?)([^)]+)\\2\\s*\\))', $m)) {
  1654. $out = array(Type::T_STRING, '', array('url(' . $m[2] . $m[3] . $m[2] . ')'));
  1655. return true;
  1656. }
  1657. return false;
  1658. }
  1659. /**
  1660. * Consume an end of statement delimiter
  1661. *
  1662. * @return boolean
  1663. */
  1664. protected function end()
  1665. {
  1666. if ($this->literal(';')) {
  1667. return true;
  1668. }
  1669. if ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
  1670. // if there is end of file or a closing block next then we don't need a ;
  1671. return true;
  1672. }
  1673. return false;
  1674. }
  1675. /**
  1676. * Strip assignment flag from the list
  1677. *
  1678. * @param array $value
  1679. *
  1680. * @return array
  1681. */
  1682. protected function stripAssignmentFlags(&$value)
  1683. {
  1684. $flags = array();
  1685. for ($token =& $value; $token[0] === Type::T_LIST && ($s = count($token[2])); $token =& $lastNode) {
  1686. $lastNode =& $token[2][$s - 1];
  1687. while ($lastNode[0] === Type::T_KEYWORD && in_array($lastNode[1], array('!default', '!global'))) {
  1688. array_pop($token[2]);
  1689. $node = end($token[2]);
  1690. $token = $this->flattenList($token);
  1691. $flags[] = $lastNode[1];
  1692. $lastNode = $node;
  1693. }
  1694. }
  1695. return $flags;
  1696. }
  1697. /**
  1698. * Strip optional flag from selector list
  1699. *
  1700. * @param array $selectors
  1701. *
  1702. * @return string
  1703. */
  1704. protected function stripOptionalFlag(&$selectors)
  1705. {
  1706. $optional = false;
  1707. $selector = end($selectors);
  1708. $part = end($selector);
  1709. if ($part === array('!optional')) {
  1710. array_pop($selectors[count($selectors) - 1]);
  1711. $optional = true;
  1712. }
  1713. return $optional;
  1714. }
  1715. /**
  1716. * Turn list of length 1 into value type
  1717. *
  1718. * @param array $value
  1719. *
  1720. * @return array
  1721. */
  1722. protected function flattenList($value)
  1723. {
  1724. if ($value[0] === Type::T_LIST && count($value[2]) === 1) {
  1725. return $this->flattenList($value[2][0]);
  1726. }
  1727. return $value;
  1728. }
  1729. /**
  1730. * @deprecated
  1731. *
  1732. * {@internal
  1733. * advance counter to next occurrence of $what
  1734. * $until - don't include $what in advance
  1735. * $allowNewline, if string, will be used as valid char set
  1736. * }}
  1737. */
  1738. protected function to($what, &$out, $until = false, $allowNewline = false)
  1739. {
  1740. if (is_string($allowNewline)) {
  1741. $validChars = $allowNewline;
  1742. } else {
  1743. $validChars = $allowNewline ? '.' : '[^
  1744. ]';
  1745. }
  1746. if (!$this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, !$until)) {
  1747. return false;
  1748. }
  1749. if ($until) {
  1750. $this->count -= strlen($what);
  1751. }
  1752. $out = $m[1];
  1753. return true;
  1754. }
  1755. /**
  1756. * @deprecated
  1757. */
  1758. protected function show()
  1759. {
  1760. if ($this->peek('(.*?)(
  1761. |$)', $m, $this->count)) {
  1762. return $m[1];
  1763. }
  1764. return '';
  1765. }
  1766. /**
  1767. * Quote regular expression
  1768. *
  1769. * @param string $what
  1770. *
  1771. * @return string
  1772. */
  1773. private function pregQuote($what)
  1774. {
  1775. return preg_quote($what, '/');
  1776. }
  1777. /**
  1778. * Extract line numbers from buffer
  1779. *
  1780. * @param string $buffer
  1781. */
  1782. private function extractLineNumbers($buffer)
  1783. {
  1784. $this->sourcePositions = array(0 => 0);
  1785. $prev = 0;
  1786. while (($pos = strpos($buffer, '
  1787. ', $prev)) !== false) {
  1788. $this->sourcePositions[] = $pos;
  1789. $prev = $pos + 1;
  1790. }
  1791. $this->sourcePositions[] = strlen($buffer);
  1792. if (substr($buffer, -1) !== '
  1793. ') {
  1794. $this->sourcePositions[] = strlen($buffer) + 1;
  1795. }
  1796. }
  1797. /**
  1798. * Get source line number and column (given character position in the buffer)
  1799. *
  1800. * @param integer $pos
  1801. *
  1802. * @return integer
  1803. */
  1804. private function getSourcePosition($pos)
  1805. {
  1806. $low = 0;
  1807. $high = count($this->sourcePositions);
  1808. while ($low < $high) {
  1809. $mid = (int) (($high + $low) / 2);
  1810. if ($pos < $this->sourcePositions[$mid]) {
  1811. $high = $mid - 1;
  1812. continue;
  1813. }
  1814. if ($pos >= $this->sourcePositions[$mid + 1]) {
  1815. $low = $mid + 1;
  1816. continue;
  1817. }
  1818. return array($mid + 1, $pos - $this->sourcePositions[$mid]);
  1819. }
  1820. return array($low + 1, $pos - $this->sourcePositions[$low]);
  1821. }
  1822. /**
  1823. * Save internal encoding
  1824. */
  1825. private function saveEncoding()
  1826. {
  1827. if (ini_get('mbstring.func_overload') & 2) {
  1828. $this->encoding = mb_internal_encoding();
  1829. mb_internal_encoding('iso-8859-1');
  1830. }
  1831. }
  1832. /**
  1833. * Restore internal encoding
  1834. */
  1835. private function restoreEncoding()
  1836. {
  1837. if ($this->encoding) {
  1838. mb_internal_encoding($this->encoding);
  1839. }
  1840. }
  1841. }