PageRenderTime 67ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/lessphp/Parser.php

https://bitbucket.org/moodle/moodle
PHP | 2627 lines | 1607 code | 502 blank | 518 comment | 334 complexity | 637692940e3923f1b2ec84eae7872a22 MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. require_once( dirname(__FILE__).'/Cache.php');
  3. /**
  4. * Class for parsing and compiling less files into css
  5. *
  6. * @package Less
  7. * @subpackage parser
  8. *
  9. */
  10. class Less_Parser{
  11. /**
  12. * Default parser options
  13. */
  14. public static $default_options = array(
  15. 'compress' => false, // option - whether to compress
  16. 'strictUnits' => false, // whether units need to evaluate correctly
  17. 'strictMath' => false, // whether math has to be within parenthesis
  18. 'relativeUrls' => true, // option - whether to adjust URL's to be relative
  19. 'urlArgs' => '', // whether to add args into url tokens
  20. 'numPrecision' => 8,
  21. 'import_dirs' => array(),
  22. 'import_callback' => null,
  23. 'cache_dir' => null,
  24. 'cache_method' => 'php', // false, 'serialize', 'php', 'var_export', 'callback';
  25. 'cache_callback_get' => null,
  26. 'cache_callback_set' => null,
  27. 'sourceMap' => false, // whether to output a source map
  28. 'sourceMapBasepath' => null,
  29. 'sourceMapWriteTo' => null,
  30. 'sourceMapURL' => null,
  31. 'indentation' => ' ',
  32. 'plugins' => array(),
  33. );
  34. public static $options = array();
  35. private $input; // Less input string
  36. private $input_len; // input string length
  37. private $pos; // current index in `input`
  38. private $saveStack = array(); // holds state for backtracking
  39. private $furthest;
  40. private $mb_internal_encoding = ''; // for remember exists value of mbstring.internal_encoding
  41. /**
  42. * @var Less_Environment
  43. */
  44. private $env;
  45. protected $rules = array();
  46. private static $imports = array();
  47. public static $has_extends = false;
  48. public static $next_id = 0;
  49. /**
  50. * Filename to contents of all parsed the files
  51. *
  52. * @var array
  53. */
  54. public static $contentsMap = array();
  55. /**
  56. * @param Less_Environment|array|null $env
  57. */
  58. public function __construct( $env = null ){
  59. // Top parser on an import tree must be sure there is one "env"
  60. // which will then be passed around by reference.
  61. if( $env instanceof Less_Environment ){
  62. $this->env = $env;
  63. }else{
  64. $this->SetOptions(Less_Parser::$default_options);
  65. $this->Reset( $env );
  66. }
  67. // mbstring.func_overload > 1 bugfix
  68. // The encoding value must be set for each source file,
  69. // therefore, to conserve resources and improve the speed of this design is taken here
  70. if (ini_get('mbstring.func_overload')) {
  71. $this->mb_internal_encoding = ini_get('mbstring.internal_encoding');
  72. @ini_set('mbstring.internal_encoding', 'ascii');
  73. }
  74. }
  75. /**
  76. * Reset the parser state completely
  77. *
  78. */
  79. public function Reset( $options = null ){
  80. $this->rules = array();
  81. self::$imports = array();
  82. self::$has_extends = false;
  83. self::$imports = array();
  84. self::$contentsMap = array();
  85. $this->env = new Less_Environment($options);
  86. $this->env->Init();
  87. //set new options
  88. if( is_array($options) ){
  89. $this->SetOptions(Less_Parser::$default_options);
  90. $this->SetOptions($options);
  91. }
  92. }
  93. /**
  94. * Set one or more compiler options
  95. * options: import_dirs, cache_dir, cache_method
  96. *
  97. */
  98. public function SetOptions( $options ){
  99. foreach($options as $option => $value){
  100. $this->SetOption($option,$value);
  101. }
  102. }
  103. /**
  104. * Set one compiler option
  105. *
  106. */
  107. public function SetOption($option,$value){
  108. switch($option){
  109. case 'import_dirs':
  110. $this->SetImportDirs($value);
  111. return;
  112. case 'cache_dir':
  113. if( is_string($value) ){
  114. Less_Cache::SetCacheDir($value);
  115. Less_Cache::CheckCacheDir();
  116. }
  117. return;
  118. }
  119. Less_Parser::$options[$option] = $value;
  120. }
  121. /**
  122. * Registers a new custom function
  123. *
  124. * @param string $name function name
  125. * @param callable $callback callback
  126. */
  127. public function registerFunction($name, $callback) {
  128. $this->env->functions[$name] = $callback;
  129. }
  130. /**
  131. * Removed an already registered function
  132. *
  133. * @param string $name function name
  134. */
  135. public function unregisterFunction($name) {
  136. if( isset($this->env->functions[$name]) )
  137. unset($this->env->functions[$name]);
  138. }
  139. /**
  140. * Get the current css buffer
  141. *
  142. * @return string
  143. */
  144. public function getCss(){
  145. $precision = ini_get('precision');
  146. @ini_set('precision',16);
  147. $locale = setlocale(LC_NUMERIC, 0);
  148. setlocale(LC_NUMERIC, "C");
  149. try {
  150. $root = new Less_Tree_Ruleset(array(), $this->rules );
  151. $root->root = true;
  152. $root->firstRoot = true;
  153. $this->PreVisitors($root);
  154. self::$has_extends = false;
  155. $evaldRoot = $root->compile($this->env);
  156. $this->PostVisitors($evaldRoot);
  157. if( Less_Parser::$options['sourceMap'] ){
  158. $generator = new Less_SourceMap_Generator($evaldRoot, Less_Parser::$contentsMap, Less_Parser::$options );
  159. // will also save file
  160. // FIXME: should happen somewhere else?
  161. $css = $generator->generateCSS();
  162. }else{
  163. $css = $evaldRoot->toCSS();
  164. }
  165. if( Less_Parser::$options['compress'] ){
  166. $css = preg_replace('/(^(\s)+)|((\s)+$)/', '', $css);
  167. }
  168. } catch (Exception $exc) {
  169. // Intentional fall-through so we can reset environment
  170. }
  171. //reset php settings
  172. @ini_set('precision',$precision);
  173. setlocale(LC_NUMERIC, $locale);
  174. // If you previously defined $this->mb_internal_encoding
  175. // is required to return the encoding as it was before
  176. if ($this->mb_internal_encoding != '') {
  177. @ini_set("mbstring.internal_encoding", $this->mb_internal_encoding);
  178. $this->mb_internal_encoding = '';
  179. }
  180. // Rethrow exception after we handled resetting the environment
  181. if (!empty($exc)) {
  182. throw $exc;
  183. }
  184. return $css;
  185. }
  186. /**
  187. * Run pre-compile visitors
  188. *
  189. */
  190. private function PreVisitors($root){
  191. if( Less_Parser::$options['plugins'] ){
  192. foreach(Less_Parser::$options['plugins'] as $plugin){
  193. if( !empty($plugin->isPreEvalVisitor) ){
  194. $plugin->run($root);
  195. }
  196. }
  197. }
  198. }
  199. /**
  200. * Run post-compile visitors
  201. *
  202. */
  203. private function PostVisitors($evaldRoot){
  204. $visitors = array();
  205. $visitors[] = new Less_Visitor_joinSelector();
  206. if( self::$has_extends ){
  207. $visitors[] = new Less_Visitor_processExtends();
  208. }
  209. $visitors[] = new Less_Visitor_toCSS();
  210. if( Less_Parser::$options['plugins'] ){
  211. foreach(Less_Parser::$options['plugins'] as $plugin){
  212. if( property_exists($plugin,'isPreEvalVisitor') && $plugin->isPreEvalVisitor ){
  213. continue;
  214. }
  215. if( property_exists($plugin,'isPreVisitor') && $plugin->isPreVisitor ){
  216. array_unshift( $visitors, $plugin);
  217. }else{
  218. $visitors[] = $plugin;
  219. }
  220. }
  221. }
  222. for($i = 0; $i < count($visitors); $i++ ){
  223. $visitors[$i]->run($evaldRoot);
  224. }
  225. }
  226. /**
  227. * Parse a Less string into css
  228. *
  229. * @param string $str The string to convert
  230. * @param string $uri_root The url of the file
  231. * @return Less_Tree_Ruleset|Less_Parser
  232. */
  233. public function parse( $str, $file_uri = null ){
  234. if( !$file_uri ){
  235. $uri_root = '';
  236. $filename = 'anonymous-file-'.Less_Parser::$next_id++.'.less';
  237. }else{
  238. $file_uri = self::WinPath($file_uri);
  239. $filename = $file_uri;
  240. $uri_root = dirname($file_uri);
  241. }
  242. $previousFileInfo = $this->env->currentFileInfo;
  243. $uri_root = self::WinPath($uri_root);
  244. $this->SetFileInfo($filename, $uri_root);
  245. $this->input = $str;
  246. $this->_parse();
  247. if( $previousFileInfo ){
  248. $this->env->currentFileInfo = $previousFileInfo;
  249. }
  250. return $this;
  251. }
  252. /**
  253. * Parse a Less string from a given file
  254. *
  255. * @throws Less_Exception_Parser
  256. * @param string $filename The file to parse
  257. * @param string $uri_root The url of the file
  258. * @param bool $returnRoot Indicates whether the return value should be a css string a root node
  259. * @return Less_Tree_Ruleset|Less_Parser
  260. */
  261. public function parseFile( $filename, $uri_root = '', $returnRoot = false){
  262. if( !file_exists($filename) ){
  263. $this->Error(sprintf('File `%s` not found.', $filename));
  264. }
  265. // fix uri_root?
  266. // Instead of The mixture of file path for the first argument and directory path for the second argument has bee
  267. if( !$returnRoot && !empty($uri_root) && basename($uri_root) == basename($filename) ){
  268. $uri_root = dirname($uri_root);
  269. }
  270. $previousFileInfo = $this->env->currentFileInfo;
  271. if( $filename ){
  272. $filename = self::WinPath(realpath($filename));
  273. }
  274. $uri_root = self::WinPath($uri_root);
  275. $this->SetFileInfo($filename, $uri_root);
  276. self::AddParsedFile($filename);
  277. if( $returnRoot ){
  278. $rules = $this->GetRules( $filename );
  279. $return = new Less_Tree_Ruleset(array(), $rules );
  280. }else{
  281. $this->_parse( $filename );
  282. $return = $this;
  283. }
  284. if( $previousFileInfo ){
  285. $this->env->currentFileInfo = $previousFileInfo;
  286. }
  287. return $return;
  288. }
  289. /**
  290. * Allows a user to set variables values
  291. * @param array $vars
  292. * @return Less_Parser
  293. */
  294. public function ModifyVars( $vars ){
  295. $this->input = Less_Parser::serializeVars( $vars );
  296. $this->_parse();
  297. return $this;
  298. }
  299. /**
  300. * @param string $filename
  301. */
  302. public function SetFileInfo( $filename, $uri_root = ''){
  303. $filename = Less_Environment::normalizePath($filename);
  304. $dirname = preg_replace('/[^\/\\\\]*$/','',$filename);
  305. if( !empty($uri_root) ){
  306. $uri_root = rtrim($uri_root,'/').'/';
  307. }
  308. $currentFileInfo = array();
  309. //entry info
  310. if( isset($this->env->currentFileInfo) ){
  311. $currentFileInfo['entryPath'] = $this->env->currentFileInfo['entryPath'];
  312. $currentFileInfo['entryUri'] = $this->env->currentFileInfo['entryUri'];
  313. $currentFileInfo['rootpath'] = $this->env->currentFileInfo['rootpath'];
  314. }else{
  315. $currentFileInfo['entryPath'] = $dirname;
  316. $currentFileInfo['entryUri'] = $uri_root;
  317. $currentFileInfo['rootpath'] = $dirname;
  318. }
  319. $currentFileInfo['currentDirectory'] = $dirname;
  320. $currentFileInfo['currentUri'] = $uri_root.basename($filename);
  321. $currentFileInfo['filename'] = $filename;
  322. $currentFileInfo['uri_root'] = $uri_root;
  323. //inherit reference
  324. if( isset($this->env->currentFileInfo['reference']) && $this->env->currentFileInfo['reference'] ){
  325. $currentFileInfo['reference'] = true;
  326. }
  327. $this->env->currentFileInfo = $currentFileInfo;
  328. }
  329. /**
  330. * @deprecated 1.5.1.2
  331. *
  332. */
  333. public function SetCacheDir( $dir ){
  334. if( !file_exists($dir) ){
  335. if( mkdir($dir) ){
  336. return true;
  337. }
  338. throw new Less_Exception_Parser('Less.php cache directory couldn\'t be created: '.$dir);
  339. }elseif( !is_dir($dir) ){
  340. throw new Less_Exception_Parser('Less.php cache directory doesn\'t exist: '.$dir);
  341. }elseif( !is_writable($dir) ){
  342. throw new Less_Exception_Parser('Less.php cache directory isn\'t writable: '.$dir);
  343. }else{
  344. $dir = self::WinPath($dir);
  345. Less_Cache::$cache_dir = rtrim($dir,'/').'/';
  346. return true;
  347. }
  348. }
  349. /**
  350. * Set a list of directories or callbacks the parser should use for determining import paths
  351. *
  352. * @param array $dirs
  353. */
  354. public function SetImportDirs( $dirs ){
  355. Less_Parser::$options['import_dirs'] = array();
  356. foreach($dirs as $path => $uri_root){
  357. $path = self::WinPath($path);
  358. if( !empty($path) ){
  359. $path = rtrim($path,'/').'/';
  360. }
  361. if ( !is_callable($uri_root) ){
  362. $uri_root = self::WinPath($uri_root);
  363. if( !empty($uri_root) ){
  364. $uri_root = rtrim($uri_root,'/').'/';
  365. }
  366. }
  367. Less_Parser::$options['import_dirs'][$path] = $uri_root;
  368. }
  369. }
  370. /**
  371. * @param string $file_path
  372. */
  373. private function _parse( $file_path = null ){
  374. $this->rules = array_merge($this->rules, $this->GetRules( $file_path ));
  375. }
  376. /**
  377. * Return the results of parsePrimary for $file_path
  378. * Use cache and save cached results if possible
  379. *
  380. * @param string|null $file_path
  381. */
  382. private function GetRules( $file_path ){
  383. $this->SetInput($file_path);
  384. $cache_file = $this->CacheFile( $file_path );
  385. if( $cache_file ){
  386. if( Less_Parser::$options['cache_method'] == 'callback' ){
  387. if( is_callable(Less_Parser::$options['cache_callback_get']) ){
  388. $cache = call_user_func_array(
  389. Less_Parser::$options['cache_callback_get'],
  390. array($this, $file_path, $cache_file)
  391. );
  392. if( $cache ){
  393. $this->UnsetInput();
  394. return $cache;
  395. }
  396. }
  397. }elseif( file_exists($cache_file) ){
  398. switch(Less_Parser::$options['cache_method']){
  399. // Using serialize
  400. // Faster but uses more memory
  401. case 'serialize':
  402. $cache = unserialize(file_get_contents($cache_file));
  403. if( $cache ){
  404. touch($cache_file);
  405. $this->UnsetInput();
  406. return $cache;
  407. }
  408. break;
  409. // Using generated php code
  410. case 'var_export':
  411. case 'php':
  412. $this->UnsetInput();
  413. return include($cache_file);
  414. }
  415. }
  416. }
  417. $rules = $this->parsePrimary();
  418. if( $this->pos < $this->input_len ){
  419. throw new Less_Exception_Chunk($this->input, null, $this->furthest, $this->env->currentFileInfo);
  420. }
  421. $this->UnsetInput();
  422. //save the cache
  423. if( $cache_file ){
  424. if( Less_Parser::$options['cache_method'] == 'callback' ){
  425. if( is_callable(Less_Parser::$options['cache_callback_set']) ){
  426. call_user_func_array(
  427. Less_Parser::$options['cache_callback_set'],
  428. array($this, $file_path, $cache_file, $rules)
  429. );
  430. }
  431. }else{
  432. //msg('write cache file');
  433. switch(Less_Parser::$options['cache_method']){
  434. case 'serialize':
  435. file_put_contents( $cache_file, serialize($rules) );
  436. break;
  437. case 'php':
  438. file_put_contents( $cache_file, '<?php return '.self::ArgString($rules).'; ?>' );
  439. break;
  440. case 'var_export':
  441. //Requires __set_state()
  442. file_put_contents( $cache_file, '<?php return '.var_export($rules,true).'; ?>' );
  443. break;
  444. }
  445. Less_Cache::CleanCache();
  446. }
  447. }
  448. return $rules;
  449. }
  450. /**
  451. * Set up the input buffer
  452. *
  453. */
  454. public function SetInput( $file_path ){
  455. if( $file_path ){
  456. $this->input = file_get_contents( $file_path );
  457. }
  458. $this->pos = $this->furthest = 0;
  459. // Remove potential UTF Byte Order Mark
  460. $this->input = preg_replace('/\\G\xEF\xBB\xBF/', '', $this->input);
  461. $this->input_len = strlen($this->input);
  462. if( Less_Parser::$options['sourceMap'] && $this->env->currentFileInfo ){
  463. $uri = $this->env->currentFileInfo['currentUri'];
  464. Less_Parser::$contentsMap[$uri] = $this->input;
  465. }
  466. }
  467. /**
  468. * Free up some memory
  469. *
  470. */
  471. public function UnsetInput(){
  472. unset($this->input, $this->pos, $this->input_len, $this->furthest);
  473. $this->saveStack = array();
  474. }
  475. public function CacheFile( $file_path ){
  476. if( $file_path && $this->CacheEnabled() ){
  477. $env = get_object_vars($this->env);
  478. unset($env['frames']);
  479. $parts = array();
  480. $parts[] = $file_path;
  481. $parts[] = filesize( $file_path );
  482. $parts[] = filemtime( $file_path );
  483. $parts[] = $env;
  484. $parts[] = Less_Version::cache_version;
  485. $parts[] = Less_Parser::$options['cache_method'];
  486. return Less_Cache::$cache_dir . Less_Cache::$prefix . base_convert( sha1(json_encode($parts) ), 16, 36) . '.lesscache';
  487. }
  488. }
  489. static function AddParsedFile($file){
  490. self::$imports[] = $file;
  491. }
  492. static function AllParsedFiles(){
  493. return self::$imports;
  494. }
  495. /**
  496. * @param string $file
  497. */
  498. static function FileParsed($file){
  499. return in_array($file,self::$imports);
  500. }
  501. function save() {
  502. $this->saveStack[] = $this->pos;
  503. }
  504. private function restore() {
  505. $this->pos = array_pop($this->saveStack);
  506. }
  507. private function forget(){
  508. array_pop($this->saveStack);
  509. }
  510. private function isWhitespace($offset = 0) {
  511. return preg_match('/\s/',$this->input[ $this->pos + $offset]);
  512. }
  513. /**
  514. * Parse from a token, regexp or string, and move forward if match
  515. *
  516. * @param array $toks
  517. * @return array
  518. */
  519. private function match($toks){
  520. // The match is confirmed, add the match length to `this::pos`,
  521. // and consume any extra white-space characters (' ' || '\n')
  522. // which come after that. The reason for this is that LeSS's
  523. // grammar is mostly white-space insensitive.
  524. //
  525. foreach($toks as $tok){
  526. $char = $tok[0];
  527. if( $char === '/' ){
  528. $match = $this->MatchReg($tok);
  529. if( $match ){
  530. return count($match) === 1 ? $match[0] : $match;
  531. }
  532. }elseif( $char === '#' ){
  533. $match = $this->MatchChar($tok[1]);
  534. }else{
  535. // Non-terminal, match using a function call
  536. $match = $this->$tok();
  537. }
  538. if( $match ){
  539. return $match;
  540. }
  541. }
  542. }
  543. /**
  544. * @param string[] $toks
  545. *
  546. * @return string
  547. */
  548. private function MatchFuncs($toks){
  549. if( $this->pos < $this->input_len ){
  550. foreach($toks as $tok){
  551. $match = $this->$tok();
  552. if( $match ){
  553. return $match;
  554. }
  555. }
  556. }
  557. }
  558. // Match a single character in the input,
  559. private function MatchChar($tok){
  560. if( ($this->pos < $this->input_len) && ($this->input[$this->pos] === $tok) ){
  561. $this->skipWhitespace(1);
  562. return $tok;
  563. }
  564. }
  565. // Match a regexp from the current start point
  566. private function MatchReg($tok){
  567. if( preg_match($tok, $this->input, $match, 0, $this->pos) ){
  568. $this->skipWhitespace(strlen($match[0]));
  569. return $match;
  570. }
  571. }
  572. /**
  573. * Same as match(), but don't change the state of the parser,
  574. * just return the match.
  575. *
  576. * @param string $tok
  577. * @return integer
  578. */
  579. public function PeekReg($tok){
  580. return preg_match($tok, $this->input, $match, 0, $this->pos);
  581. }
  582. /**
  583. * @param string $tok
  584. */
  585. public function PeekChar($tok){
  586. //return ($this->input[$this->pos] === $tok );
  587. return ($this->pos < $this->input_len) && ($this->input[$this->pos] === $tok );
  588. }
  589. /**
  590. * @param integer $length
  591. */
  592. public function skipWhitespace($length){
  593. $this->pos += $length;
  594. for(; $this->pos < $this->input_len; $this->pos++ ){
  595. $c = $this->input[$this->pos];
  596. if( ($c !== "\n") && ($c !== "\r") && ($c !== "\t") && ($c !== ' ') ){
  597. break;
  598. }
  599. }
  600. }
  601. /**
  602. * @param string $tok
  603. * @param string|null $msg
  604. */
  605. public function expect($tok, $msg = NULL) {
  606. $result = $this->match( array($tok) );
  607. if (!$result) {
  608. $this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg );
  609. } else {
  610. return $result;
  611. }
  612. }
  613. /**
  614. * @param string $tok
  615. */
  616. public function expectChar($tok, $msg = null ){
  617. $result = $this->MatchChar($tok);
  618. if( !$result ){
  619. $this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg );
  620. }else{
  621. return $result;
  622. }
  623. }
  624. //
  625. // Here in, the parsing rules/functions
  626. //
  627. // The basic structure of the syntax tree generated is as follows:
  628. //
  629. // Ruleset -> Rule -> Value -> Expression -> Entity
  630. //
  631. // Here's some LESS code:
  632. //
  633. // .class {
  634. // color: #fff;
  635. // border: 1px solid #000;
  636. // width: @w + 4px;
  637. // > .child {...}
  638. // }
  639. //
  640. // And here's what the parse tree might look like:
  641. //
  642. // Ruleset (Selector '.class', [
  643. // Rule ("color", Value ([Expression [Color #fff]]))
  644. // Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]]))
  645. // Rule ("width", Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]]))
  646. // Ruleset (Selector [Element '>', '.child'], [...])
  647. // ])
  648. //
  649. // In general, most rules will try to parse a token with the `$()` function, and if the return
  650. // value is truly, will return a new node, of the relevant type. Sometimes, we need to check
  651. // first, before parsing, that's when we use `peek()`.
  652. //
  653. //
  654. // The `primary` rule is the *entry* and *exit* point of the parser.
  655. // The rules here can appear at any level of the parse tree.
  656. //
  657. // The recursive nature of the grammar is an interplay between the `block`
  658. // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule,
  659. // as represented by this simplified grammar:
  660. //
  661. // primary → (ruleset | rule)+
  662. // ruleset → selector+ block
  663. // block → '{' primary '}'
  664. //
  665. // Only at one point is the primary rule not called from the
  666. // block rule: at the root level.
  667. //
  668. private function parsePrimary(){
  669. $root = array();
  670. while( true ){
  671. if( $this->pos >= $this->input_len ){
  672. break;
  673. }
  674. $node = $this->parseExtend(true);
  675. if( $node ){
  676. $root = array_merge($root,$node);
  677. continue;
  678. }
  679. //$node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseDirective'));
  680. $node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseNameValue', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseRulesetCall', 'parseDirective'));
  681. if( $node ){
  682. $root[] = $node;
  683. }elseif( !$this->MatchReg('/\\G[\s\n;]+/') ){
  684. break;
  685. }
  686. if( $this->PeekChar('}') ){
  687. break;
  688. }
  689. }
  690. return $root;
  691. }
  692. // We create a Comment node for CSS comments `/* */`,
  693. // but keep the LeSS comments `//` silent, by just skipping
  694. // over them.
  695. private function parseComment(){
  696. if( $this->input[$this->pos] !== '/' ){
  697. return;
  698. }
  699. if( $this->input[$this->pos+1] === '/' ){
  700. $match = $this->MatchReg('/\\G\/\/.*/');
  701. return $this->NewObj4('Less_Tree_Comment',array($match[0], true, $this->pos, $this->env->currentFileInfo));
  702. }
  703. //$comment = $this->MatchReg('/\\G\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/');
  704. $comment = $this->MatchReg('/\\G\/\*(?s).*?\*+\/\n?/');//not the same as less.js to prevent fatal errors
  705. if( $comment ){
  706. return $this->NewObj4('Less_Tree_Comment',array($comment[0], false, $this->pos, $this->env->currentFileInfo));
  707. }
  708. }
  709. private function parseComments(){
  710. $comments = array();
  711. while( $this->pos < $this->input_len ){
  712. $comment = $this->parseComment();
  713. if( !$comment ){
  714. break;
  715. }
  716. $comments[] = $comment;
  717. }
  718. return $comments;
  719. }
  720. //
  721. // A string, which supports escaping " and '
  722. //
  723. // "milky way" 'he\'s the one!'
  724. //
  725. private function parseEntitiesQuoted() {
  726. $j = $this->pos;
  727. $e = false;
  728. $index = $this->pos;
  729. if( $this->input[$this->pos] === '~' ){
  730. $j++;
  731. $e = true; // Escaped strings
  732. }
  733. if( $this->input[$j] != '"' && $this->input[$j] !== "'" ){
  734. return;
  735. }
  736. if ($e) {
  737. $this->MatchChar('~');
  738. }
  739. // Fix for #124: match escaped newlines
  740. //$str = $this->MatchReg('/\\G"((?:[^"\\\\\r\n]|\\\\.)*)"|\'((?:[^\'\\\\\r\n]|\\\\.)*)\'/');
  741. $str = $this->MatchReg('/\\G"((?:[^"\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)"|\'((?:[^\'\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)\'/');
  742. if( $str ){
  743. $result = $str[0][0] == '"' ? $str[1] : $str[2];
  744. return $this->NewObj5('Less_Tree_Quoted',array($str[0], $result, $e, $index, $this->env->currentFileInfo) );
  745. }
  746. return;
  747. }
  748. //
  749. // A catch-all word, such as:
  750. //
  751. // black border-collapse
  752. //
  753. private function parseEntitiesKeyword(){
  754. //$k = $this->MatchReg('/\\G[_A-Za-z-][_A-Za-z0-9-]*/');
  755. $k = $this->MatchReg('/\\G%|\\G[_A-Za-z-][_A-Za-z0-9-]*/');
  756. if( $k ){
  757. $k = $k[0];
  758. $color = $this->fromKeyword($k);
  759. if( $color ){
  760. return $color;
  761. }
  762. return $this->NewObj1('Less_Tree_Keyword',$k);
  763. }
  764. }
  765. // duplicate of Less_Tree_Color::FromKeyword
  766. private function FromKeyword( $keyword ){
  767. $keyword = strtolower($keyword);
  768. if( Less_Colors::hasOwnProperty($keyword) ){
  769. // detect named color
  770. return $this->NewObj1('Less_Tree_Color',substr(Less_Colors::color($keyword), 1));
  771. }
  772. if( $keyword === 'transparent' ){
  773. return $this->NewObj3('Less_Tree_Color', array( array(0, 0, 0), 0, true));
  774. }
  775. }
  776. //
  777. // A function call
  778. //
  779. // rgb(255, 0, 255)
  780. //
  781. // We also try to catch IE's `alpha()`, but let the `alpha` parser
  782. // deal with the details.
  783. //
  784. // The arguments are parsed with the `entities.arguments` parser.
  785. //
  786. private function parseEntitiesCall(){
  787. $index = $this->pos;
  788. if( !preg_match('/\\G([\w-]+|%|progid:[\w\.]+)\(/', $this->input, $name,0,$this->pos) ){
  789. return;
  790. }
  791. $name = $name[1];
  792. $nameLC = strtolower($name);
  793. if ($nameLC === 'url') {
  794. return null;
  795. }
  796. $this->pos += strlen($name);
  797. if( $nameLC === 'alpha' ){
  798. $alpha_ret = $this->parseAlpha();
  799. if( $alpha_ret ){
  800. return $alpha_ret;
  801. }
  802. }
  803. $this->MatchChar('('); // Parse the '(' and consume whitespace.
  804. $args = $this->parseEntitiesArguments();
  805. if( !$this->MatchChar(')') ){
  806. return;
  807. }
  808. if ($name) {
  809. return $this->NewObj4('Less_Tree_Call',array($name, $args, $index, $this->env->currentFileInfo) );
  810. }
  811. }
  812. /**
  813. * Parse a list of arguments
  814. *
  815. * @return array
  816. */
  817. private function parseEntitiesArguments(){
  818. $args = array();
  819. while( true ){
  820. $arg = $this->MatchFuncs( array('parseEntitiesAssignment','parseExpression') );
  821. if( !$arg ){
  822. break;
  823. }
  824. $args[] = $arg;
  825. if( !$this->MatchChar(',') ){
  826. break;
  827. }
  828. }
  829. return $args;
  830. }
  831. private function parseEntitiesLiteral(){
  832. return $this->MatchFuncs( array('parseEntitiesDimension','parseEntitiesColor','parseEntitiesQuoted','parseUnicodeDescriptor') );
  833. }
  834. // Assignments are argument entities for calls.
  835. // They are present in ie filter properties as shown below.
  836. //
  837. // filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* )
  838. //
  839. private function parseEntitiesAssignment() {
  840. $key = $this->MatchReg('/\\G\w+(?=\s?=)/');
  841. if( !$key ){
  842. return;
  843. }
  844. if( !$this->MatchChar('=') ){
  845. return;
  846. }
  847. $value = $this->parseEntity();
  848. if( $value ){
  849. return $this->NewObj2('Less_Tree_Assignment',array($key[0], $value));
  850. }
  851. }
  852. //
  853. // Parse url() tokens
  854. //
  855. // We use a specific rule for urls, because they don't really behave like
  856. // standard function calls. The difference is that the argument doesn't have
  857. // to be enclosed within a string, so it can't be parsed as an Expression.
  858. //
  859. private function parseEntitiesUrl(){
  860. if( $this->input[$this->pos] !== 'u' || !$this->matchReg('/\\Gurl\(/') ){
  861. return;
  862. }
  863. $value = $this->match( array('parseEntitiesQuoted','parseEntitiesVariable','/\\Gdata\:.*?[^\)]+/','/\\G(?:(?:\\\\[\(\)\'"])|[^\(\)\'"])+/') );
  864. if( !$value ){
  865. $value = '';
  866. }
  867. $this->expectChar(')');
  868. if( isset($value->value) || $value instanceof Less_Tree_Variable ){
  869. return $this->NewObj2('Less_Tree_Url',array($value, $this->env->currentFileInfo));
  870. }
  871. return $this->NewObj2('Less_Tree_Url', array( $this->NewObj1('Less_Tree_Anonymous',$value), $this->env->currentFileInfo) );
  872. }
  873. //
  874. // A Variable entity, such as `@fink`, in
  875. //
  876. // width: @fink + 2px
  877. //
  878. // We use a different parser for variable definitions,
  879. // see `parsers.variable`.
  880. //
  881. private function parseEntitiesVariable(){
  882. $index = $this->pos;
  883. if ($this->PeekChar('@') && ($name = $this->MatchReg('/\\G@@?[\w-]+/'))) {
  884. return $this->NewObj3('Less_Tree_Variable', array( $name[0], $index, $this->env->currentFileInfo));
  885. }
  886. }
  887. // A variable entity useing the protective {} e.g. @{var}
  888. private function parseEntitiesVariableCurly() {
  889. $index = $this->pos;
  890. if( $this->input_len > ($this->pos+1) && $this->input[$this->pos] === '@' && ($curly = $this->MatchReg('/\\G@\{([\w-]+)\}/')) ){
  891. return $this->NewObj3('Less_Tree_Variable',array('@'.$curly[1], $index, $this->env->currentFileInfo));
  892. }
  893. }
  894. //
  895. // A Hexadecimal color
  896. //
  897. // #4F3C2F
  898. //
  899. // `rgb` and `hsl` colors are parsed through the `entities.call` parser.
  900. //
  901. private function parseEntitiesColor(){
  902. if ($this->PeekChar('#') && ($rgb = $this->MatchReg('/\\G#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/'))) {
  903. return $this->NewObj1('Less_Tree_Color',$rgb[1]);
  904. }
  905. }
  906. //
  907. // A Dimension, that is, a number and a unit
  908. //
  909. // 0.5em 95%
  910. //
  911. private function parseEntitiesDimension(){
  912. $c = @ord($this->input[$this->pos]);
  913. //Is the first char of the dimension 0-9, '.', '+' or '-'
  914. if (($c > 57 || $c < 43) || $c === 47 || $c == 44){
  915. return;
  916. }
  917. $value = $this->MatchReg('/\\G([+-]?\d*\.?\d+)(%|[a-z]+)?/');
  918. if( $value ){
  919. if( isset($value[2]) ){
  920. return $this->NewObj2('Less_Tree_Dimension', array($value[1],$value[2]));
  921. }
  922. return $this->NewObj1('Less_Tree_Dimension',$value[1]);
  923. }
  924. }
  925. //
  926. // A unicode descriptor, as is used in unicode-range
  927. //
  928. // U+0?? or U+00A1-00A9
  929. //
  930. function parseUnicodeDescriptor() {
  931. $ud = $this->MatchReg('/\\G(U\+[0-9a-fA-F?]+)(\-[0-9a-fA-F?]+)?/');
  932. if( $ud ){
  933. return $this->NewObj1('Less_Tree_UnicodeDescriptor', $ud[0]);
  934. }
  935. }
  936. //
  937. // JavaScript code to be evaluated
  938. //
  939. // `window.location.href`
  940. //
  941. private function parseEntitiesJavascript(){
  942. $e = false;
  943. $j = $this->pos;
  944. if( $this->input[$j] === '~' ){
  945. $j++;
  946. $e = true;
  947. }
  948. if( $this->input[$j] !== '`' ){
  949. return;
  950. }
  951. if( $e ){
  952. $this->MatchChar('~');
  953. }
  954. $str = $this->MatchReg('/\\G`([^`]*)`/');
  955. if( $str ){
  956. return $this->NewObj3('Less_Tree_Javascript', array($str[1], $this->pos, $e));
  957. }
  958. }
  959. //
  960. // The variable part of a variable definition. Used in the `rule` parser
  961. //
  962. // @fink:
  963. //
  964. private function parseVariable(){
  965. if ($this->PeekChar('@') && ($name = $this->MatchReg('/\\G(@[\w-]+)\s*:/'))) {
  966. return $name[1];
  967. }
  968. }
  969. //
  970. // The variable part of a variable definition. Used in the `rule` parser
  971. //
  972. // @fink();
  973. //
  974. private function parseRulesetCall(){
  975. if( $this->input[$this->pos] === '@' && ($name = $this->MatchReg('/\\G(@[\w-]+)\s*\(\s*\)\s*;/')) ){
  976. return $this->NewObj1('Less_Tree_RulesetCall', $name[1] );
  977. }
  978. }
  979. //
  980. // extend syntax - used to extend selectors
  981. //
  982. function parseExtend($isRule = false){
  983. $index = $this->pos;
  984. $extendList = array();
  985. if( !$this->MatchReg( $isRule ? '/\\G&:extend\(/' : '/\\G:extend\(/' ) ){ return; }
  986. do{
  987. $option = null;
  988. $elements = array();
  989. while( true ){
  990. $option = $this->MatchReg('/\\G(all)(?=\s*(\)|,))/');
  991. if( $option ){ break; }
  992. $e = $this->parseElement();
  993. if( !$e ){ break; }
  994. $elements[] = $e;
  995. }
  996. if( $option ){
  997. $option = $option[1];
  998. }
  999. $extendList[] = $this->NewObj3('Less_Tree_Extend', array( $this->NewObj1('Less_Tree_Selector',$elements), $option, $index ));
  1000. }while( $this->MatchChar(",") );
  1001. $this->expect('/\\G\)/');
  1002. if( $isRule ){
  1003. $this->expect('/\\G;/');
  1004. }
  1005. return $extendList;
  1006. }
  1007. //
  1008. // A Mixin call, with an optional argument list
  1009. //
  1010. // #mixins > .square(#fff);
  1011. // .rounded(4px, black);
  1012. // .button;
  1013. //
  1014. // The `while` loop is there because mixins can be
  1015. // namespaced, but we only support the child and descendant
  1016. // selector for now.
  1017. //
  1018. private function parseMixinCall(){
  1019. $char = $this->input[$this->pos];
  1020. if( $char !== '.' && $char !== '#' ){
  1021. return;
  1022. }
  1023. $index = $this->pos;
  1024. $this->save(); // stop us absorbing part of an invalid selector
  1025. $elements = $this->parseMixinCallElements();
  1026. if( $elements ){
  1027. if( $this->MatchChar('(') ){
  1028. $returned = $this->parseMixinArgs(true);
  1029. $args = $returned['args'];
  1030. $this->expectChar(')');
  1031. }else{
  1032. $args = array();
  1033. }
  1034. $important = $this->parseImportant();
  1035. if( $this->parseEnd() ){
  1036. $this->forget();
  1037. return $this->NewObj5('Less_Tree_Mixin_Call', array( $elements, $args, $index, $this->env->currentFileInfo, $important));
  1038. }
  1039. }
  1040. $this->restore();
  1041. }
  1042. private function parseMixinCallElements(){
  1043. $elements = array();
  1044. $c = null;
  1045. while( true ){
  1046. $elemIndex = $this->pos;
  1047. $e = $this->MatchReg('/\\G[#.](?:[\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/');
  1048. if( !$e ){
  1049. break;
  1050. }
  1051. $elements[] = $this->NewObj4('Less_Tree_Element', array($c, $e[0], $elemIndex, $this->env->currentFileInfo));
  1052. $c = $this->MatchChar('>');
  1053. }
  1054. return $elements;
  1055. }
  1056. /**
  1057. * @param boolean $isCall
  1058. */
  1059. private function parseMixinArgs( $isCall ){
  1060. $expressions = array();
  1061. $argsSemiColon = array();
  1062. $isSemiColonSeperated = null;
  1063. $argsComma = array();
  1064. $expressionContainsNamed = null;
  1065. $name = null;
  1066. $returner = array('args'=>array(), 'variadic'=> false);
  1067. $this->save();
  1068. while( true ){
  1069. if( $isCall ){
  1070. $arg = $this->MatchFuncs( array( 'parseDetachedRuleset','parseExpression' ) );
  1071. } else {
  1072. $this->parseComments();
  1073. if( $this->input[ $this->pos ] === '.' && $this->MatchReg('/\\G\.{3}/') ){
  1074. $returner['variadic'] = true;
  1075. if( $this->MatchChar(";") && !$isSemiColonSeperated ){
  1076. $isSemiColonSeperated = true;
  1077. }
  1078. if( $isSemiColonSeperated ){
  1079. $argsSemiColon[] = array('variadic'=>true);
  1080. }else{
  1081. $argsComma[] = array('variadic'=>true);
  1082. }
  1083. break;
  1084. }
  1085. $arg = $this->MatchFuncs( array('parseEntitiesVariable','parseEntitiesLiteral','parseEntitiesKeyword') );
  1086. }
  1087. if( !$arg ){
  1088. break;
  1089. }
  1090. $nameLoop = null;
  1091. if( $arg instanceof Less_Tree_Expression ){
  1092. $arg->throwAwayComments();
  1093. }
  1094. $value = $arg;
  1095. $val = null;
  1096. if( $isCall ){
  1097. // Variable
  1098. if( property_exists($arg,'value') && count($arg->value) == 1 ){
  1099. $val = $arg->value[0];
  1100. }
  1101. } else {
  1102. $val = $arg;
  1103. }
  1104. if( $val instanceof Less_Tree_Variable ){
  1105. if( $this->MatchChar(':') ){
  1106. if( $expressions ){
  1107. if( $isSemiColonSeperated ){
  1108. $this->Error('Cannot mix ; and , as delimiter types');
  1109. }
  1110. $expressionContainsNamed = true;
  1111. }
  1112. // we do not support setting a ruleset as a default variable - it doesn't make sense
  1113. // However if we do want to add it, there is nothing blocking it, just don't error
  1114. // and remove isCall dependency below
  1115. $value = null;
  1116. if( $isCall ){
  1117. $value = $this->parseDetachedRuleset();
  1118. }
  1119. if( !$value ){
  1120. $value = $this->parseExpression();
  1121. }
  1122. if( !$value ){
  1123. if( $isCall ){
  1124. $this->Error('could not understand value for named argument');
  1125. } else {
  1126. $this->restore();
  1127. $returner['args'] = array();
  1128. return $returner;
  1129. }
  1130. }
  1131. $nameLoop = ($name = $val->name);
  1132. }elseif( !$isCall && $this->MatchReg('/\\G\.{3}/') ){
  1133. $returner['variadic'] = true;
  1134. if( $this->MatchChar(";") && !$isSemiColonSeperated ){
  1135. $isSemiColonSeperated = true;
  1136. }
  1137. if( $isSemiColonSeperated ){
  1138. $argsSemiColon[] = array('name'=> $arg->name, 'variadic' => true);
  1139. }else{
  1140. $argsComma[] = array('name'=> $arg->name, 'variadic' => true);
  1141. }
  1142. break;
  1143. }elseif( !$isCall ){
  1144. $name = $nameLoop = $val->name;
  1145. $value = null;
  1146. }
  1147. }
  1148. if( $value ){
  1149. $expressions[] = $value;
  1150. }
  1151. $argsComma[] = array('name'=>$nameLoop, 'value'=>$value );
  1152. if( $this->MatchChar(',') ){
  1153. continue;
  1154. }
  1155. if( $this->MatchChar(';') || $isSemiColonSeperated ){
  1156. if( $expressionContainsNamed ){
  1157. $this->Error('Cannot mix ; and , as delimiter types');
  1158. }
  1159. $isSemiColonSeperated = true;
  1160. if( count($expressions) > 1 ){
  1161. $value = $this->NewObj1('Less_Tree_Value', $expressions);
  1162. }
  1163. $argsSemiColon[] = array('name'=>$name, 'value'=>$value );
  1164. $name = null;
  1165. $expressions = array();
  1166. $expressionContainsNamed = false;
  1167. }
  1168. }
  1169. $this->forget();
  1170. $returner['args'] = ($isSemiColonSeperated ? $argsSemiColon : $argsComma);
  1171. return $returner;
  1172. }
  1173. //
  1174. // A Mixin definition, with a list of parameters
  1175. //
  1176. // .rounded (@radius: 2px, @color) {
  1177. // ...
  1178. // }
  1179. //
  1180. // Until we have a finer grained state-machine, we have to
  1181. // do a look-ahead, to make sure we don't have a mixin call.
  1182. // See the `rule` function for more information.
  1183. //
  1184. // We start by matching `.rounded (`, and then proceed on to
  1185. // the argument list, which has optional default values.
  1186. // We store the parameters in `params`, with a `value` key,
  1187. // if there is a value, such as in the case of `@radius`.
  1188. //
  1189. // Once we've got our params list, and a closing `)`, we parse
  1190. // the `{...}` block.
  1191. //
  1192. private function parseMixinDefinition(){
  1193. $cond = null;
  1194. $char = $this->input[$this->pos];
  1195. if( ($char !== '.' && $char !== '#') || ($char === '{' && $this->PeekReg('/\\G[^{]*\}/')) ){
  1196. return;
  1197. }
  1198. $this->save();
  1199. $match = $this->MatchReg('/\\G([#.](?:[\w-]|\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/');
  1200. if( $match ){
  1201. $name = $match[1];
  1202. $argInfo = $this->parseMixinArgs( false );
  1203. $params = $argInfo['args'];
  1204. $variadic = $argInfo['variadic'];
  1205. // .mixincall("@{a}");
  1206. // looks a bit like a mixin definition..
  1207. // also
  1208. // .mixincall(@a: {rule: set;});
  1209. // so we have to be nice and restore
  1210. if( !$this->MatchChar(')') ){
  1211. $this->furthest = $this->pos;
  1212. $this->restore();
  1213. return;
  1214. }
  1215. $this->parseComments();
  1216. if ($this->MatchReg('/\\Gwhen/')) { // Guard
  1217. $cond = $this->expect('parseConditions', 'Expected conditions');
  1218. }
  1219. $ruleset = $this->parseBlock();
  1220. if( is_array($ruleset) ){
  1221. $this->forget();
  1222. return $this->NewObj5('Less_Tree_Mixin_Definition', array( $name, $params, $ruleset, $cond, $variadic));
  1223. }
  1224. $this->restore();
  1225. }else{
  1226. $this->forget();
  1227. }
  1228. }
  1229. //
  1230. // Entities are the smallest recognized token,
  1231. // and can be found inside a rule's value.
  1232. //
  1233. private function parseEntity(){
  1234. return $this->MatchFuncs( array('parseEntitiesLiteral','parseEntitiesVariable','parseEntitiesUrl','parseEntitiesCall','parseEntitiesKeyword','parseEntitiesJavascript','parseComment') );
  1235. }
  1236. //
  1237. // A Rule terminator. Note that we use `peek()` to check for '}',
  1238. // because the `block` rule will be expecting it, but we still need to make sure
  1239. // it's there, if ';' was ommitted.
  1240. //
  1241. private function parseEnd(){
  1242. return $this->MatchChar(';') || $this->PeekChar('}');
  1243. }
  1244. //
  1245. // IE's alpha function
  1246. //
  1247. // alpha(opacity=88)
  1248. //
  1249. private function parseAlpha(){
  1250. if ( ! $this->MatchReg('/\\G\(opacity=/i')) {
  1251. return;
  1252. }
  1253. $value = $this->MatchReg('/\\G[0-9]+/');
  1254. if( $value ){
  1255. $value = $value[0];
  1256. }else{
  1257. $value = $this->parseEntitiesVariable();
  1258. if( !$value ){
  1259. return;
  1260. }
  1261. }
  1262. $this->expectChar(')');
  1263. return $this->NewObj1('Less_Tree_Alpha',$value);
  1264. }
  1265. //
  1266. // A Selector Element
  1267. //
  1268. // div
  1269. // + h1
  1270. // #socks
  1271. // input[type="text"]
  1272. //
  1273. // Elements are the building blocks for Selectors,
  1274. // they are made out of a `Combinator` (see combinator rule),
  1275. // and an element name, such as a tag a class, or `*`.
  1276. //
  1277. private function parseElement(){
  1278. $c = $this->parseCombinator();
  1279. $index = $this->pos;
  1280. $e = $this->match( array('/\\G(?:\d+\.\d+|\d+)%/', '/\\G(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/',
  1281. '#*', '#&', 'parseAttribute', '/\\G\([^()@]+\)/', '/\\G[\.#](?=@)/', 'parseEntitiesVariableCurly') );
  1282. if( is_null($e) ){
  1283. $this->save();
  1284. if( $this->MatchChar('(') ){
  1285. if( ($v = $this->parseSelector()) && $this->MatchChar(')') ){
  1286. $e = $this->NewObj1('Less_Tree_Paren',$v);
  1287. $this->forget();
  1288. }else{
  1289. $this->restore();
  1290. }
  1291. }else{
  1292. $this->forget();
  1293. }
  1294. }
  1295. if( !is_null($e) ){
  1296. return $this->NewObj4('Less_Tree_Element',array( $c, $e, $index, $this->env->currentFileInfo));
  1297. }
  1298. }
  1299. //
  1300. // Combinators combine elements together, in a Selector.
  1301. //
  1302. // Because our parser isn't white-space sensitive, special care
  1303. // has to be taken, when parsing the descendant combinator, ` `,
  1304. // as it's an empty space. We have to check the previous character
  1305. // in the input, to see if it's a ` ` character.
  1306. //
  1307. private function parseCombinator(){
  1308. if( $this->pos < $this->input_len ){
  1309. $c = $this->input[$this->pos];
  1310. if ($c === '>' || $c === '+' || $c === '~' || $c === '|' || $c === '^' ){
  1311. $this->pos++;
  1312. if( $this->input[$this->pos] === '^' ){
  1313. $c = '^^';
  1314. $this->pos++;
  1315. }
  1316. $this->skipWhitespace(0);
  1317. return $c;
  1318. }
  1319. if( $this->pos > 0 && $this->isWhitespace(-1) ){
  1320. return ' ';
  1321. }
  1322. }
  1323. }
  1324. //
  1325. // A CSS selector (see selector below)
  1326. // with less extensions e.g. the ability to extend and guard
  1327. //
  1328. private function parseLessSelector(){
  1329. return $this->parseSelector(true);
  1330. }
  1331. //
  1332. // A CSS Selector
  1333. //
  1334. // .class > div + h1
  1335. // li a:hover
  1336. //
  1337. // Selectors are made out of one or more Elements, see above.
  1338. //
  1339. private function parseSelector( $isLess = false ){
  1340. $elements = array();
  1341. $extendList = array();
  1342. $condition = null;
  1343. $when = false;
  1344. $extend = false;
  1345. $e = null;
  1346. $c = null;
  1347. $index = $this->pos;
  1348. while( ($isLess && ($extend = $this->parseExtend())) || ($isLess && ($when = $this->MatchReg('/\\Gwhen/') )) || ($e = $this->parseElement()) ){
  1349. if( $when ){
  1350. $condition = $this->expect('parseConditions', 'expected condition');
  1351. }elseif( $condition ){
  1352. //error("CSS guard can only be used at the end of selector");
  1353. }elseif( $extend ){
  1354. $extendList = array_merge($extendList,$extend);
  1355. }else{
  1356. //if( count($extendList) ){
  1357. //error("Extend can only be used at the end of selector");
  1358. //}
  1359. if( $this->pos < $this->input_len ){
  1360. $c = $this->input[ $this->pos ];
  1361. }
  1362. $elements[] = $e;
  1363. $e = null;
  1364. }
  1365. if( $c === '{' || $c === '}' || $c === ';' || $c === ',' || $c === ')') { break; }
  1366. }
  1367. if( $elements ){
  1368. return $this->NewObj5('Less_Tree_Selector',array($elements, $extendList, $condition, $index, $this->env->currentFileInfo));
  1369. }
  1370. if( $extendList ) {
  1371. $this->Error('Extend must be used to extend a selector, it cannot be used on its own');
  1372. }
  1373. }
  1374. private function parseTag(){
  1375. return ( $tag = $this->MatchReg('/\\G[A-Za-z][A-Za-z-]*[0-9]?/') ) ? $tag : $this->MatchChar('*');
  1376. }
  1377. private function parseAttribute(){
  1378. $val = null;
  1379. if( !$this->MatchChar('[') ){
  1380. return;
  1381. }
  1382. $key = $this->parseEntitiesVariableCurly();
  1383. if( !$key ){
  1384. $key = $this->expect('/\\G(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\\\.)+/');
  1385. }
  1386. $op = $this->MatchReg('/\\G[|~*$^]?=/');
  1387. if( $op ){
  1388. $val = $this->match( array('parseEntitiesQuoted','/\\G[0-9]+%/','/\\G[\w-]+/','parseEntitiesVariableCurly') );
  1389. }
  1390. $this->expectChar(']');
  1391. return $this->NewObj3('Less_Tree_Attribute',array( $key, $op[0], $val));
  1392. }
  1393. //
  1394. // The `block` rule is used by `ruleset` and `mixin.definition`.
  1395. // It's a wrapper around the `primary` rule, with added `{}`.
  1396. //
  1397. private function parseBlock(){
  1398. if( $this->MatchChar('{') ){
  1399. $content = $this->parsePrimary();
  1400. if( $this->MatchChar('}') ){
  1401. return $content;
  1402. }
  1403. }
  1404. }
  1405. private function parseBlockRuleset(){
  1406. $block = $this->parseBlock();
  1407. if( $block ){
  1408. $block = $this->NewObj2('Less_Tree_Ruleset',array( null, $block));
  1409. }
  1410. return $block;
  1411. }
  1412. private function parseDetachedRuleset(){
  1413. $blockRuleset = $this->parseBlockRuleset();
  1414. if( $blockRuleset ){
  1415. return $this->NewObj1('Less_Tree_DetachedRuleset',$blockRuleset);
  1416. }
  1417. }
  1418. //
  1419. // div, .class, body > p {...}
  1420. //
  1421. private function parseRuleset(){
  1422. $selectors = array();
  1423. $this->save();
  1424. while( true ){
  1425. $s = $this->parseLessSelector();
  1426. if( !$s ){
  1427. break;
  1428. }
  1429. $selectors[] = $s;
  1430. $this->parseComments();
  1431. if( $s->condition && count($selectors) > 1 ){
  1432. $this->Error('Guards are only currently allowed on a single selector.');
  1433. }
  1434. if( !$this->MatchChar(',') ){
  1435. break;
  1436. }
  1437. if( $s->condition ){
  1438. $this->Error('Guards are only currently allowed on a single selector.');
  1439. }
  1440. $this->parseComments();
  1441. }
  1442. if( $selectors ){
  1443. $rules = $this->parseBlock();
  1444. if( is_array($rules) ){
  1445. $this->forget();
  1446. return $this->NewObj2('Less_Tree_Ruleset',array( $selectors, $rules)); //Less_Environment::$strictImports
  1447. }
  1448. }
  1449. // Backtrack
  1450. $this->furthest = $this->pos;
  1451. $this->restore();
  1452. }
  1453. /**
  1454. * Custom less.php parse function for finding simple name-value css pairs
  1455. * ex: width:100px;
  1456. *
  1457. */
  1458. private function parseNameValue(){
  1459. $index = $this->pos;
  1460. $this->save();
  1461. //$match = $this->MatchReg('/\\G([a-zA-Z\-]+)\s*:\s*((?:\'")?[a-zA-Z0-9\-% \.,!]+?(?:\'")?)\s*([;}])/');
  1462. $match = $this->MatchReg('/\\G([a-zA-Z\-]+)\s*:\s*([\'"]?[#a-zA-Z0-9\-%\.,]+?[\'"]?) *(! *important)?\s*([;}])/');
  1463. if( $match ){
  1464. if( $match[4] == '}' ){
  1465. $this->pos = $index + strlen($match[0])-1;
  1466. }
  1467. if( $match[3] ){
  1468. $match[2] .= ' !important';
  1469. }
  1470. return $this->NewObj4('Less_Tree_NameValue',array( $match[1], $match[2], $index, $this->env->currentFileInfo));
  1471. }
  1472. $this->restore();
  1473. }
  1474. private function parseRule( $tryAnonymous = null ){
  1475. $merge = false;
  1476. $startOfRule = $this->pos;
  1477. $c = $this->input[$this->pos];
  1478. if( $c === '.' || $c === '#' || $c === '&' ){
  1479. return;
  1480. }
  1481. $this->save();
  1482. $name = $this->MatchFuncs( array('parseVariable','parseRuleProperty'));
  1483. if( $name ){
  1484. $isVariable = is_string($name);
  1485. $value = null;
  1486. if( $isVariable ){
  1487. $value = $this->parseDetachedRuleset();
  1488. }
  1489. $important = null;
  1490. if( !$value ){
  1491. // prefer to try to parse first if its a variable or we are compressing
  1492. // but always fallback on the other one
  1493. //if( !$tryAnonymous && is_string($name) && $name[0] === '@' ){
  1494. if( !$tryAnonymous && (Less_Parser::$options['compress'] || $isVariable) ){
  1495. $value = $this->MatchFuncs( array('parseValue','parseAnonymousValue'));
  1496. }else{
  1497. $value = $this->MatchFuncs( array('parseAnonymousValue','parseValue'));
  1498. }
  1499. $important = $this->parseImportant();
  1500. // a name returned by this.ruleProperty() is always an array of the form:
  1501. // [string-1, ..., string-n, ""] or [string-1, ..., string-n, "+"]
  1502. // where each item is a tree.Keyword or tree.Variable
  1503. if( !$isVariable && is_array($name) ){
  1504. $nm = array_pop($name);
  1505. if( $nm->value ){
  1506. $merge = $nm->value;
  1507. }
  1508. }
  1509. }
  1510. if( $value && $this->parseEnd() ){
  1511. $this->forget();
  1512. return $this->NewObj6('Less_Tree_Rule',array( $name, $value, $important, $merge, $startOfRule, $this->env->currentFileInfo));
  1513. }else{
  1514. $this->furthest = $this->pos;
  1515. $this->restore();
  1516. if( $value && !$tryAnonymous ){
  1517. return $this->parseRule(true);
  1518. }
  1519. }
  1520. }else{
  1521. $this->forget();
  1522. }
  1523. }
  1524. function parseAnonymousValue(){
  1525. if( preg_match('/\\G([^@+\/\'"*`(;{}-]*);/',$this->input, $match, 0, $this->pos) ){
  1526. $this->pos += strlen($match[1]);
  1527. return $this->NewObj1('Less_Tree_Anonymous',$match[1]);
  1528. }
  1529. }
  1530. //
  1531. // An @import directive
  1532. //
  1533. // @import "lib";
  1534. //
  1535. // Depending on our environment, importing is done differently:
  1536. // In the browser, it's an XHR request, in Node, it would be a
  1537. // file-system operation. The function used for importing is
  1538. // stored in `import`, which we pass to the Import constructor.
  1539. //
  1540. private function parseImport(){
  1541. $this->save();
  1542. $dir = $this->MatchReg('/\\G@import?\s+/');
  1543. if( $dir ){
  1544. $options = $this->parseImportOptions();
  1545. $path = $this->MatchFuncs( array('parseEntitiesQuoted','parseEntitiesUrl'));
  1546. if( $path ){
  1547. $features = $this->parseMediaFeatures();
  1548. if( $this->MatchChar(';') ){
  1549. if( $features ){
  1550. $features = $this->NewObj1('Less_Tree_Value',$features);
  1551. }
  1552. $this->forget();
  1553. return $this->NewObj5('Less_Tree_Import',array( $path, $features, $options, $this->pos, $this->env->currentFileInfo));
  1554. }
  1555. }
  1556. }
  1557. $this->restore();
  1558. }
  1559. private function parseImportOptions(){
  1560. $options = array();
  1561. // list of options, surrounded by parens
  1562. if( !$this->MatchChar('(') ){
  1563. return $options;
  1564. }
  1565. do{
  1566. $optionName = $this->parseImportOption();
  1567. if( $optionName ){
  1568. $value = true;
  1569. switch( $optionName ){
  1570. case "css":
  1571. $optionName = "less";
  1572. $value = false;
  1573. break;
  1574. case "once":
  1575. $optionName = "multiple";
  1576. $value = false;
  1577. break;
  1578. }
  1579. $options[$optionName] = $value;
  1580. if( !$this->MatchChar(',') ){ break; }
  1581. }
  1582. }while( $optionName );
  1583. $this->expectChar(')');
  1584. return $options;
  1585. }
  1586. private function parseImportOption(){
  1587. $opt = $this->MatchReg('/\\G(less|css|multiple|once|inline|reference|optional)/');
  1588. if( $opt ){
  1589. return $opt[1];
  1590. }
  1591. }
  1592. private function parseMediaFeature() {
  1593. $nodes = array();
  1594. do{
  1595. $e = $this->MatchFuncs(array('parseEntitiesKeyword','parseEntitiesVariable'));
  1596. if( $e ){
  1597. $nodes[] = $e;
  1598. } elseif ($this->MatchChar('(')) {
  1599. $p = $this->parseProperty();
  1600. $e = $this->parseValue();
  1601. if ($this->MatchChar(')')) {
  1602. if ($p && $e) {
  1603. $r = $this->NewObj7('Less_Tree_Rule', array( $p, $e, null, null, $this->pos, $this->env->currentFileInfo, true));
  1604. $nodes[] = $this->NewObj1('Less_Tree_Paren',$r);
  1605. } elseif ($e) {
  1606. $nodes[] = $this->NewObj1('Less_Tree_Paren',$e);
  1607. } else {
  1608. return null;
  1609. }
  1610. } else
  1611. return null;
  1612. }
  1613. } while ($e);
  1614. if ($nodes) {
  1615. return $this->NewObj1('Less_Tree_Expression',$nodes);
  1616. }
  1617. }
  1618. private function parseMediaFeatures() {
  1619. $features = array();
  1620. do{
  1621. $e = $this->parseMediaFeature();
  1622. if( $e ){
  1623. $features[] = $e;
  1624. if (!$this->MatchChar(',')) break;
  1625. }else{
  1626. $e = $this->parseEntitiesVariable();
  1627. if( $e ){
  1628. $features[] = $e;
  1629. if (!$this->MatchChar(',')) break;
  1630. }
  1631. }
  1632. } while ($e);
  1633. return $features ? $features : null;
  1634. }
  1635. private function parseMedia() {
  1636. if( $this->MatchReg('/\\G@media/') ){
  1637. $features = $this->parseMediaFeatures();
  1638. $rules = $this->parseBlock();
  1639. if( is_array($rules) ){
  1640. return $this->NewObj4('Less_Tree_Media',array( $rules, $features, $this->pos, $this->env->currentFileInfo));
  1641. }
  1642. }
  1643. }
  1644. //
  1645. // A CSS Directive
  1646. //
  1647. // @charset "utf-8";
  1648. //
  1649. private function parseDirective(){
  1650. if( !$this->PeekChar('@') ){
  1651. return;
  1652. }
  1653. $rules = null;
  1654. $index = $this->pos;
  1655. $hasBlock = true;
  1656. $hasIdentifier = false;
  1657. $hasExpression = false;
  1658. $hasUnknown = false;
  1659. $value = $this->MatchFuncs(array('parseImport','parseMedia'));
  1660. if( $value ){
  1661. return $value;
  1662. }
  1663. $this->save();
  1664. $name = $this->MatchReg('/\\G@[a-z-]+/');
  1665. if( !$name ) return;
  1666. $name = $name[0];
  1667. $nonVendorSpecificName = $name;
  1668. $pos = strpos($name,'-', 2);
  1669. if( $name[1] == '-' && $pos > 0 ){
  1670. $nonVendorSpecificName = "@" . substr($name, $pos + 1);
  1671. }
  1672. switch( $nonVendorSpecificName ){
  1673. /*
  1674. case "@font-face":
  1675. case "@viewport":
  1676. case "@top-left":
  1677. case "@top-left-corner":
  1678. case "@top-center":
  1679. case "@top-right":
  1680. case "@top-right-corner":
  1681. case "@bottom-left":
  1682. case "@bottom-left-corner":
  1683. case "@bottom-center":
  1684. case "@bottom-right":
  1685. case "@bottom-right-corner":
  1686. case "@left-top":
  1687. case "@left-middle":
  1688. case "@left-bottom":
  1689. case "@right-top":
  1690. case "@right-middle":
  1691. case "@right-bottom":
  1692. hasBlock = true;
  1693. break;
  1694. */
  1695. case "@charset":
  1696. $hasIdentifier = true;
  1697. $hasBlock = false;
  1698. break;
  1699. case "@namespace":
  1700. $hasExpression = true;
  1701. $hasBlock = fal

Large files files are truncated, but you can click here to view the full file