scss.inc_BAK_caching.php 109 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574
  1. <?php
  2. /**
  3. * SCSS compiler written in PHP
  4. *
  5. * @copyright 2012-2013 Leaf Corcoran
  6. *
  7. * @license http://opensource.org/licenses/gpl-license GPL-3.0
  8. * @license http://opensource.org/licenses/MIT MIT
  9. *
  10. * @link http://leafo.net/scssphp
  11. */
  12. /**
  13. * The scss compiler and parser.
  14. *
  15. * Converting SCSS to CSS is a three stage process. The incoming file is parsed
  16. * by `scss_parser` into a syntax tree, then it is compiled into another tree
  17. * representing the CSS structure by `scssc`. The CSS tree is fed into a
  18. * formatter, like `scss_formatter` which then outputs CSS as a string.
  19. *
  20. * During the first compile, all values are *reduced*, which means that their
  21. * types are brought to the lowest form before being dump as strings. This
  22. * handles math equations, variable dereferences, and the like.
  23. *
  24. * The `parse` function of `scssc` is the entry point.
  25. *
  26. * In summary:
  27. *
  28. * The `scssc` class creates an instance of the parser, feeds it SCSS code,
  29. * then transforms the resulting tree to a CSS tree. This class also holds the
  30. * evaluation context, such as all available mixins and variables at any given
  31. * time.
  32. *
  33. * The `scss_parser` class is only concerned with parsing its input.
  34. *
  35. * The `scss_formatter` takes a CSS tree, and dumps it to a formatted string,
  36. * handling things like indentation.
  37. */
  38. /**
  39. * SCSS compiler
  40. *
  41. * @author Leaf Corcoran <leafot@gmail.com>
  42. */
  43. class scssc {
  44. static public $VERSION = 'v0.0.12';
  45. static protected $operatorNames = array(
  46. '+' => "add",
  47. '-' => "sub",
  48. '*' => "mul",
  49. '/' => "div",
  50. '%' => "mod",
  51. '==' => "eq",
  52. '!=' => "neq",
  53. '<' => "lt",
  54. '>' => "gt",
  55. '<=' => "lte",
  56. '>=' => "gte",
  57. );
  58. static protected $namespaces = array(
  59. "special" => "%",
  60. "mixin" => "@",
  61. "function" => "^",
  62. );
  63. static protected $unitTable = array(
  64. "in" => array(
  65. "in" => 1,
  66. "pt" => 72,
  67. "pc" => 6,
  68. "cm" => 2.54,
  69. "mm" => 25.4,
  70. "px" => 96,
  71. )
  72. );
  73. static public $true = array("keyword", "true");
  74. static public $false = array("keyword", "false");
  75. static public $null = array("null");
  76. static public $defaultValue = array("keyword", "");
  77. static public $selfSelector = array("self");
  78. protected $importPaths = array("");
  79. protected $importCache = array();
  80. protected $userFunctions = array();
  81. protected $registeredVars = array();
  82. protected $numberPrecision = 5;
  83. protected $formatter = "scss_formatter_nested";
  84. /**
  85. * Compile scss
  86. *
  87. * @param string $code
  88. * @param string $name
  89. *
  90. * @return string
  91. */
  92. public function compile($code, $name = null)
  93. {
  94. $this->indentLevel = -1;
  95. $this->commentsSeen = array();
  96. $this->extends = array();
  97. $this->extendsMap = array();
  98. $this->parsedFiles = array();
  99. $this->env = null;
  100. $this->scope = null;
  101. $locale = setlocale(LC_NUMERIC, 0);
  102. setlocale(LC_NUMERIC, "C");
  103. $this->parser = new scss_parser($name);
  104. $tree = $this->parser->parse($code);
  105. $this->formatter = new $this->formatter();
  106. $this->pushEnv($tree);
  107. $this->injectVariables($this->registeredVars);
  108. $this->compileRoot($tree);
  109. $this->popEnv();
  110. $out = $this->formatter->format($this->scope);
  111. setlocale(LC_NUMERIC, $locale);
  112. return $out;
  113. }
  114. protected function isSelfExtend($target, $origin) {
  115. foreach ($origin as $sel) {
  116. if (in_array($target, $sel)) {
  117. return true;
  118. }
  119. }
  120. return false;
  121. }
  122. protected function pushExtends($target, $origin) {
  123. if ($this->isSelfExtend($target, $origin)) {
  124. return;
  125. }
  126. $i = count($this->extends);
  127. $this->extends[] = array($target, $origin);
  128. foreach ($target as $part) {
  129. if (isset($this->extendsMap[$part])) {
  130. $this->extendsMap[$part][] = $i;
  131. } else {
  132. $this->extendsMap[$part] = array($i);
  133. }
  134. }
  135. }
  136. protected function makeOutputBlock($type, $selectors = null) {
  137. $out = new stdClass;
  138. $out->type = $type;
  139. $out->lines = array();
  140. $out->children = array();
  141. $out->parent = $this->scope;
  142. $out->selectors = $selectors;
  143. $out->depth = $this->env->depth;
  144. return $out;
  145. }
  146. protected function matchExtendsSingle($single, &$outOrigin) {
  147. $counts = array();
  148. foreach ($single as $part) {
  149. if (!is_string($part)) return false; // hmm
  150. if (isset($this->extendsMap[$part])) {
  151. foreach ($this->extendsMap[$part] as $idx) {
  152. $counts[$idx] =
  153. isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
  154. }
  155. }
  156. }
  157. $outOrigin = array();
  158. $found = false;
  159. foreach ($counts as $idx => $count) {
  160. list($target, $origin) = $this->extends[$idx];
  161. // check count
  162. if ($count != count($target)) continue;
  163. // check if target is subset of single
  164. if (array_diff(array_intersect($single, $target), $target)) continue;
  165. $rem = array_diff($single, $target);
  166. foreach ($origin as $j => $new) {
  167. // prevent infinite loop when target extends itself
  168. foreach ($new as $new_selector) {
  169. if (!array_diff($single, $new_selector)) {
  170. continue 2;
  171. }
  172. }
  173. $origin[$j][count($origin[$j]) - 1] = $this->combineSelectorSingle(end($new), $rem);
  174. }
  175. $outOrigin = array_merge($outOrigin, $origin);
  176. $found = true;
  177. }
  178. return $found;
  179. }
  180. protected function combineSelectorSingle($base, $other) {
  181. $tag = null;
  182. $out = array();
  183. foreach (array($base, $other) as $single) {
  184. foreach ($single as $part) {
  185. if (preg_match('/^[^\[.#:]/', $part)) {
  186. $tag = $part;
  187. } else {
  188. $out[] = $part;
  189. }
  190. }
  191. }
  192. if ($tag) {
  193. array_unshift($out, $tag);
  194. }
  195. return $out;
  196. }
  197. protected function matchExtends($selector, &$out, $from = 0, $initial=true) {
  198. foreach ($selector as $i => $part) {
  199. if ($i < $from) continue;
  200. if ($this->matchExtendsSingle($part, $origin)) {
  201. $before = array_slice($selector, 0, $i);
  202. $after = array_slice($selector, $i + 1);
  203. foreach ($origin as $new) {
  204. $k = 0;
  205. // remove shared parts
  206. if ($initial) {
  207. foreach ($before as $k => $val) {
  208. if (!isset($new[$k]) || $val != $new[$k]) {
  209. break;
  210. }
  211. }
  212. }
  213. $result = array_merge(
  214. $before,
  215. $k > 0 ? array_slice($new, $k) : $new,
  216. $after);
  217. if ($result == $selector) continue;
  218. $out[] = $result;
  219. // recursively check for more matches
  220. $this->matchExtends($result, $out, $i, false);
  221. // selector sequence merging
  222. if (!empty($before) && count($new) > 1) {
  223. $result2 = array_merge(
  224. array_slice($new, 0, -1),
  225. $k > 0 ? array_slice($before, $k) : $before,
  226. array_slice($new, -1),
  227. $after);
  228. $out[] = $result2;
  229. }
  230. }
  231. }
  232. }
  233. }
  234. protected function flattenSelectors($block, $parentKey = null) {
  235. if ($block->selectors) {
  236. $selectors = array();
  237. foreach ($block->selectors as $s) {
  238. $selectors[] = $s;
  239. if (!is_array($s)) continue;
  240. // check extends
  241. if (!empty($this->extendsMap)) {
  242. $this->matchExtends($s, $selectors);
  243. }
  244. }
  245. $block->selectors = array();
  246. $placeholderSelector = false;
  247. foreach ($selectors as $selector) {
  248. if ($this->hasSelectorPlaceholder($selector)) {
  249. $placeholderSelector = true;
  250. continue;
  251. }
  252. $block->selectors[] = $this->compileSelector($selector);
  253. }
  254. if ($placeholderSelector && 0 == count($block->selectors) && null !== $parentKey) {
  255. unset($block->parent->children[$parentKey]);
  256. return;
  257. }
  258. }
  259. foreach ($block->children as $key => $child) {
  260. $this->flattenSelectors($child, $key);
  261. }
  262. }
  263. protected function compileRoot($rootBlock)
  264. {
  265. $this->scope = $this->makeOutputBlock('root');
  266. $this->compileChildren($rootBlock->children, $this->scope);
  267. $this->flattenSelectors($this->scope);
  268. }
  269. protected function compileMedia($media) {
  270. $this->pushEnv($media);
  271. $mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env));
  272. if (!empty($mediaQuery)) {
  273. $this->scope = $this->makeOutputBlock("media", array($mediaQuery));
  274. $parentScope = $this->mediaParent($this->scope);
  275. $parentScope->children[] = $this->scope;
  276. // top level properties in a media cause it to be wrapped
  277. $needsWrap = false;
  278. foreach ($media->children as $child) {
  279. $type = $child[0];
  280. if ($type !== 'block' && $type !== 'media' && $type !== 'directive') {
  281. $needsWrap = true;
  282. break;
  283. }
  284. }
  285. if ($needsWrap) {
  286. $wrapped = (object)array(
  287. "selectors" => array(),
  288. "children" => $media->children
  289. );
  290. $media->children = array(array("block", $wrapped));
  291. }
  292. $this->compileChildren($media->children, $this->scope);
  293. $this->scope = $this->scope->parent;
  294. }
  295. $this->popEnv();
  296. }
  297. protected function mediaParent($scope) {
  298. while (!empty($scope->parent)) {
  299. if (!empty($scope->type) && $scope->type != "media") {
  300. break;
  301. }
  302. $scope = $scope->parent;
  303. }
  304. return $scope;
  305. }
  306. // TODO refactor compileNestedBlock and compileMedia into same thing
  307. protected function compileNestedBlock($block, $selectors) {
  308. $this->pushEnv($block);
  309. $this->scope = $this->makeOutputBlock($block->type, $selectors);
  310. $this->scope->parent->children[] = $this->scope;
  311. $this->compileChildren($block->children, $this->scope);
  312. $this->scope = $this->scope->parent;
  313. $this->popEnv();
  314. }
  315. /**
  316. * Recursively compiles a block.
  317. *
  318. * A block is analogous to a CSS block in most cases. A single SCSS document
  319. * is encapsulated in a block when parsed, but it does not have parent tags
  320. * so all of its children appear on the root level when compiled.
  321. *
  322. * Blocks are made up of selectors and children.
  323. *
  324. * The children of a block are just all the blocks that are defined within.
  325. *
  326. * Compiling the block involves pushing a fresh environment on the stack,
  327. * and iterating through the props, compiling each one.
  328. *
  329. * @see scss::compileChild()
  330. *
  331. * @param \StdClass $block
  332. */
  333. protected function compileBlock($block) {
  334. $env = $this->pushEnv($block);
  335. $env->selectors =
  336. array_map(array($this, "evalSelector"), $block->selectors);
  337. $out = $this->makeOutputBlock(null, $this->multiplySelectors($env));
  338. $this->scope->children[] = $out;
  339. $this->compileChildren($block->children, $out);
  340. $this->popEnv();
  341. }
  342. // joins together .classes and #ids
  343. protected function flattenSelectorSingle($single) {
  344. $joined = array();
  345. foreach ($single as $part) {
  346. if (empty($joined) ||
  347. !is_string($part) ||
  348. preg_match('/[\[.:#%]/', $part))
  349. {
  350. $joined[] = $part;
  351. continue;
  352. }
  353. if (is_array(end($joined))) {
  354. $joined[] = $part;
  355. } else {
  356. $joined[count($joined) - 1] .= $part;
  357. }
  358. }
  359. return $joined;
  360. }
  361. // replaces all the interpolates
  362. protected function evalSelector($selector) {
  363. return array_map(array($this, "evalSelectorPart"), $selector);
  364. }
  365. protected function evalSelectorPart($piece) {
  366. foreach ($piece as &$p) {
  367. if (!is_array($p)) continue;
  368. switch ($p[0]) {
  369. case "interpolate":
  370. $p = $this->compileValue($p);
  371. break;
  372. case "string":
  373. $p = $this->compileValue($p);
  374. break;
  375. }
  376. }
  377. return $this->flattenSelectorSingle($piece);
  378. }
  379. // compiles to string
  380. // self(&) should have been replaced by now
  381. protected function compileSelector($selector) {
  382. if (!is_array($selector)) return $selector; // media and the like
  383. return implode(" ", array_map(
  384. array($this, "compileSelectorPart"), $selector));
  385. }
  386. protected function compileSelectorPart($piece) {
  387. foreach ($piece as &$p) {
  388. if (!is_array($p)) continue;
  389. switch ($p[0]) {
  390. case "self":
  391. $p = "&";
  392. break;
  393. default:
  394. $p = $this->compileValue($p);
  395. break;
  396. }
  397. }
  398. return implode($piece);
  399. }
  400. protected function hasSelectorPlaceholder($selector)
  401. {
  402. if (!is_array($selector)) return false;
  403. foreach ($selector as $parts) {
  404. foreach ($parts as $part) {
  405. if ('%' == $part[0]) {
  406. return true;
  407. }
  408. }
  409. }
  410. return false;
  411. }
  412. protected function compileChildren($stms, $out) {
  413. foreach ($stms as $stm) {
  414. $ret = $this->compileChild($stm, $out);
  415. if (isset($ret)) return $ret;
  416. }
  417. }
  418. protected function compileMediaQuery($queryList) {
  419. $out = "@media";
  420. $first = true;
  421. foreach ($queryList as $query){
  422. $type = null;
  423. $parts = array();
  424. foreach ($query as $q) {
  425. switch ($q[0]) {
  426. case "mediaType":
  427. if ($type) {
  428. $type = $this->mergeMediaTypes($type, array_map(array($this, "compileValue"), array_slice($q, 1)));
  429. if (empty($type)) { // merge failed
  430. return null;
  431. }
  432. } else {
  433. $type = array_map(array($this, "compileValue"), array_slice($q, 1));
  434. }
  435. break;
  436. case "mediaExp":
  437. if (isset($q[2])) {
  438. $parts[] = "(". $this->compileValue($q[1]) . $this->formatter->assignSeparator . $this->compileValue($q[2]) . ")";
  439. } else {
  440. $parts[] = "(" . $this->compileValue($q[1]) . ")";
  441. }
  442. break;
  443. }
  444. }
  445. if ($type) {
  446. array_unshift($parts, implode(' ', array_filter($type)));
  447. }
  448. if (!empty($parts)) {
  449. if ($first) {
  450. $first = false;
  451. $out .= " ";
  452. } else {
  453. $out .= $this->formatter->tagSeparator;
  454. }
  455. $out .= implode(" and ", $parts);
  456. }
  457. }
  458. return $out;
  459. }
  460. protected function mergeMediaTypes($type1, $type2) {
  461. if (empty($type1)) {
  462. return $type2;
  463. }
  464. if (empty($type2)) {
  465. return $type1;
  466. }
  467. $m1 = '';
  468. $t1 = '';
  469. if (count($type1) > 1) {
  470. $m1= strtolower($type1[0]);
  471. $t1= strtolower($type1[1]);
  472. } else {
  473. $t1 = strtolower($type1[0]);
  474. }
  475. $m2 = '';
  476. $t2 = '';
  477. if (count($type2) > 1) {
  478. $m2 = strtolower($type2[0]);
  479. $t2 = strtolower($type2[1]);
  480. } else {
  481. $t2 = strtolower($type2[0]);
  482. }
  483. if (($m1 == 'not') ^ ($m2 == 'not')) {
  484. if ($t1 == $t2) {
  485. return null;
  486. }
  487. return array(
  488. $m1 == 'not' ? $m2 : $m1,
  489. $m1 == 'not' ? $t2 : $t1
  490. );
  491. } elseif ($m1 == 'not' && $m2 == 'not') {
  492. # CSS has no way of representing "neither screen nor print"
  493. if ($t1 != $t2) {
  494. return null;
  495. }
  496. return array('not', $t1);
  497. } elseif ($t1 != $t2) {
  498. return null;
  499. } else { // t1 == t2, neither m1 nor m2 are "not"
  500. return array(empty($m1)? $m2 : $m1, $t1);
  501. }
  502. }
  503. // returns true if the value was something that could be imported
  504. protected function compileImport($rawPath, $out) {
  505. if ($rawPath[0] == "string") {
  506. $path = $this->compileStringContent($rawPath);
  507. if ($path = $this->findImport($path)) {
  508. $this->importFile($path, $out);
  509. return true;
  510. }
  511. return false;
  512. }
  513. if ($rawPath[0] == "list") {
  514. // handle a list of strings
  515. if (count($rawPath[2]) == 0) return false;
  516. foreach ($rawPath[2] as $path) {
  517. if ($path[0] != "string") return false;
  518. }
  519. foreach ($rawPath[2] as $path) {
  520. $this->compileImport($path, $out);
  521. }
  522. return true;
  523. }
  524. return false;
  525. }
  526. // return a value to halt execution
  527. protected function compileChild($child, $out) {
  528. $this->sourcePos = isset($child[-1]) ? $child[-1] : -1;
  529. $this->sourceParser = isset($child[-2]) ? $child[-2] : $this->parser;
  530. switch ($child[0]) {
  531. case "import":
  532. list(,$rawPath) = $child;
  533. $rawPath = $this->reduce($rawPath);
  534. if (!$this->compileImport($rawPath, $out)) {
  535. $out->lines[] = "@import " . $this->compileValue($rawPath) . ";";
  536. }
  537. break;
  538. case "directive":
  539. list(, $directive) = $child;
  540. $s = "@" . $directive->name;
  541. if (!empty($directive->value)) {
  542. $s .= " " . $this->compileValue($directive->value);
  543. }
  544. $this->compileNestedBlock($directive, array($s));
  545. break;
  546. case "media":
  547. $this->compileMedia($child[1]);
  548. break;
  549. case "block":
  550. $this->compileBlock($child[1]);
  551. break;
  552. case "charset":
  553. $out->lines[] = "@charset ".$this->compileValue($child[1]).";";
  554. break;
  555. case "assign":
  556. list(,$name, $value) = $child;
  557. if ($name[0] == "var") {
  558. $isDefault = !empty($child[3]);
  559. if ($isDefault) {
  560. $existingValue = $this->get($name[1], true);
  561. $shouldSet = $existingValue === true || $existingValue == self::$null;
  562. }
  563. if (!$isDefault || $shouldSet) {
  564. $this->set($name[1], $this->reduce($value));
  565. }
  566. break;
  567. }
  568. // if the value reduces to null from something else then
  569. // the property should be discarded
  570. if ($value[0] != "null") {
  571. $value = $this->reduce($value);
  572. if ($value[0] == "null") {
  573. break;
  574. }
  575. }
  576. $compiledValue = $this->compileValue($value);
  577. $out->lines[] = $this->formatter->property(
  578. $this->compileValue($name),
  579. $compiledValue);
  580. break;
  581. case "comment":
  582. $out->lines[] = $child[1];
  583. break;
  584. case "mixin":
  585. case "function":
  586. list(,$block) = $child;
  587. $this->set(self::$namespaces[$block->type] . $block->name, $block);
  588. break;
  589. case "extend":
  590. list(, $selectors) = $child;
  591. foreach ($selectors as $sel) {
  592. // only use the first one
  593. $sel = current($this->evalSelector($sel));
  594. $this->pushExtends($sel, $out->selectors);
  595. }
  596. break;
  597. case "if":
  598. list(, $if) = $child;
  599. if ($this->isTruthy($this->reduce($if->cond, true))) {
  600. return $this->compileChildren($if->children, $out);
  601. } else {
  602. foreach ($if->cases as $case) {
  603. if ($case->type == "else" ||
  604. $case->type == "elseif" && $this->isTruthy($this->reduce($case->cond)))
  605. {
  606. return $this->compileChildren($case->children, $out);
  607. }
  608. }
  609. }
  610. break;
  611. case "return":
  612. return $this->reduce($child[1], true);
  613. case "each":
  614. list(,$each) = $child;
  615. $list = $this->coerceList($this->reduce($each->list));
  616. foreach ($list[2] as $item) {
  617. $this->pushEnv();
  618. $this->set($each->var, $item);
  619. // TODO: allow return from here
  620. $this->compileChildren($each->children, $out);
  621. $this->popEnv();
  622. }
  623. break;
  624. case "while":
  625. list(,$while) = $child;
  626. while ($this->isTruthy($this->reduce($while->cond, true))) {
  627. $ret = $this->compileChildren($while->children, $out);
  628. if ($ret) return $ret;
  629. }
  630. break;
  631. case "for":
  632. list(,$for) = $child;
  633. $start = $this->reduce($for->start, true);
  634. $start = $start[1];
  635. $end = $this->reduce($for->end, true);
  636. $end = $end[1];
  637. $d = $start < $end ? 1 : -1;
  638. while (true) {
  639. if ((!$for->until && $start - $d == $end) ||
  640. ($for->until && $start == $end))
  641. {
  642. break;
  643. }
  644. $this->set($for->var, array("number", $start, ""));
  645. $start += $d;
  646. $ret = $this->compileChildren($for->children, $out);
  647. if ($ret) return $ret;
  648. }
  649. break;
  650. case "nestedprop":
  651. list(,$prop) = $child;
  652. $prefixed = array();
  653. $prefix = $this->compileValue($prop->prefix) . "-";
  654. foreach ($prop->children as $child) {
  655. if ($child[0] == "assign") {
  656. array_unshift($child[1][2], $prefix);
  657. }
  658. if ($child[0] == "nestedprop") {
  659. array_unshift($child[1]->prefix[2], $prefix);
  660. }
  661. $prefixed[] = $child;
  662. }
  663. $this->compileChildren($prefixed, $out);
  664. break;
  665. case "include": // including a mixin
  666. list(,$name, $argValues, $content) = $child;
  667. $mixin = $this->get(self::$namespaces["mixin"] . $name, false);
  668. if (!$mixin) {
  669. $this->throwError("Undefined mixin $name");
  670. }
  671. $callingScope = $this->env;
  672. // push scope, apply args
  673. $this->pushEnv();
  674. if ($this->env->depth > 0) {
  675. $this->env->depth--;
  676. }
  677. if (isset($content)) {
  678. $content->scope = $callingScope;
  679. $this->setRaw(self::$namespaces["special"] . "content", $content);
  680. }
  681. if (isset($mixin->args)) {
  682. $this->applyArguments($mixin->args, $argValues);
  683. }
  684. foreach ($mixin->children as $child) {
  685. $this->compileChild($child, $out);
  686. }
  687. $this->popEnv();
  688. break;
  689. case "mixin_content":
  690. $content = $this->get(self::$namespaces["special"] . "content");
  691. if (!isset($content)) {
  692. $this->throwError("Expected @content inside of mixin");
  693. }
  694. $strongTypes = array('include', 'block', 'for', 'while');
  695. foreach ($content->children as $child) {
  696. $this->storeEnv = (in_array($child[0], $strongTypes))
  697. ? null
  698. : $content->scope;
  699. $this->compileChild($child, $out);
  700. }
  701. unset($this->storeEnv);
  702. break;
  703. case "debug":
  704. list(,$value, $pos) = $child;
  705. $line = $this->parser->getLineNo($pos);
  706. $value = $this->compileValue($this->reduce($value, true));
  707. fwrite(STDERR, "Line $line DEBUG: $value\n");
  708. break;
  709. default:
  710. $this->throwError("unknown child type: $child[0]");
  711. }
  712. }
  713. protected function expToString($exp) {
  714. list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
  715. $content = array($this->reduce($left));
  716. if ($whiteLeft) $content[] = " ";
  717. $content[] = $op;
  718. if ($whiteRight) $content[] = " ";
  719. $content[] = $this->reduce($right);
  720. return array("string", "", $content);
  721. }
  722. protected function isTruthy($value) {
  723. return $value != self::$false && $value != self::$null;
  724. }
  725. // should $value cause its operand to eval
  726. protected function shouldEval($value) {
  727. switch ($value[0]) {
  728. case "exp":
  729. if ($value[1] == "/") {
  730. return $this->shouldEval($value[2], $value[3]);
  731. }
  732. case "var":
  733. case "fncall":
  734. return true;
  735. }
  736. return false;
  737. }
  738. protected function reduce($value, $inExp = false) {
  739. list($type) = $value;
  740. switch ($type) {
  741. case "exp":
  742. list(, $op, $left, $right, $inParens) = $value;
  743. $opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op;
  744. $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
  745. $left = $this->reduce($left, true);
  746. $right = $this->reduce($right, true);
  747. // only do division in special cases
  748. if ($opName == "div" && !$inParens && !$inExp) {
  749. if ($left[0] != "color" && $right[0] != "color") {
  750. return $this->expToString($value);
  751. }
  752. }
  753. $left = $this->coerceForExpression($left);
  754. $right = $this->coerceForExpression($right);
  755. $ltype = $left[0];
  756. $rtype = $right[0];
  757. // this tries:
  758. // 1. op_[op name]_[left type]_[right type]
  759. // 2. op_[left type]_[right type] (passing the op as first arg
  760. // 3. op_[op name]
  761. $fn = "op_${opName}_${ltype}_${rtype}";
  762. if (is_callable(array($this, $fn)) ||
  763. (($fn = "op_${ltype}_${rtype}") &&
  764. is_callable(array($this, $fn)) &&
  765. $passOp = true) ||
  766. (($fn = "op_${opName}") &&
  767. is_callable(array($this, $fn)) &&
  768. $genOp = true))
  769. {
  770. $unitChange = false;
  771. if (!isset($genOp) &&
  772. $left[0] == "number" && $right[0] == "number")
  773. {
  774. if ($opName == "mod" && $right[2] != "") {
  775. $this->throwError("Cannot modulo by a number with units: $right[1]$right[2].");
  776. }
  777. $unitChange = true;
  778. $emptyUnit = $left[2] == "" || $right[2] == "";
  779. $targetUnit = "" != $left[2] ? $left[2] : $right[2];
  780. if ($opName != "mul") {
  781. $left[2] = "" != $left[2] ? $left[2] : $targetUnit;
  782. $right[2] = "" != $right[2] ? $right[2] : $targetUnit;
  783. }
  784. if ($opName != "mod") {
  785. $left = $this->normalizeNumber($left);
  786. $right = $this->normalizeNumber($right);
  787. }
  788. if ($opName == "div" && !$emptyUnit && $left[2] == $right[2]) {
  789. $targetUnit = "";
  790. }
  791. if ($opName == "mul") {
  792. $left[2] = "" != $left[2] ? $left[2] : $right[2];
  793. $right[2] = "" != $right[2] ? $right[2] : $left[2];
  794. } elseif ($opName == "div" && $left[2] == $right[2]) {
  795. $left[2] = "";
  796. $right[2] = "";
  797. }
  798. }
  799. $shouldEval = $inParens || $inExp;
  800. if (isset($passOp)) {
  801. $out = $this->$fn($op, $left, $right, $shouldEval);
  802. } else {
  803. $out = $this->$fn($left, $right, $shouldEval);
  804. }
  805. if (isset($out)) {
  806. if ($unitChange && $out[0] == "number") {
  807. $out = $this->coerceUnit($out, $targetUnit);
  808. }
  809. return $out;
  810. }
  811. }
  812. return $this->expToString($value);
  813. case "unary":
  814. list(, $op, $exp, $inParens) = $value;
  815. $inExp = $inExp || $this->shouldEval($exp);
  816. $exp = $this->reduce($exp);
  817. if ($exp[0] == "number") {
  818. switch ($op) {
  819. case "+":
  820. return $exp;
  821. case "-":
  822. $exp[1] *= -1;
  823. return $exp;
  824. }
  825. }
  826. if ($op == "not") {
  827. if ($inExp || $inParens) {
  828. if ($exp == self::$false) {
  829. return self::$true;
  830. } else {
  831. return self::$false;
  832. }
  833. } else {
  834. $op = $op . " ";
  835. }
  836. }
  837. return array("string", "", array($op, $exp));
  838. case "var":
  839. list(, $name) = $value;
  840. return $this->reduce($this->get($name));
  841. case "list":
  842. foreach ($value[2] as &$item) {
  843. $item = $this->reduce($item);
  844. }
  845. return $value;
  846. case "string":
  847. foreach ($value[2] as &$item) {
  848. if (is_array($item)) {
  849. $item = $this->reduce($item);
  850. }
  851. }
  852. return $value;
  853. case "interpolate":
  854. $value[1] = $this->reduce($value[1]);
  855. return $value;
  856. case "fncall":
  857. list(,$name, $argValues) = $value;
  858. // user defined function?
  859. $func = $this->get(self::$namespaces["function"] . $name, false);
  860. if ($func) {
  861. $this->pushEnv();
  862. // set the args
  863. if (isset($func->args)) {
  864. $this->applyArguments($func->args, $argValues);
  865. }
  866. // throw away lines and children
  867. $tmp = (object)array(
  868. "lines" => array(),
  869. "children" => array()
  870. );
  871. $ret = $this->compileChildren($func->children, $tmp);
  872. $this->popEnv();
  873. return !isset($ret) ? self::$defaultValue : $ret;
  874. }
  875. // built in function
  876. if ($this->callBuiltin($name, $argValues, $returnValue)) {
  877. return $returnValue;
  878. }
  879. // need to flatten the arguments into a list
  880. $listArgs = array();
  881. foreach ((array)$argValues as $arg) {
  882. if (empty($arg[0])) {
  883. $listArgs[] = $this->reduce($arg[1]);
  884. }
  885. }
  886. return array("function", $name, array("list", ",", $listArgs));
  887. default:
  888. return $value;
  889. }
  890. }
  891. public function normalizeValue($value) {
  892. $value = $this->coerceForExpression($this->reduce($value));
  893. list($type) = $value;
  894. switch ($type) {
  895. case "list":
  896. $value = $this->extractInterpolation($value);
  897. if ($value[0] != "list") {
  898. return array("keyword", $this->compileValue($value));
  899. }
  900. foreach ($value[2] as $key => $item) {
  901. $value[2][$key] = $this->normalizeValue($item);
  902. }
  903. return $value;
  904. case "number":
  905. return $this->normalizeNumber($value);
  906. default:
  907. return $value;
  908. }
  909. }
  910. // just does physical lengths for now
  911. protected function normalizeNumber($number) {
  912. list(, $value, $unit) = $number;
  913. if (isset(self::$unitTable["in"][$unit])) {
  914. $conv = self::$unitTable["in"][$unit];
  915. return array("number", $value / $conv, "in");
  916. }
  917. return $number;
  918. }
  919. // $number should be normalized
  920. protected function coerceUnit($number, $unit) {
  921. list(, $value, $baseUnit) = $number;
  922. if (isset(self::$unitTable[$baseUnit][$unit])) {
  923. $value = $value * self::$unitTable[$baseUnit][$unit];
  924. }
  925. return array("number", $value, $unit);
  926. }
  927. protected function op_add_number_number($left, $right) {
  928. return array("number", $left[1] + $right[1], $left[2]);
  929. }
  930. protected function op_mul_number_number($left, $right) {
  931. return array("number", $left[1] * $right[1], $left[2]);
  932. }
  933. protected function op_sub_number_number($left, $right) {
  934. return array("number", $left[1] - $right[1], $left[2]);
  935. }
  936. protected function op_div_number_number($left, $right) {
  937. return array("number", $left[1] / $right[1], $left[2]);
  938. }
  939. protected function op_mod_number_number($left, $right) {
  940. return array("number", $left[1] % $right[1], $left[2]);
  941. }
  942. // adding strings
  943. protected function op_add($left, $right) {
  944. if ($strLeft = $this->coerceString($left)) {
  945. if ($right[0] == "string") {
  946. $right[1] = "";
  947. }
  948. $strLeft[2][] = $right;
  949. return $strLeft;
  950. }
  951. if ($strRight = $this->coerceString($right)) {
  952. if ($left[0] == "string") {
  953. $left[1] = "";
  954. }
  955. array_unshift($strRight[2], $left);
  956. return $strRight;
  957. }
  958. }
  959. protected function op_and($left, $right, $shouldEval) {
  960. if (!$shouldEval) return;
  961. if ($left != self::$false) return $right;
  962. return $left;
  963. }
  964. protected function op_or($left, $right, $shouldEval) {
  965. if (!$shouldEval) return;
  966. if ($left != self::$false) return $left;
  967. return $right;
  968. }
  969. protected function op_color_color($op, $left, $right) {
  970. $out = array('color');
  971. foreach (range(1, 3) as $i) {
  972. $lval = isset($left[$i]) ? $left[$i] : 0;
  973. $rval = isset($right[$i]) ? $right[$i] : 0;
  974. switch ($op) {
  975. case '+':
  976. $out[] = $lval + $rval;
  977. break;
  978. case '-':
  979. $out[] = $lval - $rval;
  980. break;
  981. case '*':
  982. $out[] = $lval * $rval;
  983. break;
  984. case '%':
  985. $out[] = $lval % $rval;
  986. break;
  987. case '/':
  988. if ($rval == 0) {
  989. $this->throwError("color: Can't divide by zero");
  990. }
  991. $out[] = $lval / $rval;
  992. break;
  993. case "==":
  994. return $this->op_eq($left, $right);
  995. case "!=":
  996. return $this->op_neq($left, $right);
  997. default:
  998. $this->throwError("color: unknown op $op");
  999. }
  1000. }
  1001. if (isset($left[4])) $out[4] = $left[4];
  1002. elseif (isset($right[4])) $out[4] = $right[4];
  1003. return $this->fixColor($out);
  1004. }
  1005. protected function op_color_number($op, $left, $right) {
  1006. $value = $right[1];
  1007. return $this->op_color_color($op, $left,
  1008. array("color", $value, $value, $value));
  1009. }
  1010. protected function op_number_color($op, $left, $right) {
  1011. $value = $left[1];
  1012. return $this->op_color_color($op,
  1013. array("color", $value, $value, $value), $right);
  1014. }
  1015. protected function op_eq($left, $right) {
  1016. if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
  1017. $lStr[1] = "";
  1018. $rStr[1] = "";
  1019. return $this->toBool($this->compileValue($lStr) == $this->compileValue($rStr));
  1020. }
  1021. return $this->toBool($left == $right);
  1022. }
  1023. protected function op_neq($left, $right) {
  1024. return $this->toBool($left != $right);
  1025. }
  1026. protected function op_gte_number_number($left, $right) {
  1027. return $this->toBool($left[1] >= $right[1]);
  1028. }
  1029. protected function op_gt_number_number($left, $right) {
  1030. return $this->toBool($left[1] > $right[1]);
  1031. }
  1032. protected function op_lte_number_number($left, $right) {
  1033. return $this->toBool($left[1] <= $right[1]);
  1034. }
  1035. protected function op_lt_number_number($left, $right) {
  1036. return $this->toBool($left[1] < $right[1]);
  1037. }
  1038. public function toBool($thing) {
  1039. return $thing ? self::$true : self::$false;
  1040. }
  1041. /**
  1042. * Compiles a primitive value into a CSS property value.
  1043. *
  1044. * Values in scssphp are typed by being wrapped in arrays, their format is
  1045. * typically:
  1046. *
  1047. * array(type, contents [, additional_contents]*)
  1048. *
  1049. * The input is expected to be reduced. This function will not work on
  1050. * things like expressions and variables.
  1051. *
  1052. * @param array $value
  1053. */
  1054. protected function compileValue($value) {
  1055. $value = $this->reduce($value);
  1056. list($type) = $value;
  1057. switch ($type) {
  1058. case "keyword":
  1059. return $value[1];
  1060. case "color":
  1061. // [1] - red component (either number for a %)
  1062. // [2] - green component
  1063. // [3] - blue component
  1064. // [4] - optional alpha component
  1065. list(, $r, $g, $b) = $value;
  1066. $r = round($r);
  1067. $g = round($g);
  1068. $b = round($b);
  1069. if (count($value) == 5 && $value[4] != 1) { // rgba
  1070. return 'rgba('.$r.', '.$g.', '.$b.', '.$value[4].')';
  1071. }
  1072. $h = sprintf("#%02x%02x%02x", $r, $g, $b);
  1073. // Converting hex color to short notation (e.g. #003399 to #039)
  1074. if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
  1075. $h = '#' . $h[1] . $h[3] . $h[5];
  1076. }
  1077. return $h;
  1078. case "number":
  1079. return round($value[1], $this->numberPrecision) . $value[2];
  1080. case "string":
  1081. return $value[1] . $this->compileStringContent($value) . $value[1];
  1082. case "function":
  1083. $args = !empty($value[2]) ? $this->compileValue($value[2]) : "";
  1084. return "$value[1]($args)";
  1085. case "list":
  1086. $value = $this->extractInterpolation($value);
  1087. if ($value[0] != "list") return $this->compileValue($value);
  1088. list(, $delim, $items) = $value;
  1089. $filtered = array();
  1090. foreach ($items as $item) {
  1091. if ($item[0] == "null") continue;
  1092. $filtered[] = $this->compileValue($item);
  1093. }
  1094. return implode("$delim ", $filtered);
  1095. case "interpolated": # node created by extractInterpolation
  1096. list(, $interpolate, $left, $right) = $value;
  1097. list(,, $whiteLeft, $whiteRight) = $interpolate;
  1098. $left = count($left[2]) > 0 ?
  1099. $this->compileValue($left).$whiteLeft : "";
  1100. $right = count($right[2]) > 0 ?
  1101. $whiteRight.$this->compileValue($right) : "";
  1102. return $left.$this->compileValue($interpolate).$right;
  1103. case "interpolate": # raw parse node
  1104. list(, $exp) = $value;
  1105. // strip quotes if it's a string
  1106. $reduced = $this->reduce($exp);
  1107. switch ($reduced[0]) {
  1108. case "string":
  1109. $reduced = array("keyword",
  1110. $this->compileStringContent($reduced));
  1111. break;
  1112. case "null":
  1113. $reduced = array("keyword", "");
  1114. }
  1115. return $this->compileValue($reduced);
  1116. case "null":
  1117. return "null";
  1118. default:
  1119. $this->throwError("unknown value type: $type");
  1120. }
  1121. }
  1122. protected function compileStringContent($string) {
  1123. $parts = array();
  1124. foreach ($string[2] as $part) {
  1125. if (is_array($part)) {
  1126. $parts[] = $this->compileValue($part);
  1127. } else {
  1128. $parts[] = $part;
  1129. }
  1130. }
  1131. return implode($parts);
  1132. }
  1133. // doesn't need to be recursive, compileValue will handle that
  1134. protected function extractInterpolation($list) {
  1135. $items = $list[2];
  1136. foreach ($items as $i => $item) {
  1137. if ($item[0] == "interpolate") {
  1138. $before = array("list", $list[1], array_slice($items, 0, $i));
  1139. $after = array("list", $list[1], array_slice($items, $i + 1));
  1140. return array("interpolated", $item, $before, $after);
  1141. }
  1142. }
  1143. return $list;
  1144. }
  1145. // find the final set of selectors
  1146. protected function multiplySelectors($env) {
  1147. $envs = array();
  1148. while (null !== $env) {
  1149. if (!empty($env->selectors)) {
  1150. $envs[] = $env;
  1151. }
  1152. $env = $env->parent;
  1153. };
  1154. $selectors = array();
  1155. $parentSelectors = array(array());
  1156. while ($env = array_pop($envs)) {
  1157. $selectors = array();
  1158. foreach ($env->selectors as $selector) {
  1159. foreach ($parentSelectors as $parent) {
  1160. $selectors[] = $this->joinSelectors($parent, $selector);
  1161. }
  1162. }
  1163. $parentSelectors = $selectors;
  1164. }
  1165. return $selectors;
  1166. }
  1167. // looks for & to replace, or append parent before child
  1168. protected function joinSelectors($parent, $child) {
  1169. $setSelf = false;
  1170. $out = array();
  1171. foreach ($child as $part) {
  1172. $newPart = array();
  1173. foreach ($part as $p) {
  1174. if ($p == self::$selfSelector) {
  1175. $setSelf = true;
  1176. foreach ($parent as $i => $parentPart) {
  1177. if ($i > 0) {
  1178. $out[] = $newPart;
  1179. $newPart = array();
  1180. }
  1181. foreach ($parentPart as $pp) {
  1182. $newPart[] = $pp;
  1183. }
  1184. }
  1185. } else {
  1186. $newPart[] = $p;
  1187. }
  1188. }
  1189. $out[] = $newPart;
  1190. }
  1191. return $setSelf ? $out : array_merge($parent, $child);
  1192. }
  1193. protected function multiplyMedia($env, $childQueries = null) {
  1194. if (!isset($env) ||
  1195. !empty($env->block->type) && $env->block->type != "media")
  1196. {
  1197. return $childQueries;
  1198. }
  1199. // plain old block, skip
  1200. if (empty($env->block->type)) {
  1201. return $this->multiplyMedia($env->parent, $childQueries);
  1202. }
  1203. $parentQueries = $env->block->queryList;
  1204. if ($childQueries == null) {
  1205. $childQueries = $parentQueries;
  1206. } else {
  1207. $originalQueries = $childQueries;
  1208. $childQueries = array();
  1209. foreach ($parentQueries as $parentQuery){
  1210. foreach ($originalQueries as $childQuery) {
  1211. $childQueries []= array_merge($parentQuery, $childQuery);
  1212. }
  1213. }
  1214. }
  1215. return $this->multiplyMedia($env->parent, $childQueries);
  1216. }
  1217. // convert something to list
  1218. protected function coerceList($item, $delim = ",") {
  1219. if (isset($item) && $item[0] == "list") {
  1220. return $item;
  1221. }
  1222. return array("list", $delim, !isset($item) ? array(): array($item));
  1223. }
  1224. protected function applyArguments($argDef, $argValues) {
  1225. $hasVariable = false;
  1226. $args = array();
  1227. foreach ($argDef as $i => $arg) {
  1228. list($name, $default, $isVariable) = $argDef[$i];
  1229. $args[$name] = array($i, $name, $default, $isVariable);
  1230. $hasVariable |= $isVariable;
  1231. }
  1232. $keywordArgs = array();
  1233. $deferredKeywordArgs = array();
  1234. $remaining = array();
  1235. // assign the keyword args
  1236. foreach ((array) $argValues as $arg) {
  1237. if (!empty($arg[0])) {
  1238. if (!isset($args[$arg[0][1]])) {
  1239. if ($hasVariable) {
  1240. $deferredKeywordArgs[$arg[0][1]] = $arg[1];
  1241. } else {
  1242. $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
  1243. }
  1244. } elseif ($args[$arg[0][1]][0] < count($remaining)) {
  1245. $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
  1246. } else {
  1247. $keywordArgs[$arg[0][1]] = $arg[1];
  1248. }
  1249. } elseif (count($keywordArgs)) {
  1250. $this->throwError('Positional arguments must come before keyword arguments.');
  1251. } elseif ($arg[2] == true) {
  1252. $val = $this->reduce($arg[1], true);
  1253. if ($val[0] == "list") {
  1254. foreach ($val[2] as $name => $item) {
  1255. if (!is_numeric($name)) {
  1256. $keywordArgs[$name] = $item;
  1257. } else {
  1258. $remaining[] = $item;
  1259. }
  1260. }
  1261. } else {
  1262. $remaining[] = $val;
  1263. }
  1264. } else {
  1265. $remaining[] = $arg[1];
  1266. }
  1267. }
  1268. foreach ($args as $arg) {
  1269. list($i, $name, $default, $isVariable) = $arg;
  1270. if ($isVariable) {
  1271. $val = array("list", ",", array());
  1272. for ($count = count($remaining); $i < $count; $i++) {
  1273. $val[2][] = $remaining[$i];
  1274. }
  1275. foreach ($deferredKeywordArgs as $itemName => $item) {
  1276. $val[2][$itemName] = $item;
  1277. }
  1278. } elseif (isset($remaining[$i])) {
  1279. $val = $remaining[$i];
  1280. } elseif (isset($keywordArgs[$name])) {
  1281. $val = $keywordArgs[$name];
  1282. } elseif (!empty($default)) {
  1283. $val = $default;
  1284. } else {
  1285. $this->throwError("Missing argument $name");
  1286. }
  1287. $this->set($name, $this->reduce($val, true), true);
  1288. }
  1289. }
  1290. protected function pushEnv($block=null) {
  1291. $env = new stdClass;
  1292. $env->parent = $this->env;
  1293. $env->store = array();
  1294. $env->block = $block;
  1295. $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
  1296. $this->env = $env;
  1297. return $env;
  1298. }
  1299. protected function normalizeName($name) {
  1300. return str_replace("-", "_", $name);
  1301. }
  1302. protected function getStoreEnv() {
  1303. return isset($this->storeEnv) ? $this->storeEnv : $this->env;
  1304. }
  1305. protected function set($name, $value, $shadow=false) {
  1306. $name = $this->normalizeName($name);
  1307. if ($shadow) {
  1308. $this->setRaw($name, $value);
  1309. } else {
  1310. $this->setExisting($name, $value);
  1311. }
  1312. }
  1313. protected function setExisting($name, $value, $env = null) {
  1314. if (!isset($env)) $env = $this->getStoreEnv();
  1315. if (isset($env->store[$name]) || !isset($env->parent)) {
  1316. $env->store[$name] = $value;
  1317. } else {
  1318. $this->setExisting($name, $value, $env->parent);
  1319. }
  1320. }
  1321. protected function setRaw($name, $value) {
  1322. $env = $this->getStoreEnv();
  1323. $env->store[$name] = $value;
  1324. }
  1325. public function get($name, $defaultValue = null, $env = null) {
  1326. $name = $this->normalizeName($name);
  1327. if (!isset($env)) $env = $this->getStoreEnv();
  1328. if (!isset($defaultValue)) $defaultValue = self::$defaultValue;
  1329. if (isset($env->store[$name])) {
  1330. return $env->store[$name];
  1331. } elseif (isset($env->parent)) {
  1332. return $this->get($name, $defaultValue, $env->parent);
  1333. }
  1334. return $defaultValue; // found nothing
  1335. }
  1336. protected function injectVariables(array $args)
  1337. {
  1338. if (empty($args)) {
  1339. return;
  1340. }
  1341. $parser = new scss_parser(__METHOD__, false);
  1342. foreach ($args as $name => $strValue) {
  1343. if ($name[0] === '$') {
  1344. $name = substr($name, 1);
  1345. }
  1346. $parser->env = null;
  1347. $parser->count = 0;
  1348. $parser->buffer = (string) $strValue;
  1349. $parser->inParens = false;
  1350. $parser->eatWhiteDefault = true;
  1351. $parser->insertComments = true;
  1352. if ( ! $parser->valueList($value)) {
  1353. throw new Exception("failed to parse passed in variable $name: $strValue");
  1354. }
  1355. $this->set($name, $value);
  1356. }
  1357. }
  1358. /**
  1359. * Set variables
  1360. *
  1361. * @param array $variables
  1362. */
  1363. public function setVariables(array $variables)
  1364. {
  1365. $this->registeredVars = array_merge($this->registeredVars, $variables);
  1366. }
  1367. /**
  1368. * Unset variable
  1369. *
  1370. * @param string $name
  1371. */
  1372. public function unsetVariable($name)
  1373. {
  1374. unset($this->registeredVars[$name]);
  1375. }
  1376. protected function popEnv() {
  1377. $env = $this->env;
  1378. $this->env = $this->env->parent;
  1379. return $env;
  1380. }
  1381. public function getParsedFiles() {
  1382. return $this->parsedFiles;
  1383. }
  1384. public function addImportPath($path) {
  1385. $this->importPaths[] = $path;
  1386. }
  1387. public function setImportPaths($path) {
  1388. $this->importPaths = (array)$path;
  1389. }
  1390. public function setNumberPrecision($numberPrecision) {
  1391. $this->numberPrecision = $numberPrecision;
  1392. }
  1393. public function setFormatter($formatterName) {
  1394. $this->formatter = $formatterName;
  1395. }
  1396. public function registerFunction($name, $func) {
  1397. $this->userFunctions[$this->normalizeName($name)] = $func;
  1398. }
  1399. public function unregisterFunction($name) {
  1400. unset($this->userFunctions[$this->normalizeName($name)]);
  1401. }
  1402. protected function importFile($path, $out) {
  1403. // see if tree is cached
  1404. $realPath = realpath($path);
  1405. if (isset($this->importCache[$realPath])) {
  1406. $tree = $this->importCache[$realPath];
  1407. } else {
  1408. $code = file_get_contents($path);
  1409. $parser = new scss_parser($path, false);
  1410. $tree = $parser->parse($code);
  1411. $this->parsedFiles[] = $path;
  1412. $this->importCache[$realPath] = $tree;
  1413. }
  1414. $pi = pathinfo($path);
  1415. array_unshift($this->importPaths, $pi['dirname']);
  1416. $this->compileChildren($tree->children, $out);
  1417. array_shift($this->importPaths);
  1418. }
  1419. // results the file path for an import url if it exists
  1420. public function findImport($url) {
  1421. $urls = array();
  1422. // for "normal" scss imports (ignore vanilla css and external requests)
  1423. if (!preg_match('/\.css|^http:\/\/$/', $url)) {
  1424. // try both normal and the _partial filename
  1425. $urls = array($url, preg_replace('/[^\/]+$/', '_\0', $url));
  1426. }
  1427. foreach ($this->importPaths as $dir) {
  1428. if (is_string($dir)) {
  1429. // check urls for normal import paths
  1430. foreach ($urls as $full) {
  1431. $full = $dir .
  1432. (!empty($dir) && substr($dir, -1) != '/' ? '/' : '') .
  1433. $full;
  1434. if ($this->fileExists($file = $full.'.scss') ||
  1435. $this->fileExists($file = $full))
  1436. {
  1437. return $file;
  1438. }
  1439. }
  1440. } else {
  1441. // check custom callback for import path
  1442. $file = call_user_func($dir,$url,$this);
  1443. if ($file !== null) {
  1444. return $file;
  1445. }
  1446. }
  1447. }
  1448. return null;
  1449. }
  1450. protected function fileExists($name) {
  1451. return is_file($name);
  1452. }
  1453. protected function callBuiltin($name, $args, &$returnValue) {
  1454. // try a lib function
  1455. $name = $this->normalizeName($name);
  1456. $libName = "lib_".$name;
  1457. $f = array($this, $libName);
  1458. if (is_callable($f)) {
  1459. $prototype = isset(self::$$libName) ? self::$$libName : null;
  1460. $sorted = $this->sortArgs($prototype, $args);
  1461. foreach ($sorted as &$val) {
  1462. $val = $this->reduce($val, true);
  1463. }
  1464. $returnValue = call_user_func($f, $sorted, $this);
  1465. } elseif (isset($this->userFunctions[$name])) {
  1466. // see if we can find a user function
  1467. $fn = $this->userFunctions[$name];
  1468. foreach ($args as &$val) {
  1469. $val = $this->reduce($val[1], true);
  1470. }
  1471. $returnValue = call_user_func($fn, $args, $this);
  1472. }
  1473. if (isset($returnValue)) {
  1474. // coerce a php value into a scss one
  1475. if (is_numeric($returnValue)) {
  1476. $returnValue = array('number', $returnValue, "");
  1477. } elseif (is_bool($returnValue)) {
  1478. $returnValue = $returnValue ? self::$true : self::$false;
  1479. } elseif (!is_array($returnValue)) {
  1480. $returnValue = array('keyword', $returnValue);
  1481. }
  1482. return true;
  1483. }
  1484. return false;
  1485. }
  1486. // sorts any keyword arguments
  1487. // TODO: merge with apply arguments
  1488. protected function sortArgs($prototype, $args) {
  1489. $keyArgs = array();
  1490. $posArgs = array();
  1491. foreach ($args as $arg) {
  1492. list($key, $value) = $arg;
  1493. $key = $key[1];
  1494. if (empty($key)) {
  1495. $posArgs[] = $value;
  1496. } else {
  1497. $keyArgs[$key] = $value;
  1498. }
  1499. }
  1500. if (!isset($prototype)) return $posArgs;
  1501. $finalArgs = array();
  1502. foreach ($prototype as $i => $names) {
  1503. if (isset($posArgs[$i])) {
  1504. $finalArgs[] = $posArgs[$i];
  1505. continue;
  1506. }
  1507. $set = false;
  1508. foreach ((array)$names as $name) {
  1509. if (isset($keyArgs[$name])) {
  1510. $finalArgs[] = $keyArgs[$name];
  1511. $set = true;
  1512. break;
  1513. }
  1514. }
  1515. if (!$set) {
  1516. $finalArgs[] = null;
  1517. }
  1518. }
  1519. return $finalArgs;
  1520. }
  1521. protected function coerceForExpression($value) {
  1522. if ($color = $this->coerceColor($value)) {
  1523. return $color;
  1524. }
  1525. return $value;
  1526. }
  1527. protected function coerceColor($value) {
  1528. switch ($value[0]) {
  1529. case "color": return $value;
  1530. case "keyword":
  1531. $name = $value[1];
  1532. if (isset(self::$cssColors[$name])) {
  1533. $rgba = explode(',', self::$cssColors[$name]);
  1534. return isset($rgba[3])
  1535. ? array('color', (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3])
  1536. : array('color', (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]);
  1537. }
  1538. return null;
  1539. }
  1540. return null;
  1541. }
  1542. protected function coerceString($value) {
  1543. switch ($value[0]) {
  1544. case "string":
  1545. return $value;
  1546. case "keyword":
  1547. return array("string", "", array($value[1]));
  1548. }
  1549. return null;
  1550. }
  1551. public function assertList($value) {
  1552. if ($value[0] != "list")
  1553. $this->throwError("expecting list");
  1554. return $value;
  1555. }
  1556. public function assertColor($value) {
  1557. if ($color = $this->coerceColor($value)) return $color;
  1558. $this->throwError("expecting color");
  1559. }
  1560. public function assertNumber($value) {
  1561. if ($value[0] != "number")
  1562. $this->throwError("expecting number");
  1563. return $value[1];
  1564. }
  1565. protected function coercePercent($value) {
  1566. if ($value[0] == "number") {
  1567. if ($value[2] == "%") {
  1568. return $value[1] / 100;
  1569. }
  1570. return $value[1];
  1571. }
  1572. return 0;
  1573. }
  1574. // make sure a color's components don't go out of bounds
  1575. protected function fixColor($c) {
  1576. foreach (range(1, 3) as $i) {
  1577. if ($c[$i] < 0) $c[$i] = 0;
  1578. if ($c[$i] > 255) $c[$i] = 255;
  1579. }
  1580. return $c;
  1581. }
  1582. public function toHSL($red, $green, $blue) {
  1583. $min = min($red, $green, $blue);
  1584. $max = max($red, $green, $blue);
  1585. $l = $min + $max;
  1586. if ($min == $max) {
  1587. $s = $h = 0;
  1588. } else {
  1589. $d = $max - $min;
  1590. if ($l < 255)
  1591. $s = $d / $l;
  1592. else
  1593. $s = $d / (510 - $l);
  1594. if ($red == $max)
  1595. $h = 60 * ($green - $blue) / $d;
  1596. elseif ($green == $max)
  1597. $h = 60 * ($blue - $red) / $d + 120;
  1598. elseif ($blue == $max)
  1599. $h = 60 * ($red - $green) / $d + 240;
  1600. }
  1601. return array('hsl', fmod($h, 360), $s * 100, $l / 5.1);
  1602. }
  1603. public function hueToRGB($m1, $m2, $h) {
  1604. if ($h < 0)
  1605. $h += 1;
  1606. elseif ($h > 1)
  1607. $h -= 1;
  1608. if ($h * 6 < 1)
  1609. return $m1 + ($m2 - $m1) * $h * 6;
  1610. if ($h * 2 < 1)
  1611. return $m2;
  1612. if ($h * 3 < 2)
  1613. return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
  1614. return $m1;
  1615. }
  1616. // H from 0 to 360, S and L from 0 to 100
  1617. public function toRGB($hue, $saturation, $lightness) {
  1618. if ($hue < 0) {
  1619. $hue += 360;
  1620. }
  1621. $h = $hue / 360;
  1622. $s = min(100, max(0, $saturation)) / 100;
  1623. $l = min(100, max(0, $lightness)) / 100;
  1624. $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
  1625. $m1 = $l * 2 - $m2;
  1626. $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
  1627. $g = $this->hueToRGB($m1, $m2, $h) * 255;
  1628. $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
  1629. $out = array('color', $r, $g, $b);
  1630. return $out;
  1631. }
  1632. // Built in functions
  1633. protected static $lib_if = array("condition", "if-true", "if-false");
  1634. protected function lib_if($args) {
  1635. list($cond,$t, $f) = $args;
  1636. if (!$this->isTruthy($cond)) return $f;
  1637. return $t;
  1638. }
  1639. protected static $lib_index = array("list", "value");
  1640. protected function lib_index($args) {
  1641. list($list, $value) = $args;
  1642. $list = $this->assertList($list);
  1643. $values = array();
  1644. foreach ($list[2] as $item) {
  1645. $values[] = $this->normalizeValue($item);
  1646. }
  1647. $key = array_search($this->normalizeValue($value), $values);
  1648. return false === $key ? false : $key + 1;
  1649. }
  1650. protected static $lib_rgb = array("red", "green", "blue");
  1651. protected function lib_rgb($args) {
  1652. list($r,$g,$b) = $args;
  1653. return array("color", $r[1], $g[1], $b[1]);
  1654. }
  1655. protected static $lib_rgba = array(
  1656. array("red", "color"),
  1657. "green", "blue", "alpha");
  1658. protected function lib_rgba($args) {
  1659. if ($color = $this->coerceColor($args[0])) {
  1660. $num = !isset($args[1]) ? $args[3] : $args[1];
  1661. $alpha = $this->assertNumber($num);
  1662. $color[4] = $alpha;
  1663. return $color;
  1664. }
  1665. list($r,$g,$b, $a) = $args;
  1666. return array("color", $r[1], $g[1], $b[1], $a[1]);
  1667. }
  1668. // helper function for adjust_color, change_color, and scale_color
  1669. protected function alter_color($args, $fn) {
  1670. $color = $this->assertColor($args[0]);
  1671. foreach (array(1,2,3,7) as $i) {
  1672. if (isset($args[$i])) {
  1673. $val = $this->assertNumber($args[$i]);
  1674. $ii = $i == 7 ? 4 : $i; // alpha
  1675. $color[$ii] =
  1676. $this->$fn(isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
  1677. }
  1678. }
  1679. if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
  1680. $hsl = $this->toHSL($color[1], $color[2], $color[3]);
  1681. foreach (array(4,5,6) as $i) {
  1682. if (isset($args[$i])) {
  1683. $val = $this->assertNumber($args[$i]);
  1684. $hsl[$i - 3] = $this->$fn($hsl[$i - 3], $val, $i);
  1685. }
  1686. }
  1687. $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
  1688. if (isset($color[4])) $rgb[4] = $color[4];
  1689. $color = $rgb;
  1690. }
  1691. return $color;
  1692. }
  1693. protected static $lib_adjust_color = array(
  1694. "color", "red", "green", "blue",
  1695. "hue", "saturation", "lightness", "alpha"
  1696. );
  1697. protected function adjust_color_helper($base, $alter, $i) {
  1698. return $base += $alter;
  1699. }
  1700. protected function lib_adjust_color($args) {
  1701. return $this->alter_color($args, "adjust_color_helper");
  1702. }
  1703. protected static $lib_change_color = array(
  1704. "color", "red", "green", "blue",
  1705. "hue", "saturation", "lightness", "alpha"
  1706. );
  1707. protected function change_color_helper($base, $alter, $i) {
  1708. return $alter;
  1709. }
  1710. protected function lib_change_color($args) {
  1711. return $this->alter_color($args, "change_color_helper");
  1712. }
  1713. protected static $lib_scale_color = array(
  1714. "color", "red", "green", "blue",
  1715. "hue", "saturation", "lightness", "alpha"
  1716. );
  1717. protected function scale_color_helper($base, $scale, $i) {
  1718. // 1,2,3 - rgb
  1719. // 4, 5, 6 - hsl
  1720. // 7 - a
  1721. switch ($i) {
  1722. case 1:
  1723. case 2:
  1724. case 3:
  1725. $max = 255; break;
  1726. case 4:
  1727. $max = 360; break;
  1728. case 7:
  1729. $max = 1; break;
  1730. default:
  1731. $max = 100;
  1732. }
  1733. $scale = $scale / 100;
  1734. if ($scale < 0) {
  1735. return $base * $scale + $base;
  1736. } else {
  1737. return ($max - $base) * $scale + $base;
  1738. }
  1739. }
  1740. protected function lib_scale_color($args) {
  1741. return $this->alter_color($args, "scale_color_helper");
  1742. }
  1743. protected static $lib_ie_hex_str = array("color");
  1744. protected function lib_ie_hex_str($args) {
  1745. $color = $this->coerceColor($args[0]);
  1746. $color[4] = isset($color[4]) ? round(255*$color[4]) : 255;
  1747. return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
  1748. }
  1749. protected static $lib_red = array("color");
  1750. protected function lib_red($args) {
  1751. $color = $this->coerceColor($args[0]);
  1752. return $color[1];
  1753. }
  1754. protected static $lib_green = array("color");
  1755. protected function lib_green($args) {
  1756. $color = $this->coerceColor($args[0]);
  1757. return $color[2];
  1758. }
  1759. protected static $lib_blue = array("color");
  1760. protected function lib_blue($args) {
  1761. $color = $this->coerceColor($args[0]);
  1762. return $color[3];
  1763. }
  1764. protected static $lib_alpha = array("color");
  1765. protected function lib_alpha($args) {
  1766. if ($color = $this->coerceColor($args[0])) {
  1767. return isset($color[4]) ? $color[4] : 1;
  1768. }
  1769. // this might be the IE function, so return value unchanged
  1770. return null;
  1771. }
  1772. protected static $lib_opacity = array("color");
  1773. protected function lib_opacity($args) {
  1774. $value = $args[0];
  1775. if ($value[0] === 'number') return null;
  1776. return $this->lib_alpha($args);
  1777. }
  1778. // mix two colors
  1779. protected static $lib_mix = array("color-1", "color-2", "weight");
  1780. protected function lib_mix($args) {
  1781. list($first, $second, $weight) = $args;
  1782. $first = $this->assertColor($first);
  1783. $second = $this->assertColor($second);
  1784. if (!isset($weight)) {
  1785. $weight = 0.5;
  1786. } else {
  1787. $weight = $this->coercePercent($weight);
  1788. }
  1789. $firstAlpha = isset($first[4]) ? $first[4] : 1;
  1790. $secondAlpha = isset($second[4]) ? $second[4] : 1;
  1791. $w = $weight * 2 - 1;
  1792. $a = $firstAlpha - $secondAlpha;
  1793. $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
  1794. $w2 = 1.0 - $w1;
  1795. $new = array('color',
  1796. $w1 * $first[1] + $w2 * $second[1],
  1797. $w1 * $first[2] + $w2 * $second[2],
  1798. $w1 * $first[3] + $w2 * $second[3],
  1799. );
  1800. if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
  1801. $new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1);
  1802. }
  1803. return $this->fixColor($new);
  1804. }
  1805. protected static $lib_hsl = array("hue", "saturation", "lightness");
  1806. protected function lib_hsl($args) {
  1807. list($h, $s, $l) = $args;
  1808. return $this->toRGB($h[1], $s[1], $l[1]);
  1809. }
  1810. protected static $lib_hsla = array("hue", "saturation",
  1811. "lightness", "alpha");
  1812. protected function lib_hsla($args) {
  1813. list($h, $s, $l, $a) = $args;
  1814. $color = $this->toRGB($h[1], $s[1], $l[1]);
  1815. $color[4] = $a[1];
  1816. return $color;
  1817. }
  1818. protected static $lib_hue = array("color");
  1819. protected function lib_hue($args) {
  1820. $color = $this->assertColor($args[0]);
  1821. $hsl = $this->toHSL($color[1], $color[2], $color[3]);
  1822. return array("number", $hsl[1], "deg");
  1823. }
  1824. protected static $lib_saturation = array("color");
  1825. protected function lib_saturation($args) {
  1826. $color = $this->assertColor($args[0]);
  1827. $hsl = $this->toHSL($color[1], $color[2], $color[3]);
  1828. return array("number", $hsl[2], "%");
  1829. }
  1830. protected static $lib_lightness = array("color");
  1831. protected function lib_lightness($args) {
  1832. $color = $this->assertColor($args[0]);
  1833. $hsl = $this->toHSL($color[1], $color[2], $color[3]);
  1834. return array("number", $hsl[3], "%");
  1835. }
  1836. protected function adjustHsl($color, $idx, $amount) {
  1837. $hsl = $this->toHSL($color[1], $color[2], $color[3]);
  1838. $hsl[$idx] += $amount;
  1839. $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
  1840. if (isset($color[4])) $out[4] = $color[4];
  1841. return $out;
  1842. }
  1843. protected static $lib_adjust_hue = array("color", "degrees");
  1844. protected function lib_adjust_hue($args) {
  1845. $color = $this->assertColor($args[0]);
  1846. $degrees = $this->assertNumber($args[1]);
  1847. return $this->adjustHsl($color, 1, $degrees);
  1848. }
  1849. protected static $lib_lighten = array("color", "amount");
  1850. protected function lib_lighten($args) {
  1851. $color = $this->assertColor($args[0]);
  1852. $amount = 100*$this->coercePercent($args[1]);
  1853. return $this->adjustHsl($color, 3, $amount);
  1854. }
  1855. protected static $lib_darken = array("color", "amount");
  1856. protected function lib_darken($args) {
  1857. $color = $this->assertColor($args[0]);
  1858. $amount = 100*$this->coercePercent($args[1]);
  1859. return $this->adjustHsl($color, 3, -$amount);
  1860. }
  1861. protected static $lib_saturate = array("color", "amount");
  1862. protected function lib_saturate($args) {
  1863. $value = $args[0];
  1864. if ($value[0] === 'number') return null;
  1865. $color = $this->assertColor($value);
  1866. $amount = 100*$this->coercePercent($args[1]);
  1867. return $this->adjustHsl($color, 2, $amount);
  1868. }
  1869. protected static $lib_desaturate = array("color", "amount");
  1870. protected function lib_desaturate($args) {
  1871. $color = $this->assertColor($args[0]);
  1872. $amount = 100*$this->coercePercent($args[1]);
  1873. return $this->adjustHsl($color, 2, -$amount);
  1874. }
  1875. protected static $lib_grayscale = array("color");
  1876. protected function lib_grayscale($args) {
  1877. $value = $args[0];
  1878. if ($value[0] === 'number') return null;
  1879. return $this->adjustHsl($this->assertColor($value), 2, -100);
  1880. }
  1881. protected static $lib_complement = array("color");
  1882. protected function lib_complement($args) {
  1883. return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
  1884. }
  1885. protected static $lib_invert = array("color");
  1886. protected function lib_invert($args) {
  1887. $value = $args[0];
  1888. if ($value[0] === 'number') return null;
  1889. $color = $this->assertColor($value);
  1890. $color[1] = 255 - $color[1];
  1891. $color[2] = 255 - $color[2];
  1892. $color[3] = 255 - $color[3];
  1893. return $color;
  1894. }
  1895. // increases opacity by amount
  1896. protected static $lib_opacify = array("color", "amount");
  1897. protected function lib_opacify($args) {
  1898. $color = $this->assertColor($args[0]);
  1899. $amount = $this->coercePercent($args[1]);
  1900. $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
  1901. $color[4] = min(1, max(0, $color[4]));
  1902. return $color;
  1903. }
  1904. protected static $lib_fade_in = array("color", "amount");
  1905. protected function lib_fade_in($args) {
  1906. return $this->lib_opacify($args);
  1907. }
  1908. // decreases opacity by amount
  1909. protected static $lib_transparentize = array("color", "amount");
  1910. protected function lib_transparentize($args) {
  1911. $color = $this->assertColor($args[0]);
  1912. $amount = $this->coercePercent($args[1]);
  1913. $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
  1914. $color[4] = min(1, max(0, $color[4]));
  1915. return $color;
  1916. }
  1917. protected static $lib_fade_out = array("color", "amount");
  1918. protected function lib_fade_out($args) {
  1919. return $this->lib_transparentize($args);
  1920. }
  1921. protected static $lib_unquote = array("string");
  1922. protected function lib_unquote($args) {
  1923. $str = $args[0];
  1924. if ($str[0] == "string") $str[1] = "";
  1925. return $str;
  1926. }
  1927. protected static $lib_quote = array("string");
  1928. protected function lib_quote($args) {
  1929. $value = $args[0];
  1930. if ($value[0] == "string" && !empty($value[1]))
  1931. return $value;
  1932. return array("string", '"', array($value));
  1933. }
  1934. protected static $lib_percentage = array("value");
  1935. protected function lib_percentage($args) {
  1936. return array("number",
  1937. $this->coercePercent($args[0]) * 100,
  1938. "%");
  1939. }
  1940. protected static $lib_round = array("value");
  1941. protected function lib_round($args) {
  1942. $num = $args[0];
  1943. $num[1] = round($num[1]);
  1944. return $num;
  1945. }
  1946. protected static $lib_floor = array("value");
  1947. protected function lib_floor($args) {
  1948. $num = $args[0];
  1949. $num[1] = floor($num[1]);
  1950. return $num;
  1951. }
  1952. protected static $lib_ceil = array("value");
  1953. protected function lib_ceil($args) {
  1954. $num = $args[0];
  1955. $num[1] = ceil($num[1]);
  1956. return $num;
  1957. }
  1958. protected static $lib_abs = array("value");
  1959. protected function lib_abs($args) {
  1960. $num = $args[0];
  1961. $num[1] = abs($num[1]);
  1962. return $num;
  1963. }
  1964. protected function lib_min($args) {
  1965. $numbers = $this->getNormalizedNumbers($args);
  1966. $min = null;
  1967. foreach ($numbers as $key => $number) {
  1968. if (null === $min || $number[1] <= $min[1]) {
  1969. $min = array($key, $number[1]);
  1970. }
  1971. }
  1972. return $args[$min[0]];
  1973. }
  1974. protected function lib_max($args) {
  1975. $numbers = $this->getNormalizedNumbers($args);
  1976. $max = null;
  1977. foreach ($numbers as $key => $number) {
  1978. if (null === $max || $number[1] >= $max[1]) {
  1979. $max = array($key, $number[1]);
  1980. }
  1981. }
  1982. return $args[$max[0]];
  1983. }
  1984. protected function getNormalizedNumbers($args) {
  1985. $unit = null;
  1986. $originalUnit = null;
  1987. $numbers = array();
  1988. foreach ($args as $key => $item) {
  1989. if ('number' != $item[0]) {
  1990. $this->throwError("%s is not a number", $item[0]);
  1991. }
  1992. $number = $this->normalizeNumber($item);
  1993. if (null === $unit) {
  1994. $unit = $number[2];
  1995. $originalUnit = $item[2];
  1996. } elseif ($unit !== $number[2]) {
  1997. $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item[2]);
  1998. }
  1999. $numbers[$key] = $number;
  2000. }
  2001. return $numbers;
  2002. }
  2003. protected static $lib_length = array("list");
  2004. protected function lib_length($args) {
  2005. $list = $this->coerceList($args[0]);
  2006. return count($list[2]);
  2007. }
  2008. protected static $lib_nth = array("list", "n");
  2009. protected function lib_nth($args) {
  2010. $list = $this->coerceList($args[0]);
  2011. $n = $this->assertNumber($args[1]) - 1;
  2012. return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue;
  2013. }
  2014. protected function listSeparatorForJoin($list1, $sep) {
  2015. if (!isset($sep)) return $list1[1];
  2016. switch ($this->compileValue($sep)) {
  2017. case "comma":
  2018. return ",";
  2019. case "space":
  2020. return "";
  2021. default:
  2022. return $list1[1];
  2023. }
  2024. }
  2025. protected static $lib_join = array("list1", "list2", "separator");
  2026. protected function lib_join($args) {
  2027. list($list1, $list2, $sep) = $args;
  2028. $list1 = $this->coerceList($list1, " ");
  2029. $list2 = $this->coerceList($list2, " ");
  2030. $sep = $this->listSeparatorForJoin($list1, $sep);
  2031. return array("list", $sep, array_merge($list1[2], $list2[2]));
  2032. }
  2033. protected static $lib_append = array("list", "val", "separator");
  2034. protected function lib_append($args) {
  2035. list($list1, $value, $sep) = $args;
  2036. $list1 = $this->coerceList($list1, " ");
  2037. $sep = $this->listSeparatorForJoin($list1, $sep);
  2038. return array("list", $sep, array_merge($list1[2], array($value)));
  2039. }
  2040. protected function lib_zip($args) {
  2041. foreach ($args as $arg) {
  2042. $this->assertList($arg);
  2043. }
  2044. $lists = array();
  2045. $firstList = array_shift($args);
  2046. foreach ($firstList[2] as $key => $item) {
  2047. $list = array("list", "", array($item));
  2048. foreach ($args as $arg) {
  2049. if (isset($arg[2][$key])) {
  2050. $list[2][] = $arg[2][$key];
  2051. } else {
  2052. break 2;
  2053. }
  2054. }
  2055. $lists[] = $list;
  2056. }
  2057. return array("list", ",", $lists);
  2058. }
  2059. protected static $lib_type_of = array("value");
  2060. protected function lib_type_of($args) {
  2061. $value = $args[0];
  2062. switch ($value[0]) {
  2063. case "keyword":
  2064. if ($value == self::$true || $value == self::$false) {
  2065. return "bool";
  2066. }
  2067. if ($this->coerceColor($value)) {
  2068. return "color";
  2069. }
  2070. return "string";
  2071. default:
  2072. return $value[0];
  2073. }
  2074. }
  2075. protected static $lib_unit = array("number");
  2076. protected function lib_unit($args) {
  2077. $num = $args[0];
  2078. if ($num[0] == "number") {
  2079. return array("string", '"', array($num[2]));
  2080. }
  2081. return "";
  2082. }
  2083. protected static $lib_unitless = array("number");
  2084. protected function lib_unitless($args) {
  2085. $value = $args[0];
  2086. return $value[0] == "number" && empty($value[2]);
  2087. }
  2088. protected static $lib_comparable = array("number-1", "number-2");
  2089. protected function lib_comparable($args) {
  2090. list($number1, $number2) = $args;
  2091. if (!isset($number1[0]) || $number1[0] != "number" || !isset($number2[0]) || $number2[0] != "number") {
  2092. $this->throwError('Invalid argument(s) for "comparable"');
  2093. }
  2094. $number1 = $this->normalizeNumber($number1);
  2095. $number2 = $this->normalizeNumber($number2);
  2096. return $number1[2] == $number2[2] || $number1[2] == "" || $number2[2] == "";
  2097. }
  2098. /**
  2099. * Workaround IE7's content counter bug.
  2100. *
  2101. * @param array $args
  2102. */
  2103. protected function lib_counter($args) {
  2104. $list = array_map(array($this, 'compileValue'), $args);
  2105. return array('string', '', array('counter(' . implode(',', $list) . ')'));
  2106. }
  2107. public function throwError($msg = null) {
  2108. if (func_num_args() > 1) {
  2109. $msg = call_user_func_array("sprintf", func_get_args());
  2110. }
  2111. if ($this->sourcePos >= 0 && isset($this->sourceParser)) {
  2112. $this->sourceParser->throwParseError($msg, $this->sourcePos);
  2113. }
  2114. throw new Exception($msg);
  2115. }
  2116. /**
  2117. * CSS Colors
  2118. *
  2119. * @see http://www.w3.org/TR/css3-color
  2120. */
  2121. static protected $cssColors = array(
  2122. 'aliceblue' => '240,248,255',
  2123. 'antiquewhite' => '250,235,215',
  2124. 'aqua' => '0,255,255',
  2125. 'aquamarine' => '127,255,212',
  2126. 'azure' => '240,255,255',
  2127. 'beige' => '245,245,220',
  2128. 'bisque' => '255,228,196',
  2129. 'black' => '0,0,0',
  2130. 'blanchedalmond' => '255,235,205',
  2131. 'blue' => '0,0,255',
  2132. 'blueviolet' => '138,43,226',
  2133. 'brown' => '165,42,42',
  2134. 'burlywood' => '222,184,135',
  2135. 'cadetblue' => '95,158,160',
  2136. 'chartreuse' => '127,255,0',
  2137. 'chocolate' => '210,105,30',
  2138. 'coral' => '255,127,80',
  2139. 'cornflowerblue' => '100,149,237',
  2140. 'cornsilk' => '255,248,220',
  2141. 'crimson' => '220,20,60',
  2142. 'cyan' => '0,255,255',
  2143. 'darkblue' => '0,0,139',
  2144. 'darkcyan' => '0,139,139',
  2145. 'darkgoldenrod' => '184,134,11',
  2146. 'darkgray' => '169,169,169',
  2147. 'darkgreen' => '0,100,0',
  2148. 'darkgrey' => '169,169,169',
  2149. 'darkkhaki' => '189,183,107',
  2150. 'darkmagenta' => '139,0,139',
  2151. 'darkolivegreen' => '85,107,47',
  2152. 'darkorange' => '255,140,0',
  2153. 'darkorchid' => '153,50,204',
  2154. 'darkred' => '139,0,0',
  2155. 'darksalmon' => '233,150,122',
  2156. 'darkseagreen' => '143,188,143',
  2157. 'darkslateblue' => '72,61,139',
  2158. 'darkslategray' => '47,79,79',
  2159. 'darkslategrey' => '47,79,79',
  2160. 'darkturquoise' => '0,206,209',
  2161. 'darkviolet' => '148,0,211',
  2162. 'deeppink' => '255,20,147',
  2163. 'deepskyblue' => '0,191,255',
  2164. 'dimgray' => '105,105,105',
  2165. 'dimgrey' => '105,105,105',
  2166. 'dodgerblue' => '30,144,255',
  2167. 'firebrick' => '178,34,34',
  2168. 'floralwhite' => '255,250,240',
  2169. 'forestgreen' => '34,139,34',
  2170. 'fuchsia' => '255,0,255',
  2171. 'gainsboro' => '220,220,220',
  2172. 'ghostwhite' => '248,248,255',
  2173. 'gold' => '255,215,0',
  2174. 'goldenrod' => '218,165,32',
  2175. 'gray' => '128,128,128',
  2176. 'green' => '0,128,0',
  2177. 'greenyellow' => '173,255,47',
  2178. 'grey' => '128,128,128',
  2179. 'honeydew' => '240,255,240',
  2180. 'hotpink' => '255,105,180',
  2181. 'indianred' => '205,92,92',
  2182. 'indigo' => '75,0,130',
  2183. 'ivory' => '255,255,240',
  2184. 'khaki' => '240,230,140',
  2185. 'lavender' => '230,230,250',
  2186. 'lavenderblush' => '255,240,245',
  2187. 'lawngreen' => '124,252,0',
  2188. 'lemonchiffon' => '255,250,205',
  2189. 'lightblue' => '173,216,230',
  2190. 'lightcoral' => '240,128,128',
  2191. 'lightcyan' => '224,255,255',
  2192. 'lightgoldenrodyellow' => '250,250,210',
  2193. 'lightgray' => '211,211,211',
  2194. 'lightgreen' => '144,238,144',
  2195. 'lightgrey' => '211,211,211',
  2196. 'lightpink' => '255,182,193',
  2197. 'lightsalmon' => '255,160,122',
  2198. 'lightseagreen' => '32,178,170',
  2199. 'lightskyblue' => '135,206,250',
  2200. 'lightslategray' => '119,136,153',
  2201. 'lightslategrey' => '119,136,153',
  2202. 'lightsteelblue' => '176,196,222',
  2203. 'lightyellow' => '255,255,224',
  2204. 'lime' => '0,255,0',
  2205. 'limegreen' => '50,205,50',
  2206. 'linen' => '250,240,230',
  2207. 'magenta' => '255,0,255',
  2208. 'maroon' => '128,0,0',
  2209. 'mediumaquamarine' => '102,205,170',
  2210. 'mediumblue' => '0,0,205',
  2211. 'mediumorchid' => '186,85,211',
  2212. 'mediumpurple' => '147,112,219',
  2213. 'mediumseagreen' => '60,179,113',
  2214. 'mediumslateblue' => '123,104,238',
  2215. 'mediumspringgreen' => '0,250,154',
  2216. 'mediumturquoise' => '72,209,204',
  2217. 'mediumvioletred' => '199,21,133',
  2218. 'midnightblue' => '25,25,112',
  2219. 'mintcream' => '245,255,250',
  2220. 'mistyrose' => '255,228,225',
  2221. 'moccasin' => '255,228,181',
  2222. 'navajowhite' => '255,222,173',
  2223. 'navy' => '0,0,128',
  2224. 'oldlace' => '253,245,230',
  2225. 'olive' => '128,128,0',
  2226. 'olivedrab' => '107,142,35',
  2227. 'orange' => '255,165,0',
  2228. 'orangered' => '255,69,0',
  2229. 'orchid' => '218,112,214',
  2230. 'palegoldenrod' => '238,232,170',
  2231. 'palegreen' => '152,251,152',
  2232. 'paleturquoise' => '175,238,238',
  2233. 'palevioletred' => '219,112,147',
  2234. 'papayawhip' => '255,239,213',
  2235. 'peachpuff' => '255,218,185',
  2236. 'peru' => '205,133,63',
  2237. 'pink' => '255,192,203',
  2238. 'plum' => '221,160,221',
  2239. 'powderblue' => '176,224,230',
  2240. 'purple' => '128,0,128',
  2241. 'red' => '255,0,0',
  2242. 'rosybrown' => '188,143,143',
  2243. 'royalblue' => '65,105,225',
  2244. 'saddlebrown' => '139,69,19',
  2245. 'salmon' => '250,128,114',
  2246. 'sandybrown' => '244,164,96',
  2247. 'seagreen' => '46,139,87',
  2248. 'seashell' => '255,245,238',
  2249. 'sienna' => '160,82,45',
  2250. 'silver' => '192,192,192',
  2251. 'skyblue' => '135,206,235',
  2252. 'slateblue' => '106,90,205',
  2253. 'slategray' => '112,128,144',
  2254. 'slategrey' => '112,128,144',
  2255. 'snow' => '255,250,250',
  2256. 'springgreen' => '0,255,127',
  2257. 'steelblue' => '70,130,180',
  2258. 'tan' => '210,180,140',
  2259. 'teal' => '0,128,128',
  2260. 'thistle' => '216,191,216',
  2261. 'tomato' => '255,99,71',
  2262. 'transparent' => '0,0,0,0',
  2263. 'turquoise' => '64,224,208',
  2264. 'violet' => '238,130,238',
  2265. 'wheat' => '245,222,179',
  2266. 'white' => '255,255,255',
  2267. 'whitesmoke' => '245,245,245',
  2268. 'yellow' => '255,255,0',
  2269. 'yellowgreen' => '154,205,50'
  2270. );
  2271. }
  2272. /**
  2273. * SCSS parser
  2274. *
  2275. * @author Leaf Corcoran <leafot@gmail.com>
  2276. */
  2277. class scss_parser {
  2278. static protected $precedence = array(
  2279. "or" => 0,
  2280. "and" => 1,
  2281. '==' => 2,
  2282. '!=' => 2,
  2283. '<=' => 2,
  2284. '>=' => 2,
  2285. '=' => 2,
  2286. '<' => 3,
  2287. '>' => 2,
  2288. '+' => 3,
  2289. '-' => 3,
  2290. '*' => 4,
  2291. '/' => 4,
  2292. '%' => 4,
  2293. );
  2294. static protected $operators = array("+", "-", "*", "/", "%",
  2295. "==", "!=", "<=", ">=", "<", ">", "and", "or");
  2296. static protected $operatorStr;
  2297. static protected $whitePattern;
  2298. static protected $commentMulti;
  2299. static protected $commentSingle = "//";
  2300. static protected $commentMultiLeft = "/*";
  2301. static protected $commentMultiRight = "*/";
  2302. /**
  2303. * Constructor
  2304. *
  2305. * @param string $sourceName
  2306. * @param boolean $rootParser
  2307. */
  2308. public function __construct($sourceName = null, $rootParser = true) {
  2309. $this->sourceName = $sourceName;
  2310. $this->rootParser = $rootParser;
  2311. if (empty(self::$operatorStr)) {
  2312. self::$operatorStr = $this->makeOperatorStr(self::$operators);
  2313. $commentSingle = $this->preg_quote(self::$commentSingle);
  2314. $commentMultiLeft = $this->preg_quote(self::$commentMultiLeft);
  2315. $commentMultiRight = $this->preg_quote(self::$commentMultiRight);
  2316. self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight;
  2317. self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais';
  2318. }
  2319. }
  2320. static protected function makeOperatorStr($operators) {
  2321. return '('.implode('|', array_map(array('scss_parser','preg_quote'),
  2322. $operators)).')';
  2323. }
  2324. /**
  2325. * Parser buffer
  2326. *
  2327. * @param string $buffer;
  2328. *
  2329. * @return \StdClass
  2330. */
  2331. public function parse($buffer)
  2332. {
  2333. $this->count = 0;
  2334. $this->env = null;
  2335. $this->inParens = false;
  2336. $this->eatWhiteDefault = true;
  2337. $this->insertComments = true;
  2338. $this->buffer = $buffer;
  2339. $this->pushBlock(null); // root block
  2340. $this->whitespace();
  2341. while (false !== $this->parseChunk())
  2342. ;
  2343. if ($this->count != strlen($this->buffer)) {
  2344. $this->throwParseError();
  2345. }
  2346. if (!empty($this->env->parent)) {
  2347. $this->throwParseError("unclosed block");
  2348. }
  2349. $this->env->isRoot = true;
  2350. return $this->env;
  2351. }
  2352. /**
  2353. * Parse a single chunk off the head of the buffer and append it to the
  2354. * current parse environment.
  2355. *
  2356. * Returns false when the buffer is empty, or when there is an error.
  2357. *
  2358. * This function is called repeatedly until the entire document is
  2359. * parsed.
  2360. *
  2361. * This parser is most similar to a recursive descent parser. Single
  2362. * functions represent discrete grammatical rules for the language, and
  2363. * they are able to capture the text that represents those rules.
  2364. *
  2365. * Consider the function scssc::keyword(). (All parse functions are
  2366. * structured the same.)
  2367. *
  2368. * The function takes a single reference argument. When calling the
  2369. * function it will attempt to match a keyword on the head of the buffer.
  2370. * If it is successful, it will place the keyword in the referenced
  2371. * argument, advance the position in the buffer, and return true. If it
  2372. * fails then it won't advance the buffer and it will return false.
  2373. *
  2374. * All of these parse functions are powered by scssc::match(), which behaves
  2375. * the same way, but takes a literal regular expression. Sometimes it is
  2376. * more convenient to use match instead of creating a new function.
  2377. *
  2378. * Because of the format of the functions, to parse an entire string of
  2379. * grammatical rules, you can chain them together using &&.
  2380. *
  2381. * But, if some of the rules in the chain succeed before one fails, then
  2382. * the buffer position will be left at an invalid state. In order to
  2383. * avoid this, scssc::seek() is used to remember and set buffer positions.
  2384. *
  2385. * Before parsing a chain, use $s = $this->seek() to remember the current
  2386. * position into $s. Then if a chain fails, use $this->seek($s) to
  2387. * go back where we started.
  2388. *
  2389. * @return boolean
  2390. */
  2391. protected function parseChunk() {
  2392. $s = $this->seek();
  2393. // the directives
  2394. if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") {
  2395. if ($this->literal("@media") && $this->mediaQueryList($mediaQueryList) && $this->literal("{")) {
  2396. $media = $this->pushSpecialBlock("media");
  2397. $media->queryList = $mediaQueryList[2];
  2398. return true;
  2399. } else {
  2400. $this->seek($s);
  2401. }
  2402. if ($this->literal("@mixin") &&
  2403. $this->keyword($mixinName) &&
  2404. ($this->argumentDef($args) || true) &&
  2405. $this->literal("{"))
  2406. {
  2407. $mixin = $this->pushSpecialBlock("mixin");
  2408. $mixin->name = $mixinName;
  2409. $mixin->args = $args;
  2410. return true;
  2411. } else {
  2412. $this->seek($s);
  2413. }
  2414. if ($this->literal("@include") &&
  2415. $this->keyword($mixinName) &&
  2416. ($this->literal("(") &&
  2417. ($this->argValues($argValues) || true) &&
  2418. $this->literal(")") || true) &&
  2419. ($this->end() ||
  2420. $this->literal("{") && $hasBlock = true))
  2421. {
  2422. $child = array("include",
  2423. $mixinName, isset($argValues) ? $argValues : null, null);
  2424. if (!empty($hasBlock)) {
  2425. $include = $this->pushSpecialBlock("include");
  2426. $include->child = $child;
  2427. } else {
  2428. $this->append($child, $s);
  2429. }
  2430. return true;
  2431. } else {
  2432. $this->seek($s);
  2433. }
  2434. if ($this->literal("@import") &&
  2435. $this->valueList($importPath) &&
  2436. $this->end())
  2437. {
  2438. $this->append(array("import", $importPath), $s);
  2439. return true;
  2440. } else {
  2441. $this->seek($s);
  2442. }
  2443. if ($this->literal("@extend") &&
  2444. $this->selectors($selector) &&
  2445. $this->end())
  2446. {
  2447. $this->append(array("extend", $selector), $s);
  2448. return true;
  2449. } else {
  2450. $this->seek($s);
  2451. }
  2452. if ($this->literal("@function") &&
  2453. $this->keyword($fnName) &&
  2454. $this->argumentDef($args) &&
  2455. $this->literal("{"))
  2456. {
  2457. $func = $this->pushSpecialBlock("function");
  2458. $func->name = $fnName;
  2459. $func->args = $args;
  2460. return true;
  2461. } else {
  2462. $this->seek($s);
  2463. }
  2464. if ($this->literal("@return") && $this->valueList($retVal) && $this->end()) {
  2465. $this->append(array("return", $retVal), $s);
  2466. return true;
  2467. } else {
  2468. $this->seek($s);
  2469. }
  2470. if ($this->literal("@each") &&
  2471. $this->variable($varName) &&
  2472. $this->literal("in") &&
  2473. $this->valueList($list) &&
  2474. $this->literal("{"))
  2475. {
  2476. $each = $this->pushSpecialBlock("each");
  2477. $each->var = $varName[1];
  2478. $each->list = $list;
  2479. return true;
  2480. } else {
  2481. $this->seek($s);
  2482. }
  2483. if ($this->literal("@while") &&
  2484. $this->expression($cond) &&
  2485. $this->literal("{"))
  2486. {
  2487. $while = $this->pushSpecialBlock("while");
  2488. $while->cond = $cond;
  2489. return true;
  2490. } else {
  2491. $this->seek($s);
  2492. }
  2493. if ($this->literal("@for") &&
  2494. $this->variable($varName) &&
  2495. $this->literal("from") &&
  2496. $this->expression($start) &&
  2497. ($this->literal("through") ||
  2498. ($forUntil = true && $this->literal("to"))) &&
  2499. $this->expression($end) &&
  2500. $this->literal("{"))
  2501. {
  2502. $for = $this->pushSpecialBlock("for");
  2503. $for->var = $varName[1];
  2504. $for->start = $start;
  2505. $for->end = $end;
  2506. $for->until = isset($forUntil);
  2507. return true;
  2508. } else {
  2509. $this->seek($s);
  2510. }
  2511. if ($this->literal("@if") && $this->valueList($cond) && $this->literal("{")) {
  2512. $if = $this->pushSpecialBlock("if");
  2513. $if->cond = $cond;
  2514. $if->cases = array();
  2515. return true;
  2516. } else {
  2517. $this->seek($s);
  2518. }
  2519. if (($this->literal("@debug") || $this->literal("@warn")) &&
  2520. $this->valueList($value) &&
  2521. $this->end()) {
  2522. $this->append(array("debug", $value, $s), $s);
  2523. return true;
  2524. } else {
  2525. $this->seek($s);
  2526. }
  2527. if ($this->literal("@content") && $this->end()) {
  2528. $this->append(array("mixin_content"), $s);
  2529. return true;
  2530. } else {
  2531. $this->seek($s);
  2532. }
  2533. $last = $this->last();
  2534. if (isset($last) && $last[0] == "if") {
  2535. list(, $if) = $last;
  2536. if ($this->literal("@else")) {
  2537. if ($this->literal("{")) {
  2538. $else = $this->pushSpecialBlock("else");
  2539. } elseif ($this->literal("if") && $this->valueList($cond) && $this->literal("{")) {
  2540. $else = $this->pushSpecialBlock("elseif");
  2541. $else->cond = $cond;
  2542. }
  2543. if (isset($else)) {
  2544. $else->dontAppend = true;
  2545. $if->cases[] = $else;
  2546. return true;
  2547. }
  2548. }
  2549. $this->seek($s);
  2550. }
  2551. if ($this->literal("@charset") &&
  2552. $this->valueList($charset) && $this->end())
  2553. {
  2554. $this->append(array("charset", $charset), $s);
  2555. return true;
  2556. } else {
  2557. $this->seek($s);
  2558. }
  2559. // doesn't match built in directive, do generic one
  2560. if ($this->literal("@", false) && $this->keyword($dirName) &&
  2561. ($this->openString("{", $dirValue) || true) &&
  2562. $this->literal("{"))
  2563. {
  2564. $directive = $this->pushSpecialBlock("directive");
  2565. $directive->name = $dirName;
  2566. if (isset($dirValue)) $directive->value = $dirValue;
  2567. return true;
  2568. }
  2569. $this->seek($s);
  2570. return false;
  2571. }
  2572. // property shortcut
  2573. // captures most properties before having to parse a selector
  2574. if ($this->keyword($name, false) &&
  2575. $this->literal(": ") &&
  2576. $this->valueList($value) &&
  2577. $this->end())
  2578. {
  2579. $name = array("string", "", array($name));
  2580. $this->append(array("assign", $name, $value), $s);
  2581. return true;
  2582. } else {
  2583. $this->seek($s);
  2584. }
  2585. // variable assigns
  2586. if ($this->variable($name) &&
  2587. $this->literal(":") &&
  2588. $this->valueList($value) && $this->end())
  2589. {
  2590. // check for !default
  2591. $defaultVar = $value[0] == "list" && $this->stripDefault($value);
  2592. $this->append(array("assign", $name, $value, $defaultVar), $s);
  2593. return true;
  2594. } else {
  2595. $this->seek($s);
  2596. }
  2597. // misc
  2598. if ($this->literal("-->")) {
  2599. return true;
  2600. }
  2601. // opening css block
  2602. $oldComments = $this->insertComments;
  2603. $this->insertComments = false;
  2604. if ($this->selectors($selectors) && $this->literal("{")) {
  2605. $this->pushBlock($selectors);
  2606. $this->insertComments = $oldComments;
  2607. return true;
  2608. } else {
  2609. $this->seek($s);
  2610. }
  2611. $this->insertComments = $oldComments;
  2612. // property assign, or nested assign
  2613. if ($this->propertyName($name) && $this->literal(":")) {
  2614. $foundSomething = false;
  2615. if ($this->valueList($value)) {
  2616. $this->append(array("assign", $name, $value), $s);
  2617. $foundSomething = true;
  2618. }
  2619. if ($this->literal("{")) {
  2620. $propBlock = $this->pushSpecialBlock("nestedprop");
  2621. $propBlock->prefix = $name;
  2622. $foundSomething = true;
  2623. } elseif ($foundSomething) {
  2624. $foundSomething = $this->end();
  2625. }
  2626. if ($foundSomething) {
  2627. return true;
  2628. }
  2629. $this->seek($s);
  2630. } else {
  2631. $this->seek($s);
  2632. }
  2633. // closing a block
  2634. if ($this->literal("}")) {
  2635. $block = $this->popBlock();
  2636. if (isset($block->type) && $block->type == "include") {
  2637. $include = $block->child;
  2638. unset($block->child);
  2639. $include[3] = $block;
  2640. $this->append($include, $s);
  2641. } elseif (empty($block->dontAppend)) {
  2642. $type = isset($block->type) ? $block->type : "block";
  2643. $this->append(array($type, $block), $s);
  2644. }
  2645. return true;
  2646. }
  2647. // extra stuff
  2648. if ($this->literal(";") ||
  2649. $this->literal("<!--"))
  2650. {
  2651. return true;
  2652. }
  2653. return false;
  2654. }
  2655. protected function stripDefault(&$value) {
  2656. $def = end($value[2]);
  2657. if ($def[0] == "keyword" && $def[1] == "!default") {
  2658. array_pop($value[2]);
  2659. $value = $this->flattenList($value);
  2660. return true;
  2661. }
  2662. if ($def[0] == "list") {
  2663. return $this->stripDefault($value[2][count($value[2]) - 1]);
  2664. }
  2665. return false;
  2666. }
  2667. protected function literal($what, $eatWhitespace = null) {
  2668. if (!isset($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault;
  2669. // shortcut on single letter
  2670. if (!isset($what[1]) && isset($this->buffer[$this->count])) {
  2671. if ($this->buffer[$this->count] == $what) {
  2672. if (!$eatWhitespace) {
  2673. $this->count++;
  2674. return true;
  2675. }
  2676. // goes below...
  2677. } else {
  2678. return false;
  2679. }
  2680. }
  2681. return $this->match($this->preg_quote($what), $m, $eatWhitespace);
  2682. }
  2683. // tree builders
  2684. protected function pushBlock($selectors) {
  2685. $b = new stdClass;
  2686. $b->parent = $this->env; // not sure if we need this yet
  2687. $b->selectors = $selectors;
  2688. $b->children = array();
  2689. $this->env = $b;
  2690. return $b;
  2691. }
  2692. protected function pushSpecialBlock($type) {
  2693. $block = $this->pushBlock(null);
  2694. $block->type = $type;
  2695. return $block;
  2696. }
  2697. protected function popBlock() {
  2698. if (empty($this->env->parent)) {
  2699. $this->throwParseError("unexpected }");
  2700. }
  2701. $old = $this->env;
  2702. $this->env = $this->env->parent;
  2703. unset($old->parent);
  2704. return $old;
  2705. }
  2706. protected function append($statement, $pos=null) {
  2707. if ($pos !== null) {
  2708. $statement[-1] = $pos;
  2709. if (!$this->rootParser) $statement[-2] = $this;
  2710. }
  2711. $this->env->children[] = $statement;
  2712. }
  2713. // last child that was appended
  2714. protected function last() {
  2715. $i = count($this->env->children) - 1;
  2716. if (isset($this->env->children[$i]))
  2717. return $this->env->children[$i];
  2718. }
  2719. // high level parsers (they return parts of ast)
  2720. protected function mediaQueryList(&$out) {
  2721. return $this->genericList($out, "mediaQuery", ",", false);
  2722. }
  2723. protected function mediaQuery(&$out) {
  2724. $s = $this->seek();
  2725. $expressions = null;
  2726. $parts = array();
  2727. if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->mixedKeyword($mediaType)) {
  2728. $prop = array("mediaType");
  2729. if (isset($only)) $prop[] = array("keyword", "only");
  2730. if (isset($not)) $prop[] = array("keyword", "not");
  2731. $media = array("list", "", array());
  2732. foreach ((array)$mediaType as $type) {
  2733. if (is_array($type)) {
  2734. $media[2][] = $type;
  2735. } else {
  2736. $media[2][] = array("keyword", $type);
  2737. }
  2738. }
  2739. $prop[] = $media;
  2740. $parts[] = $prop;
  2741. }
  2742. if (empty($parts) || $this->literal("and")) {
  2743. $this->genericList($expressions, "mediaExpression", "and", false);
  2744. if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]);
  2745. }
  2746. $out = $parts;
  2747. return true;
  2748. }
  2749. protected function mediaExpression(&$out) {
  2750. $s = $this->seek();
  2751. $value = null;
  2752. if ($this->literal("(") &&
  2753. $this->expression($feature) &&
  2754. ($this->literal(":") && $this->expression($value) || true) &&
  2755. $this->literal(")"))
  2756. {
  2757. $out = array("mediaExp", $feature);
  2758. if ($value) $out[] = $value;
  2759. return true;
  2760. }
  2761. $this->seek($s);
  2762. return false;
  2763. }
  2764. protected function argValues(&$out) {
  2765. if ($this->genericList($list, "argValue", ",", false)) {
  2766. $out = $list[2];
  2767. return true;
  2768. }
  2769. return false;
  2770. }
  2771. protected function argValue(&$out) {
  2772. $s = $this->seek();
  2773. $keyword = null;
  2774. if (!$this->variable($keyword) || !$this->literal(":")) {
  2775. $this->seek($s);
  2776. $keyword = null;
  2777. }
  2778. if ($this->genericList($value, "expression")) {
  2779. $out = array($keyword, $value, false);
  2780. $s = $this->seek();
  2781. if ($this->literal("...")) {
  2782. $out[2] = true;
  2783. } else {
  2784. $this->seek($s);
  2785. }
  2786. return true;
  2787. }
  2788. return false;
  2789. }
  2790. /**
  2791. * Parse list
  2792. *
  2793. * @param string $out
  2794. *
  2795. * @return boolean
  2796. */
  2797. public function valueList(&$out)
  2798. {
  2799. return $this->genericList($out, 'spaceList', ',');
  2800. }
  2801. protected function spaceList(&$out)
  2802. {
  2803. return $this->genericList($out, 'expression');
  2804. }
  2805. protected function genericList(&$out, $parseItem, $delim="", $flatten=true) {
  2806. $s = $this->seek();
  2807. $items = array();
  2808. while ($this->$parseItem($value)) {
  2809. $items[] = $value;
  2810. if ($delim) {
  2811. if (!$this->literal($delim)) break;
  2812. }
  2813. }
  2814. if (count($items) == 0) {
  2815. $this->seek($s);
  2816. return false;
  2817. }
  2818. if ($flatten && count($items) == 1) {
  2819. $out = $items[0];
  2820. } else {
  2821. $out = array("list", $delim, $items);
  2822. }
  2823. return true;
  2824. }
  2825. protected function expression(&$out) {
  2826. $s = $this->seek();
  2827. if ($this->literal("(")) {
  2828. if ($this->literal(")")) {
  2829. $out = array("list", "", array());
  2830. return true;
  2831. }
  2832. if ($this->valueList($out) && $this->literal(')') && $out[0] == "list") {
  2833. return true;
  2834. }
  2835. $this->seek($s);
  2836. }
  2837. if ($this->value($lhs)) {
  2838. $out = $this->expHelper($lhs, 0);
  2839. return true;
  2840. }
  2841. return false;
  2842. }
  2843. protected function expHelper($lhs, $minP) {
  2844. $opstr = self::$operatorStr;
  2845. $ss = $this->seek();
  2846. $whiteBefore = isset($this->buffer[$this->count - 1]) &&
  2847. ctype_space($this->buffer[$this->count - 1]);
  2848. while ($this->match($opstr, $m) && self::$precedence[$m[1]] >= $minP) {
  2849. $whiteAfter = isset($this->buffer[$this->count - 1]) &&
  2850. ctype_space($this->buffer[$this->count - 1]);
  2851. $op = $m[1];
  2852. // don't turn negative numbers into expressions
  2853. if ($op == "-" && $whiteBefore) {
  2854. if (!$whiteAfter) break;
  2855. }
  2856. if (!$this->value($rhs)) break;
  2857. // peek and see if rhs belongs to next operator
  2858. if ($this->peek($opstr, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) {
  2859. $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
  2860. }
  2861. $lhs = array("exp", $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter);
  2862. $ss = $this->seek();
  2863. $whiteBefore = isset($this->buffer[$this->count - 1]) &&
  2864. ctype_space($this->buffer[$this->count - 1]);
  2865. }
  2866. $this->seek($ss);
  2867. return $lhs;
  2868. }
  2869. protected function value(&$out) {
  2870. $s = $this->seek();
  2871. if ($this->literal("not", false) && $this->whitespace() && $this->value($inner)) {
  2872. $out = array("unary", "not", $inner, $this->inParens);
  2873. return true;
  2874. } else {
  2875. $this->seek($s);
  2876. }
  2877. if ($this->literal("+") && $this->value($inner)) {
  2878. $out = array("unary", "+", $inner, $this->inParens);
  2879. return true;
  2880. } else {
  2881. $this->seek($s);
  2882. }
  2883. // negation
  2884. if ($this->literal("-", false) &&
  2885. ($this->variable($inner) ||
  2886. $this->unit($inner) ||
  2887. $this->parenValue($inner)))
  2888. {
  2889. $out = array("unary", "-", $inner, $this->inParens);
  2890. return true;
  2891. } else {
  2892. $this->seek($s);
  2893. }
  2894. if ($this->parenValue($out)) return true;
  2895. if ($this->interpolation($out)) return true;
  2896. if ($this->variable($out)) return true;
  2897. if ($this->color($out)) return true;
  2898. if ($this->unit($out)) return true;
  2899. if ($this->string($out)) return true;
  2900. if ($this->func($out)) return true;
  2901. if ($this->progid($out)) return true;
  2902. if ($this->keyword($keyword)) {
  2903. if ($keyword == "null") {
  2904. $out = array("null");
  2905. } else {
  2906. $out = array("keyword", $keyword);
  2907. }
  2908. return true;
  2909. }
  2910. return false;
  2911. }
  2912. // value wrappen in parentheses
  2913. protected function parenValue(&$out) {
  2914. $s = $this->seek();
  2915. $inParens = $this->inParens;
  2916. if ($this->literal("(") &&
  2917. ($this->inParens = true) && $this->expression($exp) &&
  2918. $this->literal(")"))
  2919. {
  2920. $out = $exp;
  2921. $this->inParens = $inParens;
  2922. return true;
  2923. } else {
  2924. $this->inParens = $inParens;
  2925. $this->seek($s);
  2926. }
  2927. return false;
  2928. }
  2929. protected function progid(&$out) {
  2930. $s = $this->seek();
  2931. if ($this->literal("progid:", false) &&
  2932. $this->openString("(", $fn) &&
  2933. $this->literal("("))
  2934. {
  2935. $this->openString(")", $args, "(");
  2936. if ($this->literal(")")) {
  2937. $out = array("string", "", array(
  2938. "progid:", $fn, "(", $args, ")"
  2939. ));
  2940. return true;
  2941. }
  2942. }
  2943. $this->seek($s);
  2944. return false;
  2945. }
  2946. protected function func(&$func) {
  2947. $s = $this->seek();
  2948. if ($this->keyword($name, false) &&
  2949. $this->literal("("))
  2950. {
  2951. if ($name == "alpha" && $this->argumentList($args)) {
  2952. $func = array("function", $name, array("string", "", $args));
  2953. return true;
  2954. }
  2955. if ($name != "expression" && !preg_match("/^(-[a-z]+-)?calc$/", $name)) {
  2956. $ss = $this->seek();
  2957. if ($this->argValues($args) && $this->literal(")")) {
  2958. $func = array("fncall", $name, $args);
  2959. return true;
  2960. }
  2961. $this->seek($ss);
  2962. }
  2963. if (($this->openString(")", $str, "(") || true ) &&
  2964. $this->literal(")"))
  2965. {
  2966. $args = array();
  2967. if (!empty($str)) {
  2968. $args[] = array(null, array("string", "", array($str)));
  2969. }
  2970. $func = array("fncall", $name, $args);
  2971. return true;
  2972. }
  2973. }
  2974. $this->seek($s);
  2975. return false;
  2976. }
  2977. protected function argumentList(&$out) {
  2978. $s = $this->seek();
  2979. $this->literal("(");
  2980. $args = array();
  2981. while ($this->keyword($var)) {
  2982. $ss = $this->seek();
  2983. if ($this->literal("=") && $this->expression($exp)) {
  2984. $args[] = array("string", "", array($var."="));
  2985. $arg = $exp;
  2986. } else {
  2987. break;
  2988. }
  2989. $args[] = $arg;
  2990. if (!$this->literal(",")) break;
  2991. $args[] = array("string", "", array(", "));
  2992. }
  2993. if (!$this->literal(")") || !count($args)) {
  2994. $this->seek($s);
  2995. return false;
  2996. }
  2997. $out = $args;
  2998. return true;
  2999. }
  3000. protected function argumentDef(&$out) {
  3001. $s = $this->seek();
  3002. $this->literal("(");
  3003. $args = array();
  3004. while ($this->variable($var)) {
  3005. $arg = array($var[1], null, false);
  3006. $ss = $this->seek();
  3007. if ($this->literal(":") && $this->genericList($defaultVal, "expression")) {
  3008. $arg[1] = $defaultVal;
  3009. } else {
  3010. $this->seek($ss);
  3011. }
  3012. $ss = $this->seek();
  3013. if ($this->literal("...")) {
  3014. $sss = $this->seek();
  3015. if (!$this->literal(")")) {
  3016. $this->throwParseError("... has to be after the final argument");
  3017. }
  3018. $arg[2] = true;
  3019. $this->seek($sss);
  3020. } else {
  3021. $this->seek($ss);
  3022. }
  3023. $args[] = $arg;
  3024. if (!$this->literal(",")) break;
  3025. }
  3026. if (!$this->literal(")")) {
  3027. $this->seek($s);
  3028. return false;
  3029. }
  3030. $out = $args;
  3031. return true;
  3032. }
  3033. protected function color(&$out) {
  3034. $color = array('color');
  3035. if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
  3036. if (isset($m[3])) {
  3037. $num = $m[3];
  3038. $width = 16;
  3039. } else {
  3040. $num = $m[2];
  3041. $width = 256;
  3042. }
  3043. $num = hexdec($num);
  3044. foreach (array(3,2,1) as $i) {
  3045. $t = $num % $width;
  3046. $num /= $width;
  3047. $color[$i] = $t * (256/$width) + $t * floor(16/$width);
  3048. }
  3049. $out = $color;
  3050. return true;
  3051. }
  3052. return false;
  3053. }
  3054. protected function unit(&$unit) {
  3055. if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
  3056. $unit = array("number", $m[1], empty($m[3]) ? "" : $m[3]);
  3057. return true;
  3058. }
  3059. return false;
  3060. }
  3061. protected function string(&$out) {
  3062. $s = $this->seek();
  3063. if ($this->literal('"', false)) {
  3064. $delim = '"';
  3065. } elseif ($this->literal("'", false)) {
  3066. $delim = "'";
  3067. } else {
  3068. return false;
  3069. }
  3070. $content = array();
  3071. $oldWhite = $this->eatWhiteDefault;
  3072. $this->eatWhiteDefault = false;
  3073. while ($this->matchString($m, $delim)) {
  3074. $content[] = $m[1];
  3075. if ($m[2] == "#{") {
  3076. $this->count -= strlen($m[2]);
  3077. if ($this->interpolation($inter, false)) {
  3078. $content[] = $inter;
  3079. } else {
  3080. $this->count += strlen($m[2]);
  3081. $content[] = "#{"; // ignore it
  3082. }
  3083. } elseif ($m[2] == '\\') {
  3084. $content[] = $m[2];
  3085. if ($this->literal($delim, false)) {
  3086. $content[] = $delim;
  3087. }
  3088. } else {
  3089. $this->count -= strlen($delim);
  3090. break; // delim
  3091. }
  3092. }
  3093. $this->eatWhiteDefault = $oldWhite;
  3094. if ($this->literal($delim)) {
  3095. $out = array("string", $delim, $content);
  3096. return true;
  3097. }
  3098. $this->seek($s);
  3099. return false;
  3100. }
  3101. protected function mixedKeyword(&$out) {
  3102. $s = $this->seek();
  3103. $parts = array();
  3104. $oldWhite = $this->eatWhiteDefault;
  3105. $this->eatWhiteDefault = false;
  3106. while (true) {
  3107. if ($this->keyword($key)) {
  3108. $parts[] = $key;
  3109. continue;
  3110. }
  3111. if ($this->interpolation($inter)) {
  3112. $parts[] = $inter;
  3113. continue;
  3114. }
  3115. break;
  3116. }
  3117. $this->eatWhiteDefault = $oldWhite;
  3118. if (count($parts) == 0) return false;
  3119. if ($this->eatWhiteDefault) {
  3120. $this->whitespace();
  3121. }
  3122. $out = $parts;
  3123. return true;
  3124. }
  3125. // an unbounded string stopped by $end
  3126. protected function openString($end, &$out, $nestingOpen=null) {
  3127. $oldWhite = $this->eatWhiteDefault;
  3128. $this->eatWhiteDefault = false;
  3129. $stop = array("'", '"', "#{", $end);
  3130. $stop = array_map(array($this, "preg_quote"), $stop);
  3131. $stop[] = self::$commentMulti;
  3132. $patt = '(.*?)('.implode("|", $stop).')';
  3133. $nestingLevel = 0;
  3134. $content = array();
  3135. while ($this->match($patt, $m, false)) {
  3136. if (isset($m[1]) && $m[1] !== '') {
  3137. $content[] = $m[1];
  3138. if ($nestingOpen) {
  3139. $nestingLevel += substr_count($m[1], $nestingOpen);
  3140. }
  3141. }
  3142. $tok = $m[2];
  3143. $this->count-= strlen($tok);
  3144. if ($tok == $end) {
  3145. if ($nestingLevel == 0) {
  3146. break;
  3147. } else {
  3148. $nestingLevel--;
  3149. }
  3150. }
  3151. if (($tok == "'" || $tok == '"') && $this->string($str)) {
  3152. $content[] = $str;
  3153. continue;
  3154. }
  3155. if ($tok == "#{" && $this->interpolation($inter)) {
  3156. $content[] = $inter;
  3157. continue;
  3158. }
  3159. $content[] = $tok;
  3160. $this->count+= strlen($tok);
  3161. }
  3162. $this->eatWhiteDefault = $oldWhite;
  3163. if (count($content) == 0) return false;
  3164. // trim the end
  3165. if (is_string(end($content))) {
  3166. $content[count($content) - 1] = rtrim(end($content));
  3167. }
  3168. $out = array("string", "", $content);
  3169. return true;
  3170. }
  3171. // $lookWhite: save information about whitespace before and after
  3172. protected function interpolation(&$out, $lookWhite=true) {
  3173. $oldWhite = $this->eatWhiteDefault;
  3174. $this->eatWhiteDefault = true;
  3175. $s = $this->seek();
  3176. if ($this->literal("#{") && $this->valueList($value) && $this->literal("}", false)) {
  3177. // TODO: don't error if out of bounds
  3178. if ($lookWhite) {
  3179. $left = preg_match('/\s/', $this->buffer[$s - 1]) ? " " : "";
  3180. $right = preg_match('/\s/', $this->buffer[$this->count]) ? " ": "";
  3181. } else {
  3182. $left = $right = false;
  3183. }
  3184. $out = array("interpolate", $value, $left, $right);
  3185. $this->eatWhiteDefault = $oldWhite;
  3186. if ($this->eatWhiteDefault) $this->whitespace();
  3187. return true;
  3188. }
  3189. $this->seek($s);
  3190. $this->eatWhiteDefault = $oldWhite;
  3191. return false;
  3192. }
  3193. // low level parsers
  3194. // returns an array of parts or a string
  3195. protected function propertyName(&$out) {
  3196. $s = $this->seek();
  3197. $parts = array();
  3198. $oldWhite = $this->eatWhiteDefault;
  3199. $this->eatWhiteDefault = false;
  3200. while (true) {
  3201. if ($this->interpolation($inter)) {
  3202. $parts[] = $inter;
  3203. } elseif ($this->keyword($text)) {
  3204. $parts[] = $text;
  3205. } elseif (count($parts) == 0 && $this->match('[:.#]', $m, false)) {
  3206. // css hacks
  3207. $parts[] = $m[0];
  3208. } else {
  3209. break;
  3210. }
  3211. }
  3212. $this->eatWhiteDefault = $oldWhite;
  3213. if (count($parts) == 0) return false;
  3214. // match comment hack
  3215. if (preg_match(self::$whitePattern,
  3216. $this->buffer, $m, null, $this->count))
  3217. {
  3218. if (!empty($m[0])) {
  3219. $parts[] = $m[0];
  3220. $this->count += strlen($m[0]);
  3221. }
  3222. }
  3223. $this->whitespace(); // get any extra whitespace
  3224. $out = array("string", "", $parts);
  3225. return true;
  3226. }
  3227. // comma separated list of selectors
  3228. protected function selectors(&$out) {
  3229. $s = $this->seek();
  3230. $selectors = array();
  3231. while ($this->selector($sel)) {
  3232. $selectors[] = $sel;
  3233. if (!$this->literal(",")) break;
  3234. while ($this->literal(",")); // ignore extra
  3235. }
  3236. if (count($selectors) == 0) {
  3237. $this->seek($s);
  3238. return false;
  3239. }
  3240. $out = $selectors;
  3241. return true;
  3242. }
  3243. // whitespace separated list of selectorSingle
  3244. protected function selector(&$out) {
  3245. $selector = array();
  3246. while (true) {
  3247. if ($this->match('[>+~]+', $m)) {
  3248. $selector[] = array($m[0]);
  3249. } elseif ($this->selectorSingle($part)) {
  3250. $selector[] = $part;
  3251. $this->whitespace();
  3252. } elseif ($this->match('\/[^\/]+\/', $m)) {
  3253. $selector[] = array($m[0]);
  3254. } else {
  3255. break;
  3256. }
  3257. }
  3258. if (count($selector) == 0) {
  3259. return false;
  3260. }
  3261. $out = $selector;
  3262. return true;
  3263. }
  3264. // the parts that make up
  3265. // div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
  3266. protected function selectorSingle(&$out) {
  3267. $oldWhite = $this->eatWhiteDefault;
  3268. $this->eatWhiteDefault = false;
  3269. $parts = array();
  3270. if ($this->literal("*", false)) {
  3271. $parts[] = "*";
  3272. }
  3273. while (true) {
  3274. // see if we can stop early
  3275. if ($this->match("\s*[{,]", $m)) {
  3276. $this->count--;
  3277. break;
  3278. }
  3279. $s = $this->seek();
  3280. // self
  3281. if ($this->literal("&", false)) {
  3282. $parts[] = scssc::$selfSelector;
  3283. continue;
  3284. }
  3285. if ($this->literal(".", false)) {
  3286. $parts[] = ".";
  3287. continue;
  3288. }
  3289. if ($this->literal("|", false)) {
  3290. $parts[] = "|";
  3291. continue;
  3292. }
  3293. // for keyframes
  3294. if ($this->unit($unit)) {
  3295. $parts[] = $unit;
  3296. continue;
  3297. }
  3298. if ($this->keyword($name)) {
  3299. $parts[] = $name;
  3300. continue;
  3301. }
  3302. if ($this->interpolation($inter)) {
  3303. $parts[] = $inter;
  3304. continue;
  3305. }
  3306. if ($this->literal('%', false) && $this->placeholder($placeholder)) {
  3307. $parts[] = '%';
  3308. $parts[] = $placeholder;
  3309. continue;
  3310. }
  3311. if ($this->literal("#", false)) {
  3312. $parts[] = "#";
  3313. continue;
  3314. }
  3315. // a pseudo selector
  3316. if ($this->match("::?", $m) && $this->mixedKeyword($nameParts)) {
  3317. $parts[] = $m[0];
  3318. foreach ($nameParts as $sub) {
  3319. $parts[] = $sub;
  3320. }
  3321. $ss = $this->seek();
  3322. if ($this->literal("(") &&
  3323. ($this->openString(")", $str, "(") || true ) &&
  3324. $this->literal(")"))
  3325. {
  3326. $parts[] = "(";
  3327. if (!empty($str)) $parts[] = $str;
  3328. $parts[] = ")";
  3329. } else {
  3330. $this->seek($ss);
  3331. }
  3332. continue;
  3333. } else {
  3334. $this->seek($s);
  3335. }
  3336. // attribute selector
  3337. // TODO: replace with open string?
  3338. if ($this->literal("[", false)) {
  3339. $attrParts = array("[");
  3340. // keyword, string, operator
  3341. while (true) {
  3342. if ($this->literal("]", false)) {
  3343. $this->count--;
  3344. break; // get out early
  3345. }
  3346. if ($this->match('\s+', $m)) {
  3347. $attrParts[] = " ";
  3348. continue;
  3349. }
  3350. if ($this->string($str)) {
  3351. $attrParts[] = $str;
  3352. continue;
  3353. }
  3354. if ($this->keyword($word)) {
  3355. $attrParts[] = $word;
  3356. continue;
  3357. }
  3358. if ($this->interpolation($inter, false)) {
  3359. $attrParts[] = $inter;
  3360. continue;
  3361. }
  3362. // operator, handles attr namespace too
  3363. if ($this->match('[|-~\$\*\^=]+', $m)) {
  3364. $attrParts[] = $m[0];
  3365. continue;
  3366. }
  3367. break;
  3368. }
  3369. if ($this->literal("]", false)) {
  3370. $attrParts[] = "]";
  3371. foreach ($attrParts as $part) {
  3372. $parts[] = $part;
  3373. }
  3374. continue;
  3375. }
  3376. $this->seek($s);
  3377. // should just break here?
  3378. }
  3379. break;
  3380. }
  3381. $this->eatWhiteDefault = $oldWhite;
  3382. if (count($parts) == 0) return false;
  3383. $out = $parts;
  3384. return true;
  3385. }
  3386. protected function variable(&$out) {
  3387. $s = $this->seek();
  3388. if ($this->literal("$", false) && $this->keyword($name)) {
  3389. $out = array("var", $name);
  3390. return true;
  3391. }
  3392. $this->seek($s);
  3393. return false;
  3394. }
  3395. protected function keyword(&$word, $eatWhitespace = null) {
  3396. if ($this->match('([\w_\-\*!"\'\\\\][\w\-_"\'\\\\]*)',
  3397. $m, $eatWhitespace))
  3398. {
  3399. $word = $m[1];
  3400. return true;
  3401. }
  3402. return false;
  3403. }
  3404. protected function placeholder(&$placeholder) {
  3405. if ($this->match('([\w\-_]+)', $m)) {
  3406. $placeholder = $m[1];
  3407. return true;
  3408. }
  3409. return false;
  3410. }
  3411. // consume an end of statement delimiter
  3412. protected function end() {
  3413. if ($this->literal(';')) {
  3414. return true;
  3415. } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
  3416. // if there is end of file or a closing block next then we don't need a ;
  3417. return true;
  3418. }
  3419. return false;
  3420. }
  3421. // advance counter to next occurrence of $what
  3422. // $until - don't include $what in advance
  3423. // $allowNewline, if string, will be used as valid char set
  3424. protected function to($what, &$out, $until = false, $allowNewline = false) {
  3425. if (is_string($allowNewline)) {
  3426. $validChars = $allowNewline;
  3427. } else {
  3428. $validChars = $allowNewline ? "." : "[^\n]";
  3429. }
  3430. if (!$this->match('('.$validChars.'*?)'.$this->preg_quote($what), $m, !$until)) return false;
  3431. if ($until) $this->count -= strlen($what); // give back $what
  3432. $out = $m[1];
  3433. return true;
  3434. }
  3435. public function throwParseError($msg = "parse error", $count = null) {
  3436. $count = !isset($count) ? $this->count : $count;
  3437. $line = $this->getLineNo($count);
  3438. if (!empty($this->sourceName)) {
  3439. $loc = "$this->sourceName on line $line";
  3440. } else {
  3441. $loc = "line: $line";
  3442. }
  3443. if ($this->peek("(.*?)(\n|$)", $m, $count)) {
  3444. throw new Exception("$msg: failed at `$m[1]` $loc");
  3445. } else {
  3446. throw new Exception("$msg: $loc");
  3447. }
  3448. }
  3449. public function getLineNo($pos) {
  3450. return 1 + substr_count(substr($this->buffer, 0, $pos), "\n");
  3451. }
  3452. /**
  3453. * Match string looking for either ending delim, escape, or string interpolation
  3454. *
  3455. * {@internal This is a workaround for preg_match's 250K string match limit. }}
  3456. *
  3457. * @param array $m Matches (passed by reference)
  3458. * @param string $delim Delimeter
  3459. *
  3460. * @return boolean True if match; false otherwise
  3461. */
  3462. protected function matchString(&$m, $delim) {
  3463. $token = null;
  3464. $end = strpos($this->buffer, "\n", $this->count);
  3465. if ($end === false || $this->buffer[$end - 1] == '\\' || $this->buffer[$end - 2] == '\\' && $this->buffer[$end - 1] == "\r") {
  3466. $end = strlen($this->buffer);
  3467. }
  3468. // look for either ending delim, escape, or string interpolation
  3469. foreach (array('#{', '\\', $delim) as $lookahead) {
  3470. $pos = strpos($this->buffer, $lookahead, $this->count);
  3471. if ($pos !== false && $pos < $end) {
  3472. $end = $pos;
  3473. $token = $lookahead;
  3474. }
  3475. }
  3476. if (!isset($token)) {
  3477. return false;
  3478. }
  3479. $match = substr($this->buffer, $this->count, $end - $this->count);
  3480. $m = array(
  3481. $match . $token,
  3482. $match,
  3483. $token
  3484. );
  3485. $this->count = $end + strlen($token);
  3486. return true;
  3487. }
  3488. // try to match something on head of buffer
  3489. protected function match($regex, &$out, $eatWhitespace = null) {
  3490. if (!isset($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault;
  3491. $r = '/'.$regex.'/Ais';
  3492. if (preg_match($r, $this->buffer, $out, null, $this->count)) {
  3493. $this->count += strlen($out[0]);
  3494. if ($eatWhitespace) $this->whitespace();
  3495. return true;
  3496. }
  3497. return false;
  3498. }
  3499. // match some whitespace
  3500. protected function whitespace() {
  3501. $gotWhite = false;
  3502. while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
  3503. if ($this->insertComments) {
  3504. if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
  3505. $this->append(array("comment", $m[1]));
  3506. $this->commentsSeen[$this->count] = true;
  3507. }
  3508. }
  3509. $this->count += strlen($m[0]);
  3510. $gotWhite = true;
  3511. }
  3512. return $gotWhite;
  3513. }
  3514. protected function peek($regex, &$out, $from=null) {
  3515. if (!isset($from)) $from = $this->count;
  3516. $r = '/'.$regex.'/Ais';
  3517. $result = preg_match($r, $this->buffer, $out, null, $from);
  3518. return $result;
  3519. }
  3520. protected function seek($where = null) {
  3521. if ($where === null) return $this->count;
  3522. else $this->count = $where;
  3523. return true;
  3524. }
  3525. static function preg_quote($what) {
  3526. return preg_quote($what, '/');
  3527. }
  3528. protected function show() {
  3529. if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
  3530. return $m[1];
  3531. }
  3532. return "";
  3533. }
  3534. // turn list of length 1 into value type
  3535. protected function flattenList($value) {
  3536. if ($value[0] == "list" && count($value[2]) == 1) {
  3537. return $this->flattenList($value[2][0]);
  3538. }
  3539. return $value;
  3540. }
  3541. }
  3542. /**
  3543. * SCSS base formatter
  3544. *
  3545. * @author Leaf Corcoran <leafot@gmail.com>
  3546. */
  3547. class scss_formatter {
  3548. public $indentChar = " ";
  3549. public $break = "\n";
  3550. public $open = " {";
  3551. public $close = "}";
  3552. public $tagSeparator = ", ";
  3553. public $assignSeparator = ": ";
  3554. public function __construct() {
  3555. $this->indentLevel = 0;
  3556. }
  3557. public function indentStr($n = 0) {
  3558. return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
  3559. }
  3560. public function property($name, $value) {
  3561. return $name . $this->assignSeparator . $value . ";";
  3562. }
  3563. protected function block($block) {
  3564. if (empty($block->lines) && empty($block->children)) return;
  3565. $inner = $pre = $this->indentStr();
  3566. if (!empty($block->selectors)) {
  3567. echo $pre .
  3568. implode($this->tagSeparator, $block->selectors) .
  3569. $this->open . $this->break;
  3570. $this->indentLevel++;
  3571. $inner = $this->indentStr();
  3572. }
  3573. if (!empty($block->lines)) {
  3574. $glue = $this->break.$inner;
  3575. echo $inner . implode($glue, $block->lines);
  3576. if (!empty($block->children)) {
  3577. echo $this->break;
  3578. }
  3579. }
  3580. foreach ($block->children as $child) {
  3581. $this->block($child);
  3582. }
  3583. if (!empty($block->selectors)) {
  3584. $this->indentLevel--;
  3585. if (empty($block->children)) echo $this->break;
  3586. echo $pre . $this->close . $this->break;
  3587. }
  3588. }
  3589. public function format($block) {
  3590. ob_start();
  3591. $this->block($block);
  3592. $out = ob_get_clean();
  3593. return $out;
  3594. }
  3595. }
  3596. /**
  3597. * SCSS nested formatter
  3598. *
  3599. * @author Leaf Corcoran <leafot@gmail.com>
  3600. */
  3601. class scss_formatter_nested extends scss_formatter {
  3602. public $close = " }";
  3603. // adjust the depths of all children, depth first
  3604. public function adjustAllChildren($block) {
  3605. // flatten empty nested blocks
  3606. $children = array();
  3607. foreach ($block->children as $i => $child) {
  3608. if (empty($child->lines) && empty($child->children)) {
  3609. if (isset($block->children[$i + 1])) {
  3610. $block->children[$i + 1]->depth = $child->depth;
  3611. }
  3612. continue;
  3613. }
  3614. $children[] = $child;
  3615. }
  3616. $count = count($children);
  3617. for ($i = 0; $i < $count; $i++) {
  3618. $depth = $children[$i]->depth;
  3619. $j = $i + 1;
  3620. if (isset($children[$j]) && $depth < $children[$j]->depth) {
  3621. $childDepth = $children[$j]->depth;
  3622. for (; $j < $count; $j++) {
  3623. if ($depth < $children[$j]->depth && $childDepth >= $children[$j]->depth) {
  3624. $children[$j]->depth = $depth + 1;
  3625. }
  3626. }
  3627. }
  3628. }
  3629. $block->children = $children;
  3630. // make relative to parent
  3631. foreach ($block->children as $child) {
  3632. $this->adjustAllChildren($child);
  3633. $child->depth = $child->depth - $block->depth;
  3634. }
  3635. }
  3636. protected function block($block) {
  3637. if ($block->type == "root") {
  3638. $this->adjustAllChildren($block);
  3639. }
  3640. $inner = $pre = $this->indentStr($block->depth - 1);
  3641. if (!empty($block->selectors)) {
  3642. echo $pre .
  3643. implode($this->tagSeparator, $block->selectors) .
  3644. $this->open . $this->break;
  3645. $this->indentLevel++;
  3646. $inner = $this->indentStr($block->depth - 1);
  3647. }
  3648. if (!empty($block->lines)) {
  3649. $glue = $this->break.$inner;
  3650. echo $inner . implode($glue, $block->lines);
  3651. if (!empty($block->children)) echo $this->break;
  3652. }
  3653. foreach ($block->children as $i => $child) {
  3654. // echo "*** block: ".$block->depth." child: ".$child->depth."\n";
  3655. $this->block($child);
  3656. if ($i < count($block->children) - 1) {
  3657. echo $this->break;
  3658. if (isset($block->children[$i + 1])) {
  3659. $next = $block->children[$i + 1];
  3660. if ($next->depth == max($block->depth, 1) && $child->depth >= $next->depth) {
  3661. echo $this->break;
  3662. }
  3663. }
  3664. }
  3665. }
  3666. if (!empty($block->selectors)) {
  3667. $this->indentLevel--;
  3668. echo $this->close;
  3669. }
  3670. if ($block->type == "root") {
  3671. echo $this->break;
  3672. }
  3673. }
  3674. }
  3675. /**
  3676. * SCSS compressed formatter
  3677. *
  3678. * @author Leaf Corcoran <leafot@gmail.com>
  3679. */
  3680. class scss_formatter_compressed extends scss_formatter {
  3681. public $open = "{";
  3682. public $tagSeparator = ",";
  3683. public $assignSeparator = ":";
  3684. public $break = "";
  3685. public function indentStr($n = 0) {
  3686. return "";
  3687. }
  3688. }
  3689. /**
  3690. * SCSS server
  3691. *
  3692. * @author Leaf Corcoran <leafot@gmail.com>
  3693. */
  3694. class scss_server {
  3695. /**
  3696. * Join path components
  3697. *
  3698. * @param string $left Path component, left of the directory separator
  3699. * @param string $right Path component, right of the directory separator
  3700. *
  3701. * @return string
  3702. */
  3703. protected function join($left, $right) {
  3704. return rtrim($left, '/\\') . DIRECTORY_SEPARATOR . ltrim($right, '/\\');
  3705. }
  3706. /**
  3707. * Get name of requested .scss file
  3708. *
  3709. * @return string|null
  3710. */
  3711. protected function inputName() {
  3712. switch (true) {
  3713. case isset($_GET['p']):
  3714. return $_GET['p'];
  3715. case isset($_SERVER['PATH_INFO']):
  3716. return $_SERVER['PATH_INFO'];
  3717. case isset($_SERVER['DOCUMENT_URI']):
  3718. return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME']));
  3719. }
  3720. }
  3721. /**
  3722. * Get path to requested .scss file
  3723. *
  3724. * @return string
  3725. */
  3726. protected function findInput() {
  3727. if (($input = $this->inputName())
  3728. && strpos($input, '..') === false
  3729. && substr($input, -5) === '.scss'
  3730. ) {
  3731. $name = $this->join($this->dir, $input);
  3732. if (is_file($name) && is_readable($name)) {
  3733. return $name;
  3734. }
  3735. }
  3736. return false;
  3737. }
  3738. /**
  3739. * Get path to cached .css file
  3740. *
  3741. * @return string
  3742. */
  3743. protected function cacheName($fname) {
  3744. return $this->join($this->cacheDir, md5($fname) . '.css');
  3745. }
  3746. /**
  3747. * Get path to cached imports
  3748. *
  3749. * @return string
  3750. */
  3751. protected function importsCacheName($out) {
  3752. return $out . '.imports';
  3753. }
  3754. /**
  3755. * Determine whether .scss file needs to be re-compiled.
  3756. *
  3757. * @param string $in Input path
  3758. * @param string $out Output path
  3759. *
  3760. * @return boolean True if compile required.
  3761. */
  3762. protected function needsCompile($in, $out) {
  3763. if (!is_file($out)) return true;
  3764. $mtime = filemtime($out);
  3765. if (filemtime($in) > $mtime) return true;
  3766. // look for modified imports
  3767. $icache = $this->importsCacheName($out);
  3768. if (is_readable($icache)) {
  3769. $imports = unserialize(file_get_contents($icache));
  3770. foreach ($imports as $import) {
  3771. if (filemtime($import) > $mtime) return true;
  3772. }
  3773. }
  3774. return false;
  3775. }
  3776. /**
  3777. * Get If-Modified-Since header from client request
  3778. *
  3779. * @return string
  3780. */
  3781. protected function getModifiedSinceHeader()
  3782. {
  3783. $modifiedSince = '';
  3784. if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
  3785. $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
  3786. if (false !== ($semicolonPos = strpos($modifiedSince, ';'))) {
  3787. $modifiedSince = substr($modifiedSince, 0, $semicolonPos);
  3788. }
  3789. }
  3790. return $modifiedSince;
  3791. }
  3792. /**
  3793. * Compile .scss file
  3794. *
  3795. * @param string $in Input path (.scss)
  3796. * @param string $out Output path (.css)
  3797. *
  3798. * @return string
  3799. */
  3800. protected function compile($in, $out) {
  3801. $start = microtime(true);
  3802. $css = $this->scss->compile(file_get_contents($in), $in);
  3803. $elapsed = round((microtime(true) - $start), 4);
  3804. $v = scssc::$VERSION;
  3805. $t = @date('r');
  3806. $css = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css;
  3807. file_put_contents($out, $css);
  3808. file_put_contents($this->importsCacheName($out),
  3809. serialize($this->scss->getParsedFiles()));
  3810. return $css;
  3811. }
  3812. /**
  3813. * Compile requested scss and serve css. Outputs HTTP response.
  3814. *
  3815. * @param string $salt Prefix a string to the filename for creating the cache name hash
  3816. */
  3817. public function serve($salt = '') {
  3818. $protocol = isset($_SERVER['SERVER_PROTOCOL'])
  3819. ? $_SERVER['SERVER_PROTOCOL']
  3820. : 'HTTP/1.0';
  3821. if ($input = $this->findInput()) {
  3822. $output = $this->cacheName($salt . $input);
  3823. if ($this->needsCompile($input, $output)) {
  3824. try {
  3825. $css = $this->compile($input, $output);
  3826. $lastModified = gmdate('D, d M Y H:i:s', filemtime($output)) . ' GMT';
  3827. header('Last-Modified: ' . $lastModified);
  3828. header('Content-type: text/css');
  3829. echo $css;
  3830. return;
  3831. } catch (Exception $e) {
  3832. header($protocol . ' 500 Internal Server Error');
  3833. header('Content-type: text/plain');
  3834. echo 'Parse error: ' . $e->getMessage() . "\n";
  3835. }
  3836. }
  3837. header('X-SCSS-Cache: true');
  3838. header('Content-type: text/css');
  3839. $modifiedSince = $this->getModifiedSinceHeader();
  3840. $mtime = filemtime($output);
  3841. if (@strtotime($modifiedSince) === $mtime) {
  3842. header($protocol . ' 304 Not Modified');
  3843. return;
  3844. }
  3845. $lastModified = gmdate('D, d M Y H:i:s', $mtime) . ' GMT';
  3846. header('Last-Modified: ' . $lastModified);
  3847. echo file_get_contents($output);
  3848. return;
  3849. }
  3850. header($protocol . ' 404 Not Found');
  3851. header('Content-type: text/plain');
  3852. $v = scssc::$VERSION;
  3853. echo "/* INPUT NOT FOUND scss $v */\n";
  3854. }
  3855. /**
  3856. * Constructor
  3857. *
  3858. * @param string $dir Root directory to .scss files
  3859. * @param string $cacheDir Cache directory
  3860. * @param \scssc|null $scss SCSS compiler instance
  3861. */
  3862. public function __construct($dir, $cacheDir=null, $scss=null) {
  3863. $this->dir = $dir;
  3864. if (!isset($cacheDir)) {
  3865. $cacheDir = $this->join($dir, 'scss_cache');
  3866. }
  3867. $this->cacheDir = $cacheDir;
  3868. if (!is_dir($this->cacheDir)) mkdir($this->cacheDir, 0755, true);
  3869. if (!isset($scss)) {
  3870. $scss = new scssc();
  3871. $scss->setImportPaths($this->dir);
  3872. }
  3873. $this->scss = $scss;
  3874. }
  3875. /**
  3876. * Helper method to serve compiled scss
  3877. *
  3878. * @param string $path Root path
  3879. */
  3880. static public function serveFrom($path) {
  3881. $server = new self($path);
  3882. $server->serve();
  3883. }
  3884. }