Parser.php 64 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816
  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. //set new options
  87. if( is_array($options) ){
  88. $this->SetOptions(Less_Parser::$default_options);
  89. $this->SetOptions($options);
  90. }
  91. $this->env->Init();
  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. public function findValueOf($varName)
  187. {
  188. foreach($this->rules as $rule){
  189. if(isset($rule->variable) && ($rule->variable == true) && (str_replace("@","",$rule->name) == $varName)){
  190. return $this->getVariableValue($rule);
  191. }
  192. }
  193. return null;
  194. }
  195. /**
  196. *
  197. * this function gets the private rules variable and returns an array of the found variables
  198. * it uses a helper method getVariableValue() that contains the logic ot fetch the value from the rule object
  199. *
  200. * @return array
  201. */
  202. public function getVariables()
  203. {
  204. $variables = array();
  205. $not_variable_type = array(
  206. 'Comment', // this include less comments ( // ) and css comments (/* */)
  207. 'Import', // do not search variables in included files @import
  208. 'Ruleset', // selectors (.someclass, #someid, …)
  209. 'Operation', //
  210. );
  211. // @TODO run compilation if not runned yet
  212. foreach ($this->rules as $key => $rule) {
  213. if (in_array($rule->type, $not_variable_type)) {
  214. continue;
  215. }
  216. // Note: it seems rule->type is always Rule when variable = true
  217. if ($rule->type == 'Rule' && $rule->variable) {
  218. $variables[$rule->name] = $this->getVariableValue($rule);
  219. } else {
  220. if ($rule->type == 'Comment') {
  221. $variables[] = $this->getVariableValue($rule);
  222. }
  223. }
  224. }
  225. return $variables;
  226. }
  227. public function findVarByName($var_name)
  228. {
  229. foreach($this->rules as $rule){
  230. if(isset($rule->variable) && ($rule->variable == true)){
  231. if($rule->name == $var_name){
  232. return $this->getVariableValue($rule);
  233. }
  234. }
  235. }
  236. return null;
  237. }
  238. /**
  239. *
  240. * This method gets the value of the less variable from the rules object.
  241. * Since the objects vary here we add the logic for extracting the css/less value.
  242. *
  243. * @param $var
  244. *
  245. * @return bool|string
  246. */
  247. private function getVariableValue($var)
  248. {
  249. if (!is_a($var, 'Less_Tree')) {
  250. throw new Exception('var is not a Less_Tree object');
  251. }
  252. switch ($var->type) {
  253. case 'Color':
  254. return $this->rgb2html($var->rgb);
  255. case 'Unit':
  256. return $var->value. $var->unit->numerator[0];
  257. case 'Variable':
  258. return $this->findVarByName($var->name);
  259. case 'Keyword':
  260. return $var->value;
  261. case 'Rule':
  262. return $this->getVariableValue($var->value);
  263. case 'Value':
  264. $value = '';
  265. foreach ($var->value as $sub_value) {
  266. $value .= $this->getVariableValue($sub_value).' ';
  267. }
  268. return $value;
  269. case 'Quoted':
  270. return $var->quote.$var->value.$var->quote;
  271. case 'Dimension':
  272. $value = $var->value;
  273. if ($var->unit && $var->unit->numerator) {
  274. $value .= $var->unit->numerator[0];
  275. }
  276. return $value;
  277. case 'Expression':
  278. $value = "";
  279. foreach($var->value as $item) {
  280. $value .= $this->getVariableValue($item)." ";
  281. }
  282. return $value;
  283. case 'Operation':
  284. throw new Exception('getVariables() require Less to be compiled. please use $parser->getCss() before calling getVariables()');
  285. case 'Comment':
  286. case 'Import':
  287. case 'Ruleset':
  288. default:
  289. throw new Exception("type missing in switch/case getVariableValue for ".$var->type);
  290. }
  291. return false;
  292. }
  293. private function rgb2html($r, $g=-1, $b=-1)
  294. {
  295. if (is_array($r) && sizeof($r) == 3)
  296. list($r, $g, $b) = $r;
  297. $r = intval($r); $g = intval($g);
  298. $b = intval($b);
  299. $r = dechex($r<0?0:($r>255?255:$r));
  300. $g = dechex($g<0?0:($g>255?255:$g));
  301. $b = dechex($b<0?0:($b>255?255:$b));
  302. $color = (strlen($r) < 2?'0':'').$r;
  303. $color .= (strlen($g) < 2?'0':'').$g;
  304. $color .= (strlen($b) < 2?'0':'').$b;
  305. return '#'.$color;
  306. }
  307. /**
  308. * Run pre-compile visitors
  309. *
  310. */
  311. private function PreVisitors($root){
  312. if( Less_Parser::$options['plugins'] ){
  313. foreach(Less_Parser::$options['plugins'] as $plugin){
  314. if( !empty($plugin->isPreEvalVisitor) ){
  315. $plugin->run($root);
  316. }
  317. }
  318. }
  319. }
  320. /**
  321. * Run post-compile visitors
  322. *
  323. */
  324. private function PostVisitors($evaldRoot){
  325. $visitors = array();
  326. $visitors[] = new Less_Visitor_joinSelector();
  327. if( self::$has_extends ){
  328. $visitors[] = new Less_Visitor_processExtends();
  329. }
  330. $visitors[] = new Less_Visitor_toCSS();
  331. if( Less_Parser::$options['plugins'] ){
  332. foreach(Less_Parser::$options['plugins'] as $plugin){
  333. if( property_exists($plugin,'isPreEvalVisitor') && $plugin->isPreEvalVisitor ){
  334. continue;
  335. }
  336. if( property_exists($plugin,'isPreVisitor') && $plugin->isPreVisitor ){
  337. array_unshift( $visitors, $plugin);
  338. }else{
  339. $visitors[] = $plugin;
  340. }
  341. }
  342. }
  343. for($i = 0; $i < count($visitors); $i++ ){
  344. $visitors[$i]->run($evaldRoot);
  345. }
  346. }
  347. /**
  348. * Parse a Less string into css
  349. *
  350. * @param string $str The string to convert
  351. * @param string $uri_root The url of the file
  352. * @return Less_Tree_Ruleset|Less_Parser
  353. */
  354. public function parse( $str, $file_uri = null ){
  355. if( !$file_uri ){
  356. $uri_root = '';
  357. $filename = 'anonymous-file-'.Less_Parser::$next_id++.'.less';
  358. }else{
  359. $file_uri = self::WinPath($file_uri);
  360. $filename = $file_uri;
  361. $uri_root = dirname($file_uri);
  362. }
  363. $previousFileInfo = $this->env->currentFileInfo;
  364. $uri_root = self::WinPath($uri_root);
  365. $this->SetFileInfo($filename, $uri_root);
  366. $this->input = $str;
  367. $this->_parse();
  368. if( $previousFileInfo ){
  369. $this->env->currentFileInfo = $previousFileInfo;
  370. }
  371. return $this;
  372. }
  373. /**
  374. * Parse a Less string from a given file
  375. *
  376. * @throws Less_Exception_Parser
  377. * @param string $filename The file to parse
  378. * @param string $uri_root The url of the file
  379. * @param bool $returnRoot Indicates whether the return value should be a css string a root node
  380. * @return Less_Tree_Ruleset|Less_Parser
  381. */
  382. public function parseFile( $filename, $uri_root = '', $returnRoot = false){
  383. if( !file_exists($filename) ){
  384. $this->Error(sprintf('File `%s` not found.', $filename));
  385. }
  386. // fix uri_root?
  387. // Instead of The mixture of file path for the first argument and directory path for the second argument has bee
  388. if( !$returnRoot && !empty($uri_root) && basename($uri_root) == basename($filename) ){
  389. $uri_root = dirname($uri_root);
  390. }
  391. $previousFileInfo = $this->env->currentFileInfo;
  392. if( $filename ){
  393. $filename = self::AbsPath($filename, true);
  394. }
  395. $uri_root = self::WinPath($uri_root);
  396. $this->SetFileInfo($filename, $uri_root);
  397. self::AddParsedFile($filename);
  398. if( $returnRoot ){
  399. $rules = $this->GetRules( $filename );
  400. $return = new Less_Tree_Ruleset(array(), $rules );
  401. }else{
  402. $this->_parse( $filename );
  403. $return = $this;
  404. }
  405. if( $previousFileInfo ){
  406. $this->env->currentFileInfo = $previousFileInfo;
  407. }
  408. return $return;
  409. }
  410. /**
  411. * Allows a user to set variables values
  412. * @param array $vars
  413. * @return Less_Parser
  414. */
  415. public function ModifyVars( $vars ){
  416. $this->input = Less_Parser::serializeVars( $vars );
  417. $this->_parse();
  418. return $this;
  419. }
  420. /**
  421. * @param string $filename
  422. */
  423. public function SetFileInfo( $filename, $uri_root = ''){
  424. $filename = Less_Environment::normalizePath($filename);
  425. $dirname = preg_replace('/[^\/\\\\]*$/','',$filename);
  426. if( !empty($uri_root) ){
  427. $uri_root = rtrim($uri_root,'/').'/';
  428. }
  429. $currentFileInfo = array();
  430. //entry info
  431. if( isset($this->env->currentFileInfo) ){
  432. $currentFileInfo['entryPath'] = $this->env->currentFileInfo['entryPath'];
  433. $currentFileInfo['entryUri'] = $this->env->currentFileInfo['entryUri'];
  434. $currentFileInfo['rootpath'] = $this->env->currentFileInfo['rootpath'];
  435. }else{
  436. $currentFileInfo['entryPath'] = $dirname;
  437. $currentFileInfo['entryUri'] = $uri_root;
  438. $currentFileInfo['rootpath'] = $dirname;
  439. }
  440. $currentFileInfo['currentDirectory'] = $dirname;
  441. $currentFileInfo['currentUri'] = $uri_root.basename($filename);
  442. $currentFileInfo['filename'] = $filename;
  443. $currentFileInfo['uri_root'] = $uri_root;
  444. //inherit reference
  445. if( isset($this->env->currentFileInfo['reference']) && $this->env->currentFileInfo['reference'] ){
  446. $currentFileInfo['reference'] = true;
  447. }
  448. $this->env->currentFileInfo = $currentFileInfo;
  449. }
  450. /**
  451. * @deprecated 1.5.1.2
  452. *
  453. */
  454. public function SetCacheDir( $dir ){
  455. if( !file_exists($dir) ){
  456. if( mkdir($dir) ){
  457. return true;
  458. }
  459. throw new Less_Exception_Parser('Less.php cache directory couldn\'t be created: '.$dir);
  460. }elseif( !is_dir($dir) ){
  461. throw new Less_Exception_Parser('Less.php cache directory doesn\'t exist: '.$dir);
  462. }elseif( !is_writable($dir) ){
  463. throw new Less_Exception_Parser('Less.php cache directory isn\'t writable: '.$dir);
  464. }else{
  465. $dir = self::WinPath($dir);
  466. Less_Cache::$cache_dir = rtrim($dir,'/').'/';
  467. return true;
  468. }
  469. }
  470. /**
  471. * Set a list of directories or callbacks the parser should use for determining import paths
  472. *
  473. * @param array $dirs
  474. */
  475. public function SetImportDirs( $dirs ){
  476. Less_Parser::$options['import_dirs'] = array();
  477. foreach($dirs as $path => $uri_root){
  478. $path = self::WinPath($path);
  479. if( !empty($path) ){
  480. $path = rtrim($path,'/').'/';
  481. }
  482. if ( !is_callable($uri_root) ){
  483. $uri_root = self::WinPath($uri_root);
  484. if( !empty($uri_root) ){
  485. $uri_root = rtrim($uri_root,'/').'/';
  486. }
  487. }
  488. Less_Parser::$options['import_dirs'][$path] = $uri_root;
  489. }
  490. }
  491. /**
  492. * @param string $file_path
  493. */
  494. private function _parse( $file_path = null ){
  495. $this->rules = array_merge($this->rules, $this->GetRules( $file_path ));
  496. }
  497. /**
  498. * Return the results of parsePrimary for $file_path
  499. * Use cache and save cached results if possible
  500. *
  501. * @param string|null $file_path
  502. */
  503. private function GetRules( $file_path ){
  504. $this->SetInput($file_path);
  505. $cache_file = $this->CacheFile( $file_path );
  506. if( $cache_file ){
  507. if( Less_Parser::$options['cache_method'] == 'callback' ){
  508. if( is_callable(Less_Parser::$options['cache_callback_get']) ){
  509. $cache = call_user_func_array(
  510. Less_Parser::$options['cache_callback_get'],
  511. array($this, $file_path, $cache_file)
  512. );
  513. if( $cache ){
  514. $this->UnsetInput();
  515. return $cache;
  516. }
  517. }
  518. }elseif( file_exists($cache_file) ){
  519. switch(Less_Parser::$options['cache_method']){
  520. // Using serialize
  521. // Faster but uses more memory
  522. case 'serialize':
  523. $cache = unserialize(file_get_contents($cache_file));
  524. if( $cache ){
  525. touch($cache_file);
  526. $this->UnsetInput();
  527. return $cache;
  528. }
  529. break;
  530. // Using generated php code
  531. case 'var_export':
  532. case 'php':
  533. $this->UnsetInput();
  534. return include($cache_file);
  535. }
  536. }
  537. }
  538. $rules = $this->parsePrimary();
  539. if( $this->pos < $this->input_len ){
  540. throw new Less_Exception_Chunk($this->input, null, $this->furthest, $this->env->currentFileInfo);
  541. }
  542. $this->UnsetInput();
  543. //save the cache
  544. if( $cache_file ){
  545. if( Less_Parser::$options['cache_method'] == 'callback' ){
  546. if( is_callable(Less_Parser::$options['cache_callback_set']) ){
  547. call_user_func_array(
  548. Less_Parser::$options['cache_callback_set'],
  549. array($this, $file_path, $cache_file, $rules)
  550. );
  551. }
  552. }else{
  553. //msg('write cache file');
  554. switch(Less_Parser::$options['cache_method']){
  555. case 'serialize':
  556. file_put_contents( $cache_file, serialize($rules) );
  557. break;
  558. case 'php':
  559. file_put_contents( $cache_file, '<?php return '.self::ArgString($rules).'; ?>' );
  560. break;
  561. case 'var_export':
  562. //Requires __set_state()
  563. file_put_contents( $cache_file, '<?php return '.var_export($rules,true).'; ?>' );
  564. break;
  565. }
  566. Less_Cache::CleanCache();
  567. }
  568. }
  569. return $rules;
  570. }
  571. /**
  572. * Set up the input buffer
  573. *
  574. */
  575. public function SetInput( $file_path ){
  576. if( $file_path ){
  577. $this->input = file_get_contents( $file_path );
  578. }
  579. $this->pos = $this->furthest = 0;
  580. // Remove potential UTF Byte Order Mark
  581. $this->input = preg_replace('/\\G\xEF\xBB\xBF/', '', $this->input);
  582. $this->input_len = strlen($this->input);
  583. if( Less_Parser::$options['sourceMap'] && $this->env->currentFileInfo ){
  584. $uri = $this->env->currentFileInfo['currentUri'];
  585. Less_Parser::$contentsMap[$uri] = $this->input;
  586. }
  587. }
  588. /**
  589. * Free up some memory
  590. *
  591. */
  592. public function UnsetInput(){
  593. unset($this->input, $this->pos, $this->input_len, $this->furthest);
  594. $this->saveStack = array();
  595. }
  596. public function CacheFile( $file_path ){
  597. if( $file_path && $this->CacheEnabled() ){
  598. $env = get_object_vars($this->env);
  599. unset($env['frames']);
  600. $parts = array();
  601. $parts[] = $file_path;
  602. $parts[] = filesize( $file_path );
  603. $parts[] = filemtime( $file_path );
  604. $parts[] = $env;
  605. $parts[] = Less_Version::cache_version;
  606. $parts[] = Less_Parser::$options['cache_method'];
  607. return Less_Cache::$cache_dir . Less_Cache::$prefix . base_convert( sha1(json_encode($parts) ), 16, 36) . '.lesscache';
  608. }
  609. }
  610. static function AddParsedFile($file){
  611. self::$imports[] = $file;
  612. }
  613. static function AllParsedFiles(){
  614. return self::$imports;
  615. }
  616. /**
  617. * @param string $file
  618. */
  619. static function FileParsed($file){
  620. return in_array($file,self::$imports);
  621. }
  622. function save() {
  623. $this->saveStack[] = $this->pos;
  624. }
  625. private function restore() {
  626. $this->pos = array_pop($this->saveStack);
  627. }
  628. private function forget(){
  629. array_pop($this->saveStack);
  630. }
  631. /**
  632. * Determine if the character at the specified offset from the current position is a white space.
  633. *
  634. * @param int $offset
  635. *
  636. * @return bool
  637. */
  638. private function isWhitespace($offset = 0) {
  639. return strpos(" \t\n\r\v\f", $this->input[$this->pos + $offset]) !== false;
  640. }
  641. /**
  642. * Parse from a token, regexp or string, and move forward if match
  643. *
  644. * @param array $toks
  645. * @return array
  646. */
  647. private function match($toks){
  648. // The match is confirmed, add the match length to `this::pos`,
  649. // and consume any extra white-space characters (' ' || '\n')
  650. // which come after that. The reason for this is that LeSS's
  651. // grammar is mostly white-space insensitive.
  652. //
  653. foreach($toks as $tok){
  654. $char = $tok[0];
  655. if( $char === '/' ){
  656. $match = $this->MatchReg($tok);
  657. if( $match ){
  658. return count($match) === 1 ? $match[0] : $match;
  659. }
  660. }elseif( $char === '#' ){
  661. $match = $this->MatchChar($tok[1]);
  662. }else{
  663. // Non-terminal, match using a function call
  664. $match = $this->$tok();
  665. }
  666. if( $match ){
  667. return $match;
  668. }
  669. }
  670. }
  671. /**
  672. * @param string[] $toks
  673. *
  674. * @return string
  675. */
  676. private function MatchFuncs($toks){
  677. if( $this->pos < $this->input_len ){
  678. foreach($toks as $tok){
  679. $match = $this->$tok();
  680. if( $match ){
  681. return $match;
  682. }
  683. }
  684. }
  685. }
  686. // Match a single character in the input,
  687. private function MatchChar($tok){
  688. if( ($this->pos < $this->input_len) && ($this->input[$this->pos] === $tok) ){
  689. $this->skipWhitespace(1);
  690. return $tok;
  691. }
  692. }
  693. // Match a regexp from the current start point
  694. private function MatchReg($tok){
  695. if( preg_match($tok, $this->input, $match, 0, $this->pos) ){
  696. $this->skipWhitespace(strlen($match[0]));
  697. return $match;
  698. }
  699. }
  700. /**
  701. * Same as match(), but don't change the state of the parser,
  702. * just return the match.
  703. *
  704. * @param string $tok
  705. * @return integer
  706. */
  707. public function PeekReg($tok){
  708. return preg_match($tok, $this->input, $match, 0, $this->pos);
  709. }
  710. /**
  711. * @param string $tok
  712. */
  713. public function PeekChar($tok){
  714. //return ($this->input[$this->pos] === $tok );
  715. return ($this->pos < $this->input_len) && ($this->input[$this->pos] === $tok );
  716. }
  717. /**
  718. * @param integer $length
  719. */
  720. public function skipWhitespace($length){
  721. $this->pos += $length;
  722. for(; $this->pos < $this->input_len; $this->pos++ ){
  723. $c = $this->input[$this->pos];
  724. if( ($c !== "\n") && ($c !== "\r") && ($c !== "\t") && ($c !== ' ') ){
  725. break;
  726. }
  727. }
  728. }
  729. /**
  730. * @param string $tok
  731. * @param string|null $msg
  732. */
  733. public function expect($tok, $msg = NULL) {
  734. $result = $this->match( array($tok) );
  735. if (!$result) {
  736. $this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg );
  737. } else {
  738. return $result;
  739. }
  740. }
  741. /**
  742. * @param string $tok
  743. */
  744. public function expectChar($tok, $msg = null ){
  745. $result = $this->MatchChar($tok);
  746. if( !$result ){
  747. $msg = $msg ? $msg : "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'";
  748. $this->Error( $msg );
  749. }else{
  750. return $result;
  751. }
  752. }
  753. //
  754. // Here in, the parsing rules/functions
  755. //
  756. // The basic structure of the syntax tree generated is as follows:
  757. //
  758. // Ruleset -> Rule -> Value -> Expression -> Entity
  759. //
  760. // Here's some LESS code:
  761. //
  762. // .class {
  763. // color: #fff;
  764. // border: 1px solid #000;
  765. // width: @w + 4px;
  766. // > .child {...}
  767. // }
  768. //
  769. // And here's what the parse tree might look like:
  770. //
  771. // Ruleset (Selector '.class', [
  772. // Rule ("color", Value ([Expression [Color #fff]]))
  773. // Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]]))
  774. // Rule ("width", Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]]))
  775. // Ruleset (Selector [Element '>', '.child'], [...])
  776. // ])
  777. //
  778. // In general, most rules will try to parse a token with the `$()` function, and if the return
  779. // value is truly, will return a new node, of the relevant type. Sometimes, we need to check
  780. // first, before parsing, that's when we use `peek()`.
  781. //
  782. //
  783. // The `primary` rule is the *entry* and *exit* point of the parser.
  784. // The rules here can appear at any level of the parse tree.
  785. //
  786. // The recursive nature of the grammar is an interplay between the `block`
  787. // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule,
  788. // as represented by this simplified grammar:
  789. //
  790. // primary → (ruleset | rule)+
  791. // ruleset → selector+ block
  792. // block → '{' primary '}'
  793. //
  794. // Only at one point is the primary rule not called from the
  795. // block rule: at the root level.
  796. //
  797. private function parsePrimary(){
  798. $root = array();
  799. while( true ){
  800. if( $this->pos >= $this->input_len ){
  801. break;
  802. }
  803. $node = $this->parseExtend(true);
  804. if( $node ){
  805. $root = array_merge($root,$node);
  806. continue;
  807. }
  808. //$node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseDirective'));
  809. $node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseNameValue', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseRulesetCall', 'parseDirective'));
  810. if( $node ){
  811. $root[] = $node;
  812. }elseif( !$this->MatchReg('/\\G[\s\n;]+/') ){
  813. break;
  814. }
  815. if( $this->PeekChar('}') ){
  816. break;
  817. }
  818. }
  819. return $root;
  820. }
  821. // We create a Comment node for CSS comments `/* */`,
  822. // but keep the LeSS comments `//` silent, by just skipping
  823. // over them.
  824. private function parseComment(){
  825. if( $this->input[$this->pos] !== '/' ){
  826. return;
  827. }
  828. if( $this->input[$this->pos+1] === '/' ){
  829. $match = $this->MatchReg('/\\G\/\/.*/');
  830. return $this->NewObj4('Less_Tree_Comment',array($match[0], true, $this->pos, $this->env->currentFileInfo));
  831. }
  832. //$comment = $this->MatchReg('/\\G\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/');
  833. $comment = $this->MatchReg('/\\G\/\*(?s).*?\*+\/\n?/');//not the same as less.js to prevent fatal errors
  834. if( $comment ){
  835. return $this->NewObj4('Less_Tree_Comment',array($comment[0], false, $this->pos, $this->env->currentFileInfo));
  836. }
  837. }
  838. private function parseComments(){
  839. $comments = array();
  840. while( $this->pos < $this->input_len ){
  841. $comment = $this->parseComment();
  842. if( !$comment ){
  843. break;
  844. }
  845. $comments[] = $comment;
  846. }
  847. return $comments;
  848. }
  849. //
  850. // A string, which supports escaping " and '
  851. //
  852. // "milky way" 'he\'s the one!'
  853. //
  854. private function parseEntitiesQuoted() {
  855. $j = $this->pos;
  856. $e = false;
  857. $index = $this->pos;
  858. if( $this->input[$this->pos] === '~' ){
  859. $j++;
  860. $e = true; // Escaped strings
  861. }
  862. $char = $this->input[$j];
  863. if( $char !== '"' && $char !== "'" ){
  864. return;
  865. }
  866. if ($e) {
  867. $this->MatchChar('~');
  868. }
  869. $matched = $this->MatchQuoted($char, $j+1);
  870. if( $matched === false ){
  871. return;
  872. }
  873. $quoted = $char.$matched.$char;
  874. return $this->NewObj5('Less_Tree_Quoted',array($quoted, $matched, $e, $index, $this->env->currentFileInfo) );
  875. }
  876. /**
  877. * When PCRE JIT is enabled in php, regular expressions don't work for matching quoted strings
  878. *
  879. * $regex = '/\\G\'((?:[^\'\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)\'/';
  880. * $regex = '/\\G"((?:[^"\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)"/';
  881. *
  882. */
  883. private function MatchQuoted($quote_char, $i){
  884. $matched = '';
  885. while( $i < $this->input_len ){
  886. $c = $this->input[$i];
  887. //escaped character
  888. if( $c === '\\' ){
  889. $matched .= $c . $this->input[$i+1];
  890. $i += 2;
  891. continue;
  892. }
  893. if( $c === $quote_char ){
  894. $this->pos = $i+1;
  895. $this->skipWhitespace(0);
  896. return $matched;
  897. }
  898. if( $c === "\r" || $c === "\n" ){
  899. return false;
  900. }
  901. $i++;
  902. $matched .= $c;
  903. }
  904. return false;
  905. }
  906. //
  907. // A catch-all word, such as:
  908. //
  909. // black border-collapse
  910. //
  911. private function parseEntitiesKeyword(){
  912. //$k = $this->MatchReg('/\\G[_A-Za-z-][_A-Za-z0-9-]*/');
  913. $k = $this->MatchReg('/\\G%|\\G[_A-Za-z-][_A-Za-z0-9-]*/');
  914. if( $k ){
  915. $k = $k[0];
  916. $color = $this->fromKeyword($k);
  917. if( $color ){
  918. return $color;
  919. }
  920. return $this->NewObj1('Less_Tree_Keyword',$k);
  921. }
  922. }
  923. // duplicate of Less_Tree_Color::FromKeyword
  924. private function FromKeyword( $keyword ){
  925. $keyword = strtolower($keyword);
  926. if( Less_Colors::hasOwnProperty($keyword) ){
  927. // detect named color
  928. return $this->NewObj1('Less_Tree_Color',substr(Less_Colors::color($keyword), 1));
  929. }
  930. if( $keyword === 'transparent' ){
  931. return $this->NewObj3('Less_Tree_Color', array( array(0, 0, 0), 0, true));
  932. }
  933. }
  934. //
  935. // A function call
  936. //
  937. // rgb(255, 0, 255)
  938. //
  939. // We also try to catch IE's `alpha()`, but let the `alpha` parser
  940. // deal with the details.
  941. //
  942. // The arguments are parsed with the `entities.arguments` parser.
  943. //
  944. private function parseEntitiesCall(){
  945. $index = $this->pos;
  946. if( !preg_match('/\\G([\w-]+|%|progid:[\w\.]+)\(/', $this->input, $name,0,$this->pos) ){
  947. return;
  948. }
  949. $name = $name[1];
  950. $nameLC = strtolower($name);
  951. if ($nameLC === 'url') {
  952. return null;
  953. }
  954. $this->pos += strlen($name);
  955. if( $nameLC === 'alpha' ){
  956. $alpha_ret = $this->parseAlpha();
  957. if( $alpha_ret ){
  958. return $alpha_ret;
  959. }
  960. }
  961. $this->MatchChar('('); // Parse the '(' and consume whitespace.
  962. $args = $this->parseEntitiesArguments();
  963. if( !$this->MatchChar(')') ){
  964. return;
  965. }
  966. if ($name) {
  967. return $this->NewObj4('Less_Tree_Call',array($name, $args, $index, $this->env->currentFileInfo) );
  968. }
  969. }
  970. /**
  971. * Parse a list of arguments
  972. *
  973. * @return array
  974. */
  975. private function parseEntitiesArguments(){
  976. $args = array();
  977. while( true ){
  978. $arg = $this->MatchFuncs( array('parseEntitiesAssignment','parseExpression') );
  979. if( !$arg ){
  980. break;
  981. }
  982. $args[] = $arg;
  983. if( !$this->MatchChar(',') ){
  984. break;
  985. }
  986. }
  987. return $args;
  988. }
  989. private function parseEntitiesLiteral(){
  990. return $this->MatchFuncs( array('parseEntitiesDimension','parseEntitiesColor','parseEntitiesQuoted','parseUnicodeDescriptor') );
  991. }
  992. // Assignments are argument entities for calls.
  993. // They are present in ie filter properties as shown below.
  994. //
  995. // filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* )
  996. //
  997. private function parseEntitiesAssignment() {
  998. $key = $this->MatchReg('/\\G\w+(?=\s?=)/');
  999. if( !$key ){
  1000. return;
  1001. }
  1002. if( !$this->MatchChar('=') ){
  1003. return;
  1004. }
  1005. $value = $this->parseEntity();
  1006. if( $value ){
  1007. return $this->NewObj2('Less_Tree_Assignment',array($key[0], $value));
  1008. }
  1009. }
  1010. //
  1011. // Parse url() tokens
  1012. //
  1013. // We use a specific rule for urls, because they don't really behave like
  1014. // standard function calls. The difference is that the argument doesn't have
  1015. // to be enclosed within a string, so it can't be parsed as an Expression.
  1016. //
  1017. private function parseEntitiesUrl(){
  1018. if( $this->input[$this->pos] !== 'u' || !$this->matchReg('/\\Gurl\(/') ){
  1019. return;
  1020. }
  1021. $value = $this->match( array('parseEntitiesQuoted','parseEntitiesVariable','/\\Gdata\:.*?[^\)]+/','/\\G(?:(?:\\\\[\(\)\'"])|[^\(\)\'"])+/') );
  1022. if( !$value ){
  1023. $value = '';
  1024. }
  1025. $this->expectChar(')');
  1026. if( isset($value->value) || $value instanceof Less_Tree_Variable ){
  1027. return $this->NewObj2('Less_Tree_Url',array($value, $this->env->currentFileInfo));
  1028. }
  1029. return $this->NewObj2('Less_Tree_Url', array( $this->NewObj1('Less_Tree_Anonymous',$value), $this->env->currentFileInfo) );
  1030. }
  1031. //
  1032. // A Variable entity, such as `@fink`, in
  1033. //
  1034. // width: @fink + 2px
  1035. //
  1036. // We use a different parser for variable definitions,
  1037. // see `parsers.variable`.
  1038. //
  1039. private function parseEntitiesVariable(){
  1040. $index = $this->pos;
  1041. if ($this->PeekChar('@') && ($name = $this->MatchReg('/\\G@@?[\w-]+/'))) {
  1042. return $this->NewObj3('Less_Tree_Variable', array( $name[0], $index, $this->env->currentFileInfo));
  1043. }
  1044. }
  1045. // A variable entity using the protective {} e.g. @{var}
  1046. private function parseEntitiesVariableCurly() {
  1047. $index = $this->pos;
  1048. if( $this->input_len > ($this->pos+1) && $this->input[$this->pos] === '@' && ($curly = $this->MatchReg('/\\G@\{([\w-]+)\}/')) ){
  1049. return $this->NewObj3('Less_Tree_Variable',array('@'.$curly[1], $index, $this->env->currentFileInfo));
  1050. }
  1051. }
  1052. //
  1053. // A Hexadecimal color
  1054. //
  1055. // #4F3C2F
  1056. //
  1057. // `rgb` and `hsl` colors are parsed through the `entities.call` parser.
  1058. //
  1059. private function parseEntitiesColor(){
  1060. if ($this->PeekChar('#') && ($rgb = $this->MatchReg('/\\G#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/'))) {
  1061. return $this->NewObj1('Less_Tree_Color',$rgb[1]);
  1062. }
  1063. }
  1064. //
  1065. // A Dimension, that is, a number and a unit
  1066. //
  1067. // 0.5em 95%
  1068. //
  1069. private function parseEntitiesDimension(){
  1070. $c = @ord($this->input[$this->pos]);
  1071. //Is the first char of the dimension 0-9, '.', '+' or '-'
  1072. if (($c > 57 || $c < 43) || $c === 47 || $c == 44){
  1073. return;
  1074. }
  1075. $value = $this->MatchReg('/\\G([+-]?\d*\.?\d+)(%|[a-z]+)?/');
  1076. if( $value ){
  1077. if( isset($value[2]) ){
  1078. return $this->NewObj2('Less_Tree_Dimension', array($value[1],$value[2]));
  1079. }
  1080. return $this->NewObj1('Less_Tree_Dimension',$value[1]);
  1081. }
  1082. }
  1083. //
  1084. // A unicode descriptor, as is used in unicode-range
  1085. //
  1086. // U+0?? or U+00A1-00A9
  1087. //
  1088. function parseUnicodeDescriptor() {
  1089. $ud = $this->MatchReg('/\\G(U\+[0-9a-fA-F?]+)(\-[0-9a-fA-F?]+)?/');
  1090. if( $ud ){
  1091. return $this->NewObj1('Less_Tree_UnicodeDescriptor', $ud[0]);
  1092. }
  1093. }
  1094. //
  1095. // JavaScript code to be evaluated
  1096. //
  1097. // `window.location.href`
  1098. //
  1099. private function parseEntitiesJavascript(){
  1100. $e = false;
  1101. $j = $this->pos;
  1102. if( $this->input[$j] === '~' ){
  1103. $j++;
  1104. $e = true;
  1105. }
  1106. if( $this->input[$j] !== '`' ){
  1107. return;
  1108. }
  1109. if( $e ){
  1110. $this->MatchChar('~');
  1111. }
  1112. $str = $this->MatchReg('/\\G`([^`]*)`/');
  1113. if( $str ){
  1114. return $this->NewObj3('Less_Tree_Javascript', array($str[1], $this->pos, $e));
  1115. }
  1116. }
  1117. //
  1118. // The variable part of a variable definition. Used in the `rule` parser
  1119. //
  1120. // @fink:
  1121. //
  1122. private function parseVariable(){
  1123. if ($this->PeekChar('@') && ($name = $this->MatchReg('/\\G(@[\w-]+)\s*:/'))) {
  1124. return $name[1];
  1125. }
  1126. }
  1127. //
  1128. // The variable part of a variable definition. Used in the `rule` parser
  1129. //
  1130. // @fink();
  1131. //
  1132. private function parseRulesetCall(){
  1133. if( $this->input[$this->pos] === '@' && ($name = $this->MatchReg('/\\G(@[\w-]+)\s*\(\s*\)\s*;/')) ){
  1134. return $this->NewObj1('Less_Tree_RulesetCall', $name[1] );
  1135. }
  1136. }
  1137. //
  1138. // extend syntax - used to extend selectors
  1139. //
  1140. function parseExtend($isRule = false){
  1141. $index = $this->pos;
  1142. $extendList = array();
  1143. if( !$this->MatchReg( $isRule ? '/\\G&:extend\(/' : '/\\G:extend\(/' ) ){ return; }
  1144. do{
  1145. $option = null;
  1146. $elements = array();
  1147. while( true ){
  1148. $option = $this->MatchReg('/\\G(all)(?=\s*(\)|,))/');
  1149. if( $option ){ break; }
  1150. $e = $this->parseElement();
  1151. if( !$e ){ break; }
  1152. $elements[] = $e;
  1153. }
  1154. if( $option ){
  1155. $option = $option[1];
  1156. }
  1157. $extendList[] = $this->NewObj3('Less_Tree_Extend', array( $this->NewObj1('Less_Tree_Selector',$elements), $option, $index ));
  1158. }while( $this->MatchChar(",") );
  1159. $this->expect('/\\G\)/');
  1160. if( $isRule ){
  1161. $this->expect('/\\G;/');
  1162. }
  1163. return $extendList;
  1164. }
  1165. //
  1166. // A Mixin call, with an optional argument list
  1167. //
  1168. // #mixins > .square(#fff);
  1169. // .rounded(4px, black);
  1170. // .button;
  1171. //
  1172. // The `while` loop is there because mixins can be
  1173. // namespaced, but we only support the child and descendant
  1174. // selector for now.
  1175. //
  1176. private function parseMixinCall(){
  1177. $char = $this->input[$this->pos];
  1178. if( $char !== '.' && $char !== '#' ){
  1179. return;
  1180. }
  1181. $index = $this->pos;
  1182. $this->save(); // stop us absorbing part of an invalid selector
  1183. $elements = $this->parseMixinCallElements();
  1184. if( $elements ){
  1185. if( $this->MatchChar('(') ){
  1186. $returned = $this->parseMixinArgs(true);
  1187. $args = $returned['args'];
  1188. $this->expectChar(')');
  1189. }else{
  1190. $args = array();
  1191. }
  1192. $important = $this->parseImportant();
  1193. if( $this->parseEnd() ){
  1194. $this->forget();
  1195. return $this->NewObj5('Less_Tree_Mixin_Call', array( $elements, $args, $index, $this->env->currentFileInfo, $important));
  1196. }
  1197. }
  1198. $this->restore();
  1199. }
  1200. private function parseMixinCallElements(){
  1201. $elements = array();
  1202. $c = null;
  1203. while( true ){
  1204. $elemIndex = $this->pos;
  1205. $e = $this->MatchReg('/\\G[#.](?:[\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/');
  1206. if( !$e ){
  1207. break;
  1208. }
  1209. $elements[] = $this->NewObj4('Less_Tree_Element', array($c, $e[0], $elemIndex, $this->env->currentFileInfo));
  1210. $c = $this->MatchChar('>');
  1211. }
  1212. return $elements;
  1213. }
  1214. /**
  1215. * @param boolean $isCall
  1216. */
  1217. private function parseMixinArgs( $isCall ){
  1218. $expressions = array();
  1219. $argsSemiColon = array();
  1220. $isSemiColonSeperated = null;
  1221. $argsComma = array();
  1222. $expressionContainsNamed = null;
  1223. $name = null;
  1224. $returner = array('args'=>array(), 'variadic'=> false);
  1225. $this->save();
  1226. while( true ){
  1227. if( $isCall ){
  1228. $arg = $this->MatchFuncs( array( 'parseDetachedRuleset','parseExpression' ) );
  1229. } else {
  1230. $this->parseComments();
  1231. if( $this->input[ $this->pos ] === '.' && $this->MatchReg('/\\G\.{3}/') ){
  1232. $returner['variadic'] = true;
  1233. if( $this->MatchChar(";") && !$isSemiColonSeperated ){
  1234. $isSemiColonSeperated = true;
  1235. }
  1236. if( $isSemiColonSeperated ){
  1237. $argsSemiColon[] = array('variadic'=>true);
  1238. }else{
  1239. $argsComma[] = array('variadic'=>true);
  1240. }
  1241. break;
  1242. }
  1243. $arg = $this->MatchFuncs( array('parseEntitiesVariable','parseEntitiesLiteral','parseEntitiesKeyword') );
  1244. }
  1245. if( !$arg ){
  1246. break;
  1247. }
  1248. $nameLoop = null;
  1249. if( $arg instanceof Less_Tree_Expression ){
  1250. $arg->throwAwayComments();
  1251. }
  1252. $value = $arg;
  1253. $val = null;
  1254. if( $isCall ){
  1255. // Variable
  1256. if( property_exists($arg,'value') && count($arg->value) == 1 ){
  1257. $val = $arg->value[0];
  1258. }
  1259. } else {
  1260. $val = $arg;
  1261. }
  1262. if( $val instanceof Less_Tree_Variable ){
  1263. if( $this->MatchChar(':') ){
  1264. if( $expressions ){
  1265. if( $isSemiColonSeperated ){
  1266. $this->Error('Cannot mix ; and , as delimiter types');
  1267. }
  1268. $expressionContainsNamed = true;
  1269. }
  1270. // we do not support setting a ruleset as a default variable - it doesn't make sense
  1271. // However if we do want to add it, there is nothing blocking it, just don't error
  1272. // and remove isCall dependency below
  1273. $value = null;
  1274. if( $isCall ){
  1275. $value = $this->parseDetachedRuleset();
  1276. }
  1277. if( !$value ){
  1278. $value = $this->parseExpression();
  1279. }
  1280. if( !$value ){
  1281. if( $isCall ){
  1282. $this->Error('could not understand value for named argument');
  1283. } else {
  1284. $this->restore();
  1285. $returner['args'] = array();
  1286. return $returner;
  1287. }
  1288. }
  1289. $nameLoop = ($name = $val->name);
  1290. }elseif( !$isCall && $this->MatchReg('/\\G\.{3}/') ){
  1291. $returner['variadic'] = true;
  1292. if( $this->MatchChar(";") && !$isSemiColonSeperated ){
  1293. $isSemiColonSeperated = true;
  1294. }
  1295. if( $isSemiColonSeperated ){
  1296. $argsSemiColon[] = array('name'=> $arg->name, 'variadic' => true);
  1297. }else{
  1298. $argsComma[] = array('name'=> $arg->name, 'variadic' => true);
  1299. }
  1300. break;
  1301. }elseif( !$isCall ){
  1302. $name = $nameLoop = $val->name;
  1303. $value = null;
  1304. }
  1305. }
  1306. if( $value ){
  1307. $expressions[] = $value;
  1308. }
  1309. $argsComma[] = array('name'=>$nameLoop, 'value'=>$value );
  1310. if( $this->MatchChar(',') ){
  1311. continue;
  1312. }
  1313. if( $this->MatchChar(';') || $isSemiColonSeperated ){
  1314. if( $expressionContainsNamed ){
  1315. $this->Error('Cannot mix ; and , as delimiter types');
  1316. }
  1317. $isSemiColonSeperated = true;
  1318. if( count($expressions) > 1 ){
  1319. $value = $this->NewObj1('Less_Tree_Value', $expressions);
  1320. }
  1321. $argsSemiColon[] = array('name'=>$name, 'value'=>$value );
  1322. $name = null;
  1323. $expressions = array();
  1324. $expressionContainsNamed = false;
  1325. }
  1326. }
  1327. $this->forget();
  1328. $returner['args'] = ($isSemiColonSeperated ? $argsSemiColon : $argsComma);
  1329. return $returner;
  1330. }
  1331. //
  1332. // A Mixin definition, with a list of parameters
  1333. //
  1334. // .rounded (@radius: 2px, @color) {
  1335. // ...
  1336. // }
  1337. //
  1338. // Until we have a finer grained state-machine, we have to
  1339. // do a look-ahead, to make sure we don't have a mixin call.
  1340. // See the `rule` function for more information.
  1341. //
  1342. // We start by matching `.rounded (`, and then proceed on to
  1343. // the argument list, which has optional default values.
  1344. // We store the parameters in `params`, with a `value` key,
  1345. // if there is a value, such as in the case of `@radius`.
  1346. //
  1347. // Once we've got our params list, and a closing `)`, we parse
  1348. // the `{...}` block.
  1349. //
  1350. private function parseMixinDefinition(){
  1351. $cond = null;
  1352. $char = $this->input[$this->pos];
  1353. if( ($char !== '.' && $char !== '#') || ($char === '{' && $this->PeekReg('/\\G[^{]*\}/')) ){
  1354. return;
  1355. }
  1356. $this->save();
  1357. $match = $this->MatchReg('/\\G([#.](?:[\w-]|\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/');
  1358. if( $match ){
  1359. $name = $match[1];
  1360. $argInfo = $this->parseMixinArgs( false );
  1361. $params = $argInfo['args'];
  1362. $variadic = $argInfo['variadic'];
  1363. // .mixincall("@{a}");
  1364. // looks a bit like a mixin definition..
  1365. // also
  1366. // .mixincall(@a: {rule: set;});
  1367. // so we have to be nice and restore
  1368. if( !$this->MatchChar(')') ){
  1369. $this->furthest = $this->pos;
  1370. $this->restore();
  1371. return;
  1372. }
  1373. $this->parseComments();
  1374. if ($this->MatchReg('/\\Gwhen/')) { // Guard
  1375. $cond = $this->expect('parseConditions', 'Expected conditions');
  1376. }
  1377. $ruleset = $this->parseBlock();
  1378. if( is_array($ruleset) ){
  1379. $this->forget();
  1380. return $this->NewObj5('Less_Tree_Mixin_Definition', array( $name, $params, $ruleset, $cond, $variadic));
  1381. }
  1382. $this->restore();
  1383. }else{
  1384. $this->forget();
  1385. }
  1386. }
  1387. //
  1388. // Entities are the smallest recognized token,
  1389. // and can be found inside a rule's value.
  1390. //
  1391. private function parseEntity(){
  1392. return $this->MatchFuncs( array('parseEntitiesLiteral','parseEntitiesVariable','parseEntitiesUrl','parseEntitiesCall','parseEntitiesKeyword','parseEntitiesJavascript','parseComment') );
  1393. }
  1394. //
  1395. // A Rule terminator. Note that we use `peek()` to check for '}',
  1396. // because the `block` rule will be expecting it, but we still need to make sure
  1397. // it's there, if ';' was omitted.
  1398. //
  1399. private function parseEnd(){
  1400. return $this->MatchChar(';') || $this->PeekChar('}');
  1401. }
  1402. //
  1403. // IE's alpha function
  1404. //
  1405. // alpha(opacity=88)
  1406. //
  1407. private function parseAlpha(){
  1408. if ( ! $this->MatchReg('/\\G\(opacity=/i')) {
  1409. return;
  1410. }
  1411. $value = $this->MatchReg('/\\G[0-9]+/');
  1412. if( $value ){
  1413. $value = $value[0];
  1414. }else{
  1415. $value = $this->parseEntitiesVariable();
  1416. if( !$value ){
  1417. return;
  1418. }
  1419. }
  1420. $this->expectChar(')');
  1421. return $this->NewObj1('Less_Tree_Alpha',$value);
  1422. }
  1423. //
  1424. // A Selector Element
  1425. //
  1426. // div
  1427. // + h1
  1428. // #socks
  1429. // input[type="text"]
  1430. //
  1431. // Elements are the building blocks for Selectors,
  1432. // they are made out of a `Combinator` (see combinator rule),
  1433. // and an element name, such as a tag a class, or `*`.
  1434. //
  1435. private function parseElement(){
  1436. $c = $this->parseCombinator();
  1437. $index = $this->pos;
  1438. $e = $this->match( array('/\\G(?:\d+\.\d+|\d+)%/', '/\\G(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/',
  1439. '#*', '#&', 'parseAttribute', '/\\G\([^()@]+\)/', '/\\G[\.#](?=@)/', 'parseEntitiesVariableCurly') );
  1440. if( is_null($e) ){
  1441. $this->save();
  1442. if( $this->MatchChar('(') ){
  1443. if( ($v = $this->parseSelector()) && $this->MatchChar(')') ){
  1444. $e = $this->NewObj1('Less_Tree_Paren',$v);
  1445. $this->forget();
  1446. }else{
  1447. $this->restore();
  1448. }
  1449. }else{
  1450. $this->forget();
  1451. }
  1452. }
  1453. if( !is_null($e) ){
  1454. return $this->NewObj4('Less_Tree_Element',array( $c, $e, $index, $this->env->currentFileInfo));
  1455. }
  1456. }
  1457. //
  1458. // Combinators combine elements together, in a Selector.
  1459. //
  1460. // Because our parser isn't white-space sensitive, special care
  1461. // has to be taken, when parsing the descendant combinator, ` `,
  1462. // as it's an empty space. We have to check the previous character
  1463. // in the input, to see if it's a ` ` character.
  1464. //
  1465. private function parseCombinator(){
  1466. if( $this->pos < $this->input_len ){
  1467. $c = $this->input[$this->pos];
  1468. if ($c === '>' || $c === '+' || $c === '~' || $c === '|' || $c === '^' ){
  1469. $this->pos++;
  1470. if( $this->input[$this->pos] === '^' ){
  1471. $c = '^^';
  1472. $this->pos++;
  1473. }
  1474. $this->skipWhitespace(0);
  1475. return $c;
  1476. }
  1477. if( $this->pos > 0 && $this->isWhitespace(-1) ){
  1478. return ' ';
  1479. }
  1480. }
  1481. }
  1482. //
  1483. // A CSS selector (see selector below)
  1484. // with less extensions e.g. the ability to extend and guard
  1485. //
  1486. private function parseLessSelector(){
  1487. return $this->parseSelector(true);
  1488. }
  1489. //
  1490. // A CSS Selector
  1491. //
  1492. // .class > div + h1
  1493. // li a:hover
  1494. //
  1495. // Selectors are made out of one or more Elements, see above.
  1496. //
  1497. private function parseSelector( $isLess = false ){
  1498. $elements = array();
  1499. $extendList = array();
  1500. $condition = null;
  1501. $when = false;
  1502. $extend = false;
  1503. $e = null;
  1504. $c = null;
  1505. $index = $this->pos;
  1506. while( ($isLess && ($extend = $this->parseExtend())) || ($isLess && ($when = $this->MatchReg('/\\Gwhen/') )) || ($e = $this->parseElement()) ){
  1507. if( $when ){
  1508. $condition = $this->expect('parseConditions', 'expected condition');
  1509. }elseif( $condition ){
  1510. //error("CSS guard can only be used at the end of selector");
  1511. }elseif( $extend ){
  1512. $extendList = array_merge($extendList,$extend);
  1513. }else{
  1514. //if( count($extendList) ){
  1515. //error("Extend can only be used at the end of selector");
  1516. //}
  1517. if( $this->pos < $this->input_len ){
  1518. $c = $this->input[ $this->pos ];
  1519. }
  1520. $elements[] = $e;
  1521. $e = null;
  1522. }
  1523. if( $c === '{' || $c === '}' || $c === ';' || $c === ',' || $c === ')') { break; }
  1524. }
  1525. if( $elements ){
  1526. return $this->NewObj5('Less_Tree_Selector',array($elements, $extendList, $condition, $index, $this->env->currentFileInfo));
  1527. }
  1528. if( $extendList ) {
  1529. $this->Error('Extend must be used to extend a selector, it cannot be used on its own');
  1530. }
  1531. }
  1532. private function parseTag(){
  1533. return ( $tag = $this->MatchReg('/\\G[A-Za-z][A-Za-z-]*[0-9]?/') ) ? $tag : $this->MatchChar('*');
  1534. }
  1535. private function parseAttribute(){
  1536. $val = null;
  1537. if( !$this->MatchChar('[') ){
  1538. return;
  1539. }
  1540. $key = $this->parseEntitiesVariableCurly();
  1541. if( !$key ){
  1542. $key = $this->expect('/\\G(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\\\.)+/');
  1543. }
  1544. $op = $this->MatchReg('/\\G[|~*$^]?=/');
  1545. if( $op ){
  1546. $val = $this->match( array('parseEntitiesQuoted','/\\G[0-9]+%/','/\\G[\w-]+/','parseEntitiesVariableCurly') );
  1547. }
  1548. $this->expectChar(']');
  1549. return $this->NewObj3('Less_Tree_Attribute',array( $key, $op[0], $val));
  1550. }
  1551. //
  1552. // The `block` rule is used by `ruleset` and `mixin.definition`.
  1553. // It's a wrapper around the `primary` rule, with added `{}`.
  1554. //
  1555. private function parseBlock(){
  1556. if( $this->MatchChar('{') ){
  1557. $content = $this->parsePrimary();
  1558. if( $this->MatchChar('}') ){
  1559. return $content;
  1560. }
  1561. }
  1562. }
  1563. private function parseBlockRuleset(){
  1564. $block = $this->parseBlock();
  1565. if( $block ){
  1566. $block = $this->NewObj2('Less_Tree_Ruleset',array( null, $block));
  1567. }
  1568. return $block;
  1569. }
  1570. private function parseDetachedRuleset(){
  1571. $blockRuleset = $this->parseBlockRuleset();
  1572. if( $blockRuleset ){
  1573. return $this->NewObj1('Less_Tree_DetachedRuleset',$blockRuleset);
  1574. }
  1575. }
  1576. //
  1577. // div, .class, body > p {...}
  1578. //
  1579. private function parseRuleset(){
  1580. $selectors = array();
  1581. $this->save();
  1582. while( true ){
  1583. $s = $this->parseLessSelector();
  1584. if( !$s ){
  1585. break;
  1586. }
  1587. $selectors[] = $s;
  1588. $this->parseComments();
  1589. if( $s->condition && count($selectors) > 1 ){
  1590. $this->Error('Guards are only currently allowed on a single selector.');
  1591. }
  1592. if( !$this->MatchChar(',') ){
  1593. break;
  1594. }
  1595. if( $s->condition ){
  1596. $this->Error('Guards are only currently allowed on a single selector.');
  1597. }
  1598. $this->parseComments();
  1599. }
  1600. if( $selectors ){
  1601. $rules = $this->parseBlock();
  1602. if( is_array($rules) ){
  1603. $this->forget();
  1604. return $this->NewObj2('Less_Tree_Ruleset',array( $selectors, $rules)); //Less_Environment::$strictImports
  1605. }
  1606. }
  1607. // Backtrack
  1608. $this->furthest = $this->pos;
  1609. $this->restore();
  1610. }
  1611. /**
  1612. * Custom less.php parse function for finding simple name-value css pairs
  1613. * ex: width:100px;
  1614. *
  1615. */
  1616. private function parseNameValue(){
  1617. $index = $this->pos;
  1618. $this->save();
  1619. //$match = $this->MatchReg('/\\G([a-zA-Z\-]+)\s*:\s*((?:\'")?[a-zA-Z0-9\-% \.,!]+?(?:\'")?)\s*([;}])/');
  1620. $match = $this->MatchReg('/\\G([a-zA-Z\-]+)\s*:\s*([\'"]?[#a-zA-Z0-9\-%\.,]+?[\'"]?) *(! *important)?\s*([;}])/');
  1621. if( $match ){
  1622. if( $match[4] == '}' ){
  1623. $this->pos = $index + strlen($match[0])-1;
  1624. }
  1625. if( $match[3] ){
  1626. $match[2] .= ' !important';
  1627. }
  1628. return $this->NewObj4('Less_Tree_NameValue',array( $match[1], $match[2], $index, $this->env->currentFileInfo));
  1629. }
  1630. $this->restore();
  1631. }
  1632. private function parseRule( $tryAnonymous = null ){
  1633. $merge = false;
  1634. $startOfRule = $this->pos;
  1635. $c = $this->input[$this->pos];
  1636. if( $c === '.' || $c === '#' || $c === '&' ){
  1637. return;
  1638. }
  1639. $this->save();
  1640. $name = $this->MatchFuncs( array('parseVariable','parseRuleProperty'));
  1641. if( $name ){
  1642. $isVariable = is_string($name);
  1643. $value = null;
  1644. if( $isVariable ){
  1645. $value = $this->parseDetachedRuleset();
  1646. }
  1647. $important = null;
  1648. if( !$value ){
  1649. // prefer to try to parse first if its a variable or we are compressing
  1650. // but always fallback on the other one
  1651. //if( !$tryAnonymous && is_string($name) && $name[0] === '@' ){
  1652. if( !$tryAnonymous && (Less_Parser::$options['compress'] || $isVariable) ){
  1653. $value = $this->MatchFuncs( array('parseValue','parseAnonymousValue'));
  1654. }else{
  1655. $value = $this->MatchFuncs( array('parseAnonymousValue','parseValue'));
  1656. }
  1657. $important = $this->parseImportant();
  1658. // a name returned by this.ruleProperty() is always an array of the form:
  1659. // [string-1, ..., string-n, ""] or [string-1, ..., string-n, "+"]
  1660. // where each item is a tree.Keyword or tree.Variable
  1661. if( !$isVariable && is_array($name) ){
  1662. $nm = array_pop($name);
  1663. if( $nm->value ){
  1664. $merge = $nm->value;
  1665. }
  1666. }
  1667. }
  1668. if( $value && $this->parseEnd() ){
  1669. $this->forget();
  1670. return $this->NewObj6('Less_Tree_Rule',array( $name, $value, $important, $merge, $startOfRule, $this->env->currentFileInfo));
  1671. }else{
  1672. $this->furthest = $this->pos;
  1673. $this->restore();
  1674. if( $value && !$tryAnonymous ){
  1675. return $this->parseRule(true);
  1676. }
  1677. }
  1678. }else{
  1679. $this->forget();
  1680. }
  1681. }
  1682. function parseAnonymousValue(){
  1683. if( preg_match('/\\G([^@+\/\'"*`(;{}-]*);/',$this->input, $match, 0, $this->pos) ){
  1684. $this->pos += strlen($match[1]);
  1685. return $this->NewObj1('Less_Tree_Anonymous',$match[1]);
  1686. }
  1687. }
  1688. //
  1689. // An @import directive
  1690. //
  1691. // @import "lib";
  1692. //
  1693. // Depending on our environment, importing is done differently:
  1694. // In the browser, it's an XHR request, in Node, it would be a
  1695. // file-system operation. The function used for importing is
  1696. // stored in `import`, which we pass to the Import constructor.
  1697. //
  1698. private function parseImport(){
  1699. $this->save();
  1700. $dir = $this->MatchReg('/\\G@import?\s+/');
  1701. if( $dir ){
  1702. $options = $this->parseImportOptions();
  1703. $path = $this->MatchFuncs( array('parseEntitiesQuoted','parseEntitiesUrl'));
  1704. if( $path ){
  1705. $features = $this->parseMediaFeatures();
  1706. if( $this->MatchChar(';') ){
  1707. if( $features ){
  1708. $features = $this->NewObj1('Less_Tree_Value',$features);
  1709. }
  1710. $this->forget();
  1711. return $this->NewObj5('Less_Tree_Import',array( $path, $features, $options, $this->pos, $this->env->currentFileInfo));
  1712. }
  1713. }
  1714. }
  1715. $this->restore();
  1716. }
  1717. private function parseImportOptions(){
  1718. $options = array();
  1719. // list of options, surrounded by parens
  1720. if( !$this->MatchChar('(') ){
  1721. return $options;
  1722. }
  1723. do{
  1724. $optionName = $this->parseImportOption();
  1725. if( $optionName ){
  1726. $value = true;
  1727. switch( $optionName ){
  1728. case "css":
  1729. $optionName = "less";
  1730. $value = false;
  1731. break;
  1732. case "once":
  1733. $optionName = "multiple";
  1734. $value = false;
  1735. break;
  1736. }
  1737. $options[$optionName] = $value;
  1738. if( !$this->MatchChar(',') ){ break; }
  1739. }
  1740. }while( $optionName );
  1741. $this->expectChar(')');
  1742. return $options;
  1743. }
  1744. private function parseImportOption(){
  1745. $opt = $this->MatchReg('/\\G(less|css|multiple|once|inline|reference|optional)/');
  1746. if( $opt ){
  1747. return $opt[1];
  1748. }
  1749. }
  1750. private function parseMediaFeature() {
  1751. $nodes = array();
  1752. do{
  1753. $e = $this->MatchFuncs(array('parseEntitiesKeyword','parseEntitiesVariable'));
  1754. if( $e ){
  1755. $nodes[] = $e;
  1756. } elseif ($this->MatchChar('(')) {
  1757. $p = $this->parseProperty();
  1758. $e = $this->parseValue();
  1759. if ($this->MatchChar(')')) {
  1760. if ($p && $e) {
  1761. $r = $this->NewObj7('Less_Tree_Rule', array( $p, $e, null, null, $this->pos, $this->env->currentFileInfo, true));
  1762. $nodes[] = $this->NewObj1('Less_Tree_Paren',$r);
  1763. } elseif ($e) {
  1764. $nodes[] = $this->NewObj1('Less_Tree_Paren',$e);
  1765. } else {
  1766. return null;
  1767. }
  1768. } else
  1769. return null;
  1770. }
  1771. } while ($e);
  1772. if ($nodes) {
  1773. return $this->NewObj1('Less_Tree_Expression',$nodes);
  1774. }
  1775. }
  1776. private function parseMediaFeatures() {
  1777. $features = array();
  1778. do{
  1779. $e = $this->parseMediaFeature();
  1780. if( $e ){
  1781. $features[] = $e;
  1782. if (!$this->MatchChar(',')) break;
  1783. }else{
  1784. $e = $this->parseEntitiesVariable();
  1785. if( $e ){
  1786. $features[] = $e;
  1787. if (!$this->MatchChar(',')) break;
  1788. }
  1789. }
  1790. } while ($e);
  1791. return $features ? $features : null;
  1792. }
  1793. private function parseMedia() {
  1794. if( $this->MatchReg('/\\G@media/') ){
  1795. $features = $this->parseMediaFeatures();
  1796. $rules = $this->parseBlock();
  1797. if( is_array($rules) ){
  1798. return $this->NewObj4('Less_Tree_Media',array( $rules, $features, $this->pos, $this->env->currentFileInfo));
  1799. }
  1800. }
  1801. }
  1802. //
  1803. // A CSS Directive
  1804. //
  1805. // @charset "utf-8";
  1806. //
  1807. private function parseDirective(){
  1808. if( !$this->PeekChar('@') ){
  1809. return;
  1810. }
  1811. $rules = null;
  1812. $index = $this->pos;
  1813. $hasBlock = true;
  1814. $hasIdentifier = false;
  1815. $hasExpression = false;
  1816. $hasUnknown = false;
  1817. $value = $this->MatchFuncs(array('parseImport','parseMedia'));
  1818. if( $value ){
  1819. return $value;
  1820. }
  1821. $this->save();
  1822. $name = $this->MatchReg('/\\G@[a-z-]+/');
  1823. if( !$name ) return;
  1824. $name = $name[0];
  1825. $nonVendorSpecificName = $name;
  1826. $pos = strpos($name,'-', 2);
  1827. if( $name[1] == '-' && $pos > 0 ){
  1828. $nonVendorSpecificName = "@" . substr($name, $pos + 1);
  1829. }
  1830. switch( $nonVendorSpecificName ){
  1831. /*
  1832. case "@font-face":
  1833. case "@viewport":
  1834. case "@top-left":
  1835. case "@top-left-corner":
  1836. case "@top-center":
  1837. case "@top-right":
  1838. case "@top-right-corner":
  1839. case "@bottom-left":
  1840. case "@bottom-left-corner":
  1841. case "@bottom-center":
  1842. case "@bottom-right":
  1843. case "@bottom-right-corner":
  1844. case "@left-top":
  1845. case "@left-middle":
  1846. case "@left-bottom":
  1847. case "@right-top":
  1848. case "@right-middle":
  1849. case "@right-bottom":
  1850. hasBlock = true;
  1851. break;
  1852. */
  1853. case "@charset":
  1854. $hasIdentifier = true;
  1855. $hasBlock = false;
  1856. break;
  1857. case "@namespace":
  1858. $hasExpression = true;
  1859. $hasBlock = false;
  1860. break;
  1861. case "@keyframes":
  1862. $hasIdentifier = true;
  1863. break;
  1864. case "@host":
  1865. case "@page":
  1866. case "@document":
  1867. case "@supports":
  1868. $hasUnknown = true;
  1869. break;
  1870. }
  1871. if( $hasIdentifier ){
  1872. $value = $this->parseEntity();
  1873. if( !$value ){
  1874. $this->error("expected " . $name . " identifier");
  1875. }
  1876. } else if( $hasExpression ){
  1877. $value = $this->parseExpression();
  1878. if( !$value ){
  1879. $this->error("expected " . $name. " expression");
  1880. }
  1881. } else if ($hasUnknown) {
  1882. $value = $this->MatchReg('/\\G[^{;]+/');
  1883. if( $value ){
  1884. $value = $this->NewObj1('Less_Tree_Anonymous',trim($value[0]));
  1885. }
  1886. }
  1887. if( $hasBlock ){
  1888. $rules = $this->parseBlockRuleset();
  1889. }
  1890. if( $rules || (!$hasBlock && $value && $this->MatchChar(';'))) {
  1891. $this->forget();
  1892. return $this->NewObj5('Less_Tree_Directive',array($name, $value, $rules, $index, $this->env->currentFileInfo));
  1893. }
  1894. $this->restore();
  1895. }
  1896. //
  1897. // A Value is a comma-delimited list of Expressions
  1898. //
  1899. // font-family: Baskerville, Georgia, serif;
  1900. //
  1901. // In a Rule, a Value represents everything after the `:`,
  1902. // and before the `;`.
  1903. //
  1904. private function parseValue(){
  1905. $expressions = array();
  1906. do{
  1907. $e = $this->parseExpression();
  1908. if( $e ){
  1909. $expressions[] = $e;
  1910. if (! $this->MatchChar(',')) {
  1911. break;
  1912. }
  1913. }
  1914. }while($e);
  1915. if( $expressions ){
  1916. return $this->NewObj1('Less_Tree_Value',$expressions);
  1917. }
  1918. }
  1919. private function parseImportant (){
  1920. if( $this->PeekChar('!') && $this->MatchReg('/\\G! *important/') ){
  1921. return ' !important';
  1922. }
  1923. }
  1924. private function parseSub (){
  1925. if( $this->MatchChar('(') ){
  1926. $a = $this->parseAddition();
  1927. if( $a ){
  1928. $this->expectChar(')');
  1929. return $this->NewObj2('Less_Tree_Expression',array( array($a), true) ); //instead of $e->parens = true so the value is cached
  1930. }
  1931. }
  1932. }
  1933. /**
  1934. * Parses multiplication operation
  1935. *
  1936. * @return Less_Tree_Operation|null
  1937. */
  1938. function parseMultiplication(){
  1939. $return = $m = $this->parseOperand();
  1940. if( $return ){
  1941. while( true ){
  1942. $isSpaced = $this->isWhitespace( -1 );
  1943. if( $this->PeekReg('/\\G\/[*\/]/') ){
  1944. break;
  1945. }
  1946. $op = $this->MatchChar('/');
  1947. if( !$op ){
  1948. $op = $this->MatchChar('*');
  1949. if( !$op ){
  1950. break;
  1951. }
  1952. }
  1953. $a = $this->parseOperand();
  1954. if(!$a) { break; }
  1955. $m->parensInOp = true;
  1956. $a->parensInOp = true;
  1957. $return = $this->NewObj3('Less_Tree_Operation',array( $op, array( $return, $a ), $isSpaced) );
  1958. }
  1959. }
  1960. return $return;
  1961. }
  1962. /**
  1963. * Parses an addition operation
  1964. *
  1965. * @return Less_Tree_Operation|null
  1966. */
  1967. private function parseAddition (){
  1968. $return = $m = $this->parseMultiplication();
  1969. if( $return ){
  1970. while( true ){
  1971. $isSpaced = $this->isWhitespace( -1 );
  1972. $op = $this->MatchReg('/\\G[-+]\s+/');
  1973. if( $op ){
  1974. $op = $op[0];
  1975. }else{
  1976. if( !$isSpaced ){
  1977. $op = $this->match(array('#+','#-'));
  1978. }
  1979. if( !$op ){
  1980. break;
  1981. }
  1982. }
  1983. $a = $this->parseMultiplication();
  1984. if( !$a ){
  1985. break;
  1986. }
  1987. $m->parensInOp = true;
  1988. $a->parensInOp = true;
  1989. $return = $this->NewObj3('Less_Tree_Operation',array($op, array($return, $a), $isSpaced));
  1990. }
  1991. }
  1992. return $return;
  1993. }
  1994. /**
  1995. * Parses the conditions
  1996. *
  1997. * @return Less_Tree_Condition|null
  1998. */
  1999. private function parseConditions() {
  2000. $index = $this->pos;
  2001. $return = $a = $this->parseCondition();
  2002. if( $a ){
  2003. while( true ){
  2004. if( !$this->PeekReg('/\\G,\s*(not\s*)?\(/') || !$this->MatchChar(',') ){
  2005. break;
  2006. }
  2007. $b = $this->parseCondition();
  2008. if( !$b ){
  2009. break;
  2010. }
  2011. $return = $this->NewObj4('Less_Tree_Condition',array('or', $return, $b, $index));
  2012. }
  2013. return $return;
  2014. }
  2015. }
  2016. private function parseCondition() {
  2017. $index = $this->pos;
  2018. $negate = false;
  2019. $c = null;
  2020. if ($this->MatchReg('/\\Gnot/')) $negate = true;
  2021. $this->expectChar('(');
  2022. $a = $this->MatchFuncs(array('parseAddition','parseEntitiesKeyword','parseEntitiesQuoted'));
  2023. if( $a ){
  2024. $op = $this->MatchReg('/\\G(?:>=|<=|=<|[<=>])/');
  2025. if( $op ){
  2026. $b = $this->MatchFuncs(array('parseAddition','parseEntitiesKeyword','parseEntitiesQuoted'));
  2027. if( $b ){
  2028. $c = $this->NewObj5('Less_Tree_Condition',array($op[0], $a, $b, $index, $negate));
  2029. } else {
  2030. $this->Error('Unexpected expression');
  2031. }
  2032. } else {
  2033. $k = $this->NewObj1('Less_Tree_Keyword','true');
  2034. $c = $this->NewObj5('Less_Tree_Condition',array('=', $a, $k, $index, $negate));
  2035. }
  2036. $this->expectChar(')');
  2037. return $this->MatchReg('/\\Gand/') ? $this->NewObj3('Less_Tree_Condition',array('and', $c, $this->parseCondition())) : $c;
  2038. }
  2039. }
  2040. /**
  2041. * An operand is anything that can be part of an operation,
  2042. * such as a Color, or a Variable
  2043. *
  2044. */
  2045. private function parseOperand (){
  2046. $negate = false;
  2047. $offset = $this->pos+1;
  2048. if( $offset >= $this->input_len ){
  2049. return;
  2050. }
  2051. $char = $this->input[$offset];
  2052. if( $char === '@' || $char === '(' ){
  2053. $negate = $this->MatchChar('-');
  2054. }
  2055. $o = $this->MatchFuncs(array('parseSub','parseEntitiesDimension','parseEntitiesColor','parseEntitiesVariable','parseEntitiesCall'));
  2056. if( $negate ){
  2057. $o->parensInOp = true;
  2058. $o = $this->NewObj1('Less_Tree_Negative',$o);
  2059. }
  2060. return $o;
  2061. }
  2062. /**
  2063. * Expressions either represent mathematical operations,
  2064. * or white-space delimited Entities.
  2065. *
  2066. * 1px solid black
  2067. * @var * 2
  2068. *
  2069. * @return Less_Tree_Expression|null
  2070. */
  2071. private function parseExpression (){
  2072. $entities = array();
  2073. do{
  2074. $e = $this->MatchFuncs(array('parseAddition','parseEntity'));
  2075. if( $e ){
  2076. $entities[] = $e;
  2077. // operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here
  2078. if( !$this->PeekReg('/\\G\/[\/*]/') ){
  2079. $delim = $this->MatchChar('/');
  2080. if( $delim ){
  2081. $entities[] = $this->NewObj1('Less_Tree_Anonymous',$delim);
  2082. }
  2083. }
  2084. }
  2085. }while($e);
  2086. if( $entities ){
  2087. return $this->NewObj1('Less_Tree_Expression',$entities);
  2088. }
  2089. }
  2090. /**
  2091. * Parse a property
  2092. * eg: 'min-width', 'orientation', etc
  2093. *
  2094. * @return string
  2095. */
  2096. private function parseProperty (){
  2097. $name = $this->MatchReg('/\\G(\*?-?[_a-zA-Z0-9-]+)\s*:/');
  2098. if( $name ){
  2099. return $name[1];
  2100. }
  2101. }
  2102. /**
  2103. * Parse a rule property
  2104. * eg: 'color', 'width', 'height', etc
  2105. *
  2106. * @return string
  2107. */
  2108. private function parseRuleProperty(){
  2109. $offset = $this->pos;
  2110. $name = array();
  2111. $index = array();
  2112. $length = 0;
  2113. $this->rulePropertyMatch('/\\G(\*?)/', $offset, $length, $index, $name );
  2114. while( $this->rulePropertyMatch('/\\G((?:[\w-]+)|(?:@\{[\w-]+\}))/', $offset, $length, $index, $name )); // !
  2115. if( (count($name) > 1) && $this->rulePropertyMatch('/\\G\s*((?:\+_|\+)?)\s*:/', $offset, $length, $index, $name) ){
  2116. // at last, we have the complete match now. move forward,
  2117. // convert name particles to tree objects and return:
  2118. $this->skipWhitespace($length);
  2119. if( $name[0] === '' ){
  2120. array_shift($name);
  2121. array_shift($index);
  2122. }
  2123. foreach($name as $k => $s ){
  2124. if( !$s || $s[0] !== '@' ){
  2125. $name[$k] = $this->NewObj1('Less_Tree_Keyword',$s);
  2126. }else{
  2127. $name[$k] = $this->NewObj3('Less_Tree_Variable',array('@' . substr($s,2,-1), $index[$k], $this->env->currentFileInfo));
  2128. }
  2129. }
  2130. return $name;
  2131. }
  2132. }
  2133. private function rulePropertyMatch( $re, &$offset, &$length, &$index, &$name ){
  2134. preg_match($re, $this->input, $a, 0, $offset);
  2135. if( $a ){
  2136. $index[] = $this->pos + $length;
  2137. $length += strlen($a[0]);
  2138. $offset += strlen($a[0]);
  2139. $name[] = $a[1];
  2140. return true;
  2141. }
  2142. }
  2143. public static function serializeVars( $vars ){
  2144. $s = '';
  2145. foreach($vars as $name => $value){
  2146. $s .= (($name[0] === '@') ? '' : '@') . $name .': '. $value . ((substr($value,-1) === ';') ? '' : ';');
  2147. }
  2148. return $s;
  2149. }
  2150. /**
  2151. * Some versions of php have trouble with method_exists($a,$b) if $a is not an object
  2152. *
  2153. * @param string $b
  2154. */
  2155. public static function is_method($a,$b){
  2156. return is_object($a) && method_exists($a,$b);
  2157. }
  2158. /**
  2159. * Round numbers similarly to javascript
  2160. * eg: 1.499999 to 1 instead of 2
  2161. *
  2162. */
  2163. public static function round($i, $precision = 0){
  2164. $precision = pow(10,$precision);
  2165. $i = $i*$precision;
  2166. $ceil = ceil($i);
  2167. $floor = floor($i);
  2168. if( ($ceil - $i) <= ($i - $floor) ){
  2169. return $ceil/$precision;
  2170. }else{
  2171. return $floor/$precision;
  2172. }
  2173. }
  2174. /**
  2175. * Create Less_Tree_* objects and optionally generate a cache string
  2176. *
  2177. * @return mixed
  2178. */
  2179. public function NewObj0($class){
  2180. $obj = new $class();
  2181. if( $this->CacheEnabled() ){
  2182. $obj->cache_string = ' new '.$class.'()';
  2183. }
  2184. return $obj;
  2185. }
  2186. public function NewObj1($class, $arg){
  2187. $obj = new $class( $arg );
  2188. if( $this->CacheEnabled() ){
  2189. $obj->cache_string = ' new '.$class.'('.Less_Parser::ArgString($arg).')';
  2190. }
  2191. return $obj;
  2192. }
  2193. public function NewObj2($class, $args){
  2194. $obj = new $class( $args[0], $args[1] );
  2195. if( $this->CacheEnabled() ){
  2196. $this->ObjCache( $obj, $class, $args);
  2197. }
  2198. return $obj;
  2199. }
  2200. public function NewObj3($class, $args){
  2201. $obj = new $class( $args[0], $args[1], $args[2] );
  2202. if( $this->CacheEnabled() ){
  2203. $this->ObjCache( $obj, $class, $args);
  2204. }
  2205. return $obj;
  2206. }
  2207. public function NewObj4($class, $args){
  2208. $obj = new $class( $args[0], $args[1], $args[2], $args[3] );
  2209. if( $this->CacheEnabled() ){
  2210. $this->ObjCache( $obj, $class, $args);
  2211. }
  2212. return $obj;
  2213. }
  2214. public function NewObj5($class, $args){
  2215. $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4] );
  2216. if( $this->CacheEnabled() ){
  2217. $this->ObjCache( $obj, $class, $args);
  2218. }
  2219. return $obj;
  2220. }
  2221. public function NewObj6($class, $args){
  2222. $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5] );
  2223. if( $this->CacheEnabled() ){
  2224. $this->ObjCache( $obj, $class, $args);
  2225. }
  2226. return $obj;
  2227. }
  2228. public function NewObj7($class, $args){
  2229. $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5], $args[6] );
  2230. if( $this->CacheEnabled() ){
  2231. $this->ObjCache( $obj, $class, $args);
  2232. }
  2233. return $obj;
  2234. }
  2235. //caching
  2236. public function ObjCache($obj, $class, $args=array()){
  2237. $obj->cache_string = ' new '.$class.'('. self::ArgCache($args).')';
  2238. }
  2239. public function ArgCache($args){
  2240. return implode(',',array_map( array('Less_Parser','ArgString'),$args));
  2241. }
  2242. /**
  2243. * Convert an argument to a string for use in the parser cache
  2244. *
  2245. * @return string
  2246. */
  2247. public static function ArgString($arg){
  2248. $type = gettype($arg);
  2249. if( $type === 'object'){
  2250. $string = $arg->cache_string;
  2251. unset($arg->cache_string);
  2252. return $string;
  2253. }elseif( $type === 'array' ){
  2254. $string = ' Array(';
  2255. foreach($arg as $k => $a){
  2256. $string .= var_export($k,true).' => '.self::ArgString($a).',';
  2257. }
  2258. return $string . ')';
  2259. }
  2260. return var_export($arg,true);
  2261. }
  2262. public function Error($msg){
  2263. throw new Less_Exception_Parser($msg, null, $this->furthest, $this->env->currentFileInfo);
  2264. }
  2265. public static function WinPath($path){
  2266. return str_replace('\\', '/', $path);
  2267. }
  2268. public static function AbsPath($path, $winPath = false){
  2269. if (strpos($path, '//') !== false && preg_match('_^(https?:)?//\\w+(\\.\\w+)+/\\w+_i', $path)) {
  2270. return $winPath ? '' : false;
  2271. } else {
  2272. $path = realpath($path);
  2273. if ($winPath) {
  2274. $path = self::WinPath($path);
  2275. }
  2276. return $path;
  2277. }
  2278. }
  2279. public function CacheEnabled(){
  2280. return (Less_Parser::$options['cache_method'] && (Less_Cache::$cache_dir || (Less_Parser::$options['cache_method'] == 'callback')));
  2281. }
  2282. }