Parser.php 66 KB

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