12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989 |
- <?php
- /**
- * SCSSPHP
- *
- * @copyright 2012-2015 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://leafo.github.io/scssphp
- */
- namespace Leafo\ScssPhp;
- use Leafo\ScssPhp\Base\Range;
- use Leafo\ScssPhp\Block;
- use Leafo\ScssPhp\Colors;
- use Leafo\ScssPhp\Compiler\Environment;
- use Leafo\ScssPhp\Exception\CompilerException;
- use Leafo\ScssPhp\Formatter\OutputBlock;
- use Leafo\ScssPhp\Node;
- use Leafo\ScssPhp\Type;
- use Leafo\ScssPhp\Parser;
- use Leafo\ScssPhp\Util;
- /**
- * The scss compiler and parser.
- *
- * Converting SCSS to CSS is a three stage process. The incoming file is parsed
- * by `Parser` into a syntax tree, then it is compiled into another tree
- * representing the CSS structure by `Compiler`. The CSS tree is fed into a
- * formatter, like `Formatter` which then outputs CSS as a string.
- *
- * During the first compile, all values are *reduced*, which means that their
- * types are brought to the lowest form before being dump as strings. This
- * handles math equations, variable dereferences, and the like.
- *
- * The `compile` function of `Compiler` is the entry point.
- *
- * In summary:
- *
- * The `Compiler` class creates an instance of the parser, feeds it SCSS code,
- * then transforms the resulting tree to a CSS tree. This class also holds the
- * evaluation context, such as all available mixins and variables at any given
- * time.
- *
- * The `Parser` class is only concerned with parsing its input.
- *
- * The `Formatter` takes a CSS tree, and dumps it to a formatted string,
- * handling things like indentation.
- */
- /**
- * SCSS compiler
- *
- * @author Leaf Corcoran <leafot@gmail.com>
- */
- class Compiler
- {
- const LINE_COMMENTS = 1;
- const DEBUG_INFO = 2;
- const WITH_RULE = 1;
- const WITH_MEDIA = 2;
- const WITH_SUPPORTS = 4;
- const WITH_ALL = 7;
- /**
- * @var array
- */
- protected static $operatorNames = array('+' => 'add', '-' => 'sub', '*' => 'mul', '/' => 'div', '%' => 'mod', '==' => 'eq', '!=' => 'neq', '<' => 'lt', '>' => 'gt', '<=' => 'lte', '>=' => 'gte', '<=>' => 'cmp');
- /**
- * @var array
- */
- protected static $namespaces = array('special' => '%', 'mixin' => '@', 'function' => '^');
- public static $true = array(Type::T_KEYWORD, 'true');
- public static $false = array(Type::T_KEYWORD, 'false');
- public static $null = array(Type::T_NULL);
- public static $nullString = array(Type::T_STRING, '', array());
- public static $defaultValue = array(Type::T_KEYWORD, '');
- public static $selfSelector = array(Type::T_SELF);
- public static $emptyList = array(Type::T_LIST, '', array());
- public static $emptyMap = array(Type::T_MAP, array(), array());
- public static $emptyString = array(Type::T_STRING, '"', array());
- public static $with = array(Type::T_KEYWORD, 'with');
- public static $without = array(Type::T_KEYWORD, 'without');
- protected $importPaths = array('');
- protected $importCache = array();
- protected $importedFiles = array();
- protected $userFunctions = array();
- protected $registeredVars = array();
- protected $registeredFeatures = array('extend-selector-pseudoclass' => false, 'at-error' => true, 'units-level-3' => false, 'global-variable-shadowing' => false);
- protected $encoding = null;
- protected $lineNumberStyle = null;
- protected $formatter = 'Leafo\\ScssPhp\\Formatter\\Nested';
- protected $rootEnv;
- protected $rootBlock;
- protected $env;
- protected $scope;
- protected $storeEnv;
- protected $charsetSeen;
- protected $sourceNames;
- private $indentLevel;
- private $commentsSeen;
- private $extends;
- private $extendsMap;
- private $parsedFiles;
- private $parser;
- private $sourceIndex;
- private $sourceLine;
- private $sourceColumn;
- private $stderr;
- private $shouldEvaluate;
- private $ignoreErrors;
- /**
- * Constructor
- */
- public function __construct()
- {
- $this->parsedFiles = array();
- $this->sourceNames = array();
- }
- /**
- * Compile scss
- *
- * @api
- *
- * @param string $code
- * @param string $path
- *
- * @return string
- */
- public function compile($code, $path = null)
- {
- $locale = setlocale(LC_NUMERIC, 0);
- setlocale(LC_NUMERIC, 'C');
- $this->indentLevel = -1;
- $this->commentsSeen = array();
- $this->extends = array();
- $this->extendsMap = array();
- $this->sourceIndex = null;
- $this->sourceLine = null;
- $this->sourceColumn = null;
- $this->env = null;
- $this->scope = null;
- $this->storeEnv = null;
- $this->charsetSeen = null;
- $this->shouldEvaluate = null;
- $this->stderr = fopen('php://stderr', 'w');
- $this->parser = $this->parserFactory($path);
- $tree = $this->parser->parse($code);
- $this->parser = null;
- $this->formatter = new $this->formatter();
- $this->rootBlock = null;
- $this->rootEnv = $this->pushEnv($tree);
- $this->injectVariables($this->registeredVars);
- $this->compileRoot($tree);
- $this->popEnv();
- $out = $this->formatter->format($this->scope);
- setlocale(LC_NUMERIC, $locale);
- return $out;
- }
- /**
- * Instantiate parser
- *
- * @param string $path
- *
- * @return \Leafo\ScssPhp\Parser
- */
- protected function parserFactory($path)
- {
- $parser = new Parser($path, count($this->sourceNames), $this->encoding);
- $this->sourceNames[] = $path;
- $this->addParsedFile($path);
- return $parser;
- }
- /**
- * Is self extend?
- *
- * @param array $target
- * @param array $origin
- *
- * @return boolean
- */
- protected function isSelfExtend($target, $origin)
- {
- foreach ($origin as $sel) {
- if (in_array($target, $sel)) {
- return true;
- }
- }
- return false;
- }
- /**
- * Push extends
- *
- * @param array $target
- * @param array $origin
- * @param \stdClass $block
- */
- protected function pushExtends($target, $origin, $block)
- {
- if ($this->isSelfExtend($target, $origin)) {
- return;
- }
- $i = count($this->extends);
- $this->extends[] = array($target, $origin, $block);
- foreach ($target as $part) {
- if (isset($this->extendsMap[$part])) {
- $this->extendsMap[$part][] = $i;
- } else {
- $this->extendsMap[$part] = array($i);
- }
- }
- }
- /**
- * Make output block
- *
- * @param string $type
- * @param array $selectors
- *
- * @return \Leafo\ScssPhp\Formatter\OutputBlock
- */
- protected function makeOutputBlock($type, $selectors = null)
- {
- $out = new OutputBlock();
- $out->type = $type;
- $out->lines = array();
- $out->children = array();
- $out->parent = $this->scope;
- $out->selectors = $selectors;
- $out->depth = $this->env->depth;
- return $out;
- }
- /**
- * Compile root
- *
- * @param \Leafo\ScssPhp\Block $rootBlock
- */
- protected function compileRoot(Block $rootBlock)
- {
- $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT);
- $this->compileChildrenNoReturn($rootBlock->children, $this->scope);
- $this->flattenSelectors($this->scope);
- $this->missingSelectors();
- }
- /**
- * Report missing selectors
- */
- protected function missingSelectors()
- {
- foreach ($this->extends as $extend) {
- if (isset($extend[3])) {
- continue;
- }
- list($target, $origin, $block) = $extend;
- // ignore if !optional
- if ($block[2]) {
- continue;
- }
- $target = implode(' ', $target);
- $origin = $this->collapseSelectors($origin);
- $this->sourceLine = $block[Parser::SOURCE_LINE];
- $this->throwError("\"{$origin}\" failed to @extend \"{$target}\". The selector \"{$target}\" was not found.");
- }
- }
- /**
- * Flatten selectors
- *
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
- * @param string $parentKey
- */
- protected function flattenSelectors(OutputBlock $block, $parentKey = null)
- {
- if ($block->selectors) {
- $selectors = array();
- foreach ($block->selectors as $s) {
- $selectors[] = $s;
- if (!is_array($s)) {
- continue;
- }
- // check extends
- if (!empty($this->extendsMap)) {
- $this->matchExtends($s, $selectors);
- // remove duplicates
- array_walk($selectors, function (&$value) {
- $value = serialize($value);
- });
- $selectors = array_unique($selectors);
- array_walk($selectors, function (&$value) {
- $value = unserialize($value);
- });
- }
- }
- $block->selectors = array();
- $placeholderSelector = false;
- foreach ($selectors as $selector) {
- if ($this->hasSelectorPlaceholder($selector)) {
- $placeholderSelector = true;
- continue;
- }
- $block->selectors[] = $this->compileSelector($selector);
- }
- if ($placeholderSelector && 0 === count($block->selectors) && null !== $parentKey) {
- unset($block->parent->children[$parentKey]);
- return;
- }
- }
- foreach ($block->children as $key => $child) {
- $this->flattenSelectors($child, $key);
- }
- }
- /**
- * Match extends
- *
- * @param array $selector
- * @param array $out
- * @param integer $from
- * @param boolean $initial
- */
- protected function matchExtends($selector, &$out, $from = 0, $initial = true)
- {
- foreach ($selector as $i => $part) {
- if ($i < $from) {
- continue;
- }
- if ($this->matchExtendsSingle($part, $origin)) {
- $before = array_slice($selector, 0, $i);
- $after = array_slice($selector, $i + 1);
- $s = count($before);
- foreach ($origin as $new) {
- $k = 0;
- // remove shared parts
- if ($initial) {
- while ($k < $s && isset($new[$k]) && $before[$k] === $new[$k]) {
- $k++;
- }
- }
- $result = array_merge($before, $k > 0 ? array_slice($new, $k) : $new, $after);
- if ($result === $selector) {
- continue;
- }
- $out[] = $result;
- // recursively check for more matches
- $this->matchExtends($result, $out, $i, false);
- // selector sequence merging
- if (!empty($before) && count($new) > 1) {
- $result2 = array_merge(array_slice($new, 0, -1), $k > 0 ? array_slice($before, $k) : $before, array_slice($new, -1), $after);
- $out[] = $result2;
- }
- }
- }
- }
- }
- /**
- * Match extends single
- *
- * @param array $rawSingle
- * @param array $outOrigin
- *
- * @return boolean
- */
- protected function matchExtendsSingle($rawSingle, &$outOrigin)
- {
- $counts = array();
- $single = array();
- foreach ($rawSingle as $part) {
- // matches Number
- if (!is_string($part)) {
- return false;
- }
- if (!preg_match('/^[\\[.:#%]/', $part) && count($single)) {
- $single[count($single) - 1] .= $part;
- } else {
- $single[] = $part;
- }
- }
- foreach ($single as $part) {
- if (isset($this->extendsMap[$part])) {
- foreach ($this->extendsMap[$part] as $idx) {
- $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
- }
- }
- }
- $outOrigin = array();
- $found = false;
- foreach ($counts as $idx => $count) {
- list($target, $origin, ) = $this->extends[$idx];
- // check count
- if ($count !== count($target)) {
- continue;
- }
- $this->extends[$idx][3] = true;
- $rem = array_diff($single, $target);
- foreach ($origin as $j => $new) {
- // prevent infinite loop when target extends itself
- if ($this->isSelfExtend($single, $origin)) {
- return false;
- }
- $combined = $this->combineSelectorSingle(end($new), $rem);
- if (count(array_diff($combined, $origin[$j][count($origin[$j]) - 1]))) {
- $origin[$j][count($origin[$j]) - 1] = $combined;
- }
- }
- $outOrigin = array_merge($outOrigin, $origin);
- $found = true;
- }
- return $found;
- }
- /**
- * Combine selector single
- *
- * @param array $base
- * @param array $other
- *
- * @return array
- */
- protected function combineSelectorSingle($base, $other)
- {
- $tag = array();
- $out = array();
- $wasTag = true;
- foreach (array($base, $other) as $single) {
- foreach ($single as $part) {
- if (preg_match('/^[\\[.:#]/', $part)) {
- $out[] = $part;
- $wasTag = false;
- } elseif (preg_match('/^[^_-]/', $part)) {
- $tag[] = $part;
- $wasTag = true;
- } elseif ($wasTag) {
- $tag[count($tag) - 1] .= $part;
- } else {
- $out[count($out) - 1] .= $part;
- }
- }
- }
- if (count($tag)) {
- array_unshift($out, $tag[0]);
- }
- return $out;
- }
- /**
- * Compile media
- *
- * @param \Leafo\ScssPhp\Block $media
- */
- protected function compileMedia(Block $media)
- {
- $this->pushEnv($media);
- $mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env));
- if (!empty($mediaQuery)) {
- $this->scope = $this->makeOutputBlock(Type::T_MEDIA, array($mediaQuery));
- $parentScope = $this->mediaParent($this->scope);
- $parentScope->children[] = $this->scope;
- // top level properties in a media cause it to be wrapped
- $needsWrap = false;
- foreach ($media->children as $child) {
- $type = $child[0];
- if ($type !== Type::T_BLOCK && $type !== Type::T_MEDIA && $type !== Type::T_DIRECTIVE && $type !== Type::T_IMPORT) {
- $needsWrap = true;
- break;
- }
- }
- if ($needsWrap) {
- $wrapped = new Block();
- $wrapped->sourceIndex = $media->sourceIndex;
- $wrapped->sourceLine = $media->sourceLine;
- $wrapped->sourceColumn = $media->sourceColumn;
- $wrapped->selectors = array();
- $wrapped->comments = array();
- $wrapped->parent = $media;
- $wrapped->children = $media->children;
- $media->children = array(array(Type::T_BLOCK, $wrapped));
- }
- $this->compileChildrenNoReturn($media->children, $this->scope);
- $this->scope = $this->scope->parent;
- }
- $this->popEnv();
- }
- /**
- * Media parent
- *
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $scope
- *
- * @return \Leafo\ScssPhp\Formatter\OutputBlock
- */
- protected function mediaParent(OutputBlock $scope)
- {
- while (!empty($scope->parent)) {
- if (!empty($scope->type) && $scope->type !== Type::T_MEDIA) {
- break;
- }
- $scope = $scope->parent;
- }
- return $scope;
- }
- /**
- * Compile directive
- *
- * @param \Leafo\ScssPhp\Block $block
- */
- protected function compileDirective(Block $block)
- {
- $s = '@' . $block->name;
- if (!empty($block->value)) {
- $s .= ' ' . $this->compileValue($block->value);
- }
- if ($block->name === 'keyframes' || substr($block->name, -10) === '-keyframes') {
- $this->compileKeyframeBlock($block, array($s));
- } else {
- $this->compileNestedBlock($block, array($s));
- }
- }
- /**
- * Compile at-root
- *
- * @param \Leafo\ScssPhp\Block $block
- */
- protected function compileAtRoot(Block $block)
- {
- $env = $this->pushEnv($block);
- $envs = $this->compactEnv($env);
- $without = isset($block->with) ? $this->compileWith($block->with) : self::WITH_RULE;
- // wrap inline selector
- if ($block->selector) {
- $wrapped = new Block();
- $wrapped->sourceIndex = $block->sourceIndex;
- $wrapped->sourceLine = $block->sourceLine;
- $wrapped->sourceColumn = $block->sourceColumn;
- $wrapped->selectors = $block->selector;
- $wrapped->comments = array();
- $wrapped->parent = $block;
- $wrapped->children = $block->children;
- $block->children = array(array(Type::T_BLOCK, $wrapped));
- }
- $this->env = $this->filterWithout($envs, $without);
- $newBlock = $this->spliceTree($envs, $block, $without);
- $saveScope = $this->scope;
- $this->scope = $this->rootBlock;
- $this->compileChild($newBlock, $this->scope);
- $this->scope = $saveScope;
- $this->env = $this->extractEnv($envs);
- $this->popEnv();
- }
- /**
- * Splice parse tree
- *
- * @param array $envs
- * @param \Leafo\ScssPhp\Block $block
- * @param integer $without
- *
- * @return array
- */
- private function spliceTree($envs, Block $block, $without)
- {
- $newBlock = null;
- foreach ($envs as $e) {
- if (!isset($e->block)) {
- continue;
- }
- if ($e->block === $block) {
- continue;
- }
- if (isset($e->block->type) && $e->block->type === Type::T_AT_ROOT) {
- continue;
- }
- if ($e->block && $this->isWithout($without, $e->block)) {
- continue;
- }
- $b = new Block();
- $b->sourceIndex = $e->block->sourceIndex;
- $b->sourceLine = $e->block->sourceLine;
- $b->sourceColumn = $e->block->sourceColumn;
- $b->selectors = array();
- $b->comments = $e->block->comments;
- $b->parent = null;
- if ($newBlock) {
- $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK;
- $b->children = array(array($type, $newBlock));
- $newBlock->parent = $b;
- } elseif (count($block->children)) {
- foreach ($block->children as $child) {
- if ($child[0] === Type::T_BLOCK) {
- $child[1]->parent = $b;
- }
- }
- $b->children = $block->children;
- }
- if (isset($e->block->type)) {
- $b->type = $e->block->type;
- }
- if (isset($e->block->name)) {
- $b->name = $e->block->name;
- }
- if (isset($e->block->queryList)) {
- $b->queryList = $e->block->queryList;
- }
- if (isset($e->block->value)) {
- $b->value = $e->block->value;
- }
- $newBlock = $b;
- }
- $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK;
- return array($type, $newBlock);
- }
- /**
- * Compile @at-root's with: inclusion / without: exclusion into filter flags
- *
- * @param array $with
- *
- * @return integer
- */
- private function compileWith($with)
- {
- static $mapping = array('rule' => self::WITH_RULE, 'media' => self::WITH_MEDIA, 'supports' => self::WITH_SUPPORTS, 'all' => self::WITH_ALL);
- // exclude selectors by default
- $without = self::WITH_RULE;
- if ($this->libMapHasKey(array($with, self::$with))) {
- $without = self::WITH_ALL;
- $list = $this->coerceList($this->libMapGet(array($with, self::$with)));
- foreach ($list[2] as $item) {
- $keyword = $this->compileStringContent($this->coerceString($item));
- if (array_key_exists($keyword, $mapping)) {
- $without &= ~$mapping[$keyword];
- }
- }
- }
- if ($this->libMapHasKey(array($with, self::$without))) {
- $without = 0;
- $list = $this->coerceList($this->libMapGet(array($with, self::$without)));
- foreach ($list[2] as $item) {
- $keyword = $this->compileStringContent($this->coerceString($item));
- if (array_key_exists($keyword, $mapping)) {
- $without |= $mapping[$keyword];
- }
- }
- }
- return $without;
- }
- /**
- * Filter env stack
- *
- * @param array $envs
- * @param integer $without
- *
- * @return \Leafo\ScssPhp\Compiler\Environment
- */
- private function filterWithout($envs, $without)
- {
- $filtered = array();
- foreach ($envs as $e) {
- if ($e->block && $this->isWithout($without, $e->block)) {
- continue;
- }
- $filtered[] = $e;
- }
- return $this->extractEnv($filtered);
- }
- /**
- * Filter WITH rules
- *
- * @param integer $without
- * @param \Leafo\ScssPhp\Block $block
- *
- * @return boolean
- */
- private function isWithout($without, Block $block)
- {
- if ($without & self::WITH_RULE && isset($block->selectors) || $without & self::WITH_MEDIA && isset($block->type) && $block->type === Type::T_MEDIA || $without & self::WITH_SUPPORTS && isset($block->type) && $block->type === Type::T_DIRECTIVE && isset($block->name) && $block->name === 'supports') {
- return true;
- }
- return false;
- }
- /**
- * Compile keyframe block
- *
- * @param \Leafo\ScssPhp\Block $block
- * @param array $selectors
- */
- protected function compileKeyframeBlock(Block $block, $selectors)
- {
- $env = $this->pushEnv($block);
- $envs = $this->compactEnv($env);
- $this->env = $this->extractEnv(array_filter($envs, function (Environment $e) {
- return !isset($e->block->selectors);
- }));
- $this->scope = $this->makeOutputBlock($block->type, $selectors);
- $this->scope->depth = 1;
- $this->scope->parent->children[] = $this->scope;
- $this->compileChildrenNoReturn($block->children, $this->scope);
- $this->scope = $this->scope->parent;
- $this->env = $this->extractEnv($envs);
- $this->popEnv();
- }
- /**
- * Compile nested block
- *
- * @param \Leafo\ScssPhp\Block $block
- * @param array $selectors
- */
- protected function compileNestedBlock(Block $block, $selectors)
- {
- $this->pushEnv($block);
- $this->scope = $this->makeOutputBlock($block->type, $selectors);
- $this->scope->parent->children[] = $this->scope;
- $this->compileChildrenNoReturn($block->children, $this->scope);
- $this->scope = $this->scope->parent;
- $this->popEnv();
- }
- /**
- * Recursively compiles a block.
- *
- * A block is analogous to a CSS block in most cases. A single SCSS document
- * is encapsulated in a block when parsed, but it does not have parent tags
- * so all of its children appear on the root level when compiled.
- *
- * Blocks are made up of selectors and children.
- *
- * The children of a block are just all the blocks that are defined within.
- *
- * Compiling the block involves pushing a fresh environment on the stack,
- * and iterating through the props, compiling each one.
- *
- * @see Compiler::compileChild()
- *
- * @param \Leafo\ScssPhp\Block $block
- */
- protected function compileBlock(Block $block)
- {
- $env = $this->pushEnv($block);
- $env->selectors = $this->evalSelectors($block->selectors);
- $out = $this->makeOutputBlock(null);
- if (isset($this->lineNumberStyle) && count($env->selectors) && count($block->children)) {
- $annotation = $this->makeOutputBlock(Type::T_COMMENT);
- $annotation->depth = 0;
- $file = $this->sourceNames[$block->sourceIndex];
- $line = $block->sourceLine;
- switch ($this->lineNumberStyle) {
- case self::LINE_COMMENTS:
- $annotation->lines[] = '/* line ' . $line . ', ' . $file . ' */';
- break;
- case self::DEBUG_INFO:
- $annotation->lines[] = '@media -sass-debug-info{filename{font-family:"' . $file . '"}line{font-family:' . $line . '}}';
- break;
- }
- $this->scope->children[] = $annotation;
- }
- $this->scope->children[] = $out;
- if (count($block->children)) {
- $out->selectors = $this->multiplySelectors($env);
- $this->compileChildrenNoReturn($block->children, $out);
- }
- $this->formatter->stripSemicolon($out->lines);
- $this->popEnv();
- }
- /**
- * Compile root level comment
- *
- * @param array $block
- */
- protected function compileComment($block)
- {
- $out = $this->makeOutputBlock(Type::T_COMMENT);
- $out->lines[] = $block[1];
- $this->scope->children[] = $out;
- }
- /**
- * Evaluate selectors
- *
- * @param array $selectors
- *
- * @return array
- */
- protected function evalSelectors($selectors)
- {
- $this->shouldEvaluate = false;
- $selectors = array_map(array($this, 'evalSelector'), $selectors);
- // after evaluating interpolates, we might need a second pass
- if ($this->shouldEvaluate) {
- $buffer = $this->collapseSelectors($selectors);
- $parser = $this->parserFactory(__METHOD__);
- if ($parser->parseSelector($buffer, $newSelectors)) {
- $selectors = array_map(array($this, 'evalSelector'), $newSelectors);
- }
- }
- return $selectors;
- }
- /**
- * Evaluate selector
- *
- * @param array $selector
- *
- * @return array
- */
- protected function evalSelector($selector)
- {
- return array_map(array($this, 'evalSelectorPart'), $selector);
- }
- /**
- * Evaluate selector part; replaces all the interpolates, stripping quotes
- *
- * @param array $part
- *
- * @return array
- */
- protected function evalSelectorPart($part)
- {
- foreach ($part as &$p) {
- if (is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
- $p = $this->compileValue($p);
- // force re-evaluation
- if (strpos($p, '&') !== false || strpos($p, ',') !== false) {
- $this->shouldEvaluate = true;
- }
- } elseif (is_string($p) && strlen($p) >= 2 && ($first = $p[0]) && ($first === '"' || $first === '\'') && substr($p, -1) === $first) {
- $p = substr($p, 1, -1);
- }
- }
- return $this->flattenSelectorSingle($part);
- }
- /**
- * Collapse selectors
- *
- * @param array $selectors
- *
- * @return string
- */
- protected function collapseSelectors($selectors)
- {
- $parts = array();
- foreach ($selectors as $selector) {
- $output = '';
- array_walk_recursive($selector, function ($value, $key) use(&$output) {
- $output .= $value;
- });
- $parts[] = $output;
- }
- return implode(', ', $parts);
- }
- /**
- * Flatten selector single; joins together .classes and #ids
- *
- * @param array $single
- *
- * @return array
- */
- protected function flattenSelectorSingle($single)
- {
- $joined = array();
- foreach ($single as $part) {
- if (empty($joined) || !is_string($part) || preg_match('/[\\[.:#%]/', $part)) {
- $joined[] = $part;
- continue;
- }
- if (is_array(end($joined))) {
- $joined[] = $part;
- } else {
- $joined[count($joined) - 1] .= $part;
- }
- }
- return $joined;
- }
- /**
- * Compile selector to string; self(&) should have been replaced by now
- *
- * @param array $selector
- *
- * @return string
- */
- protected function compileSelector($selector)
- {
- if (!is_array($selector)) {
- return $selector;
- }
- return implode(' ', array_map(array($this, 'compileSelectorPart'), $selector));
- }
- /**
- * Compile selector part
- *
- * @param arary $piece
- *
- * @return string
- */
- protected function compileSelectorPart($piece)
- {
- foreach ($piece as &$p) {
- if (!is_array($p)) {
- continue;
- }
- switch ($p[0]) {
- case Type::T_SELF:
- $p = '&';
- break;
- default:
- $p = $this->compileValue($p);
- break;
- }
- }
- return implode($piece);
- }
- /**
- * Has selector placeholder?
- *
- * @param array $selector
- *
- * @return boolean
- */
- protected function hasSelectorPlaceholder($selector)
- {
- if (!is_array($selector)) {
- return false;
- }
- foreach ($selector as $parts) {
- foreach ($parts as $part) {
- if (strlen($part) && '%' === $part[0]) {
- return true;
- }
- }
- }
- return false;
- }
- /**
- * Compile children and return result
- *
- * @param array $stms
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
- *
- * @return array
- */
- protected function compileChildren($stms, OutputBlock $out)
- {
- foreach ($stms as $stm) {
- $ret = $this->compileChild($stm, $out);
- if (isset($ret)) {
- return $ret;
- }
- }
- }
- /**
- * Compile children and throw exception if unexpected @return
- *
- * @param array $stms
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
- *
- * @throws \Exception
- */
- protected function compileChildrenNoReturn($stms, OutputBlock $out)
- {
- foreach ($stms as $stm) {
- $ret = $this->compileChild($stm, $out);
- if (isset($ret)) {
- $this->throwError('@return may only be used within a function');
- return;
- }
- }
- }
- /**
- * Compile media query
- *
- * @param array $queryList
- *
- * @return string
- */
- protected function compileMediaQuery($queryList)
- {
- $out = '@media';
- $first = true;
- foreach ($queryList as $query) {
- $type = null;
- $parts = array();
- foreach ($query as $q) {
- switch ($q[0]) {
- case Type::T_MEDIA_TYPE:
- if ($type) {
- $type = $this->mergeMediaTypes($type, array_map(array($this, 'compileValue'), array_slice($q, 1)));
- if (empty($type)) {
- // merge failed
- return null;
- }
- } else {
- $type = array_map(array($this, 'compileValue'), array_slice($q, 1));
- }
- break;
- case Type::T_MEDIA_EXPRESSION:
- if (isset($q[2])) {
- $parts[] = '(' . $this->compileValue($q[1]) . $this->formatter->assignSeparator . $this->compileValue($q[2]) . ')';
- } else {
- $parts[] = '(' . $this->compileValue($q[1]) . ')';
- }
- break;
- case Type::T_MEDIA_VALUE:
- $parts[] = $this->compileValue($q[1]);
- break;
- }
- }
- if ($type) {
- array_unshift($parts, implode(' ', array_filter($type)));
- }
- if (!empty($parts)) {
- if ($first) {
- $first = false;
- $out .= ' ';
- } else {
- $out .= $this->formatter->tagSeparator;
- }
- $out .= implode(' and ', $parts);
- }
- }
- return $out;
- }
- /**
- * Merge media types
- *
- * @param array $type1
- * @param array $type2
- *
- * @return array|null
- */
- protected function mergeMediaTypes($type1, $type2)
- {
- if (empty($type1)) {
- return $type2;
- }
- if (empty($type2)) {
- return $type1;
- }
- $m1 = '';
- $t1 = '';
- if (count($type1) > 1) {
- $m1 = strtolower($type1[0]);
- $t1 = strtolower($type1[1]);
- } else {
- $t1 = strtolower($type1[0]);
- }
- $m2 = '';
- $t2 = '';
- if (count($type2) > 1) {
- $m2 = strtolower($type2[0]);
- $t2 = strtolower($type2[1]);
- } else {
- $t2 = strtolower($type2[0]);
- }
- if ($m1 === Type::T_NOT ^ $m2 === Type::T_NOT) {
- if ($t1 === $t2) {
- return null;
- }
- return array($m1 === Type::T_NOT ? $m2 : $m1, $m1 === Type::T_NOT ? $t2 : $t1);
- }
- if ($m1 === Type::T_NOT && $m2 === Type::T_NOT) {
- // CSS has no way of representing "neither screen nor print"
- if ($t1 !== $t2) {
- return null;
- }
- return array(Type::T_NOT, $t1);
- }
- if ($t1 !== $t2) {
- return null;
- }
- // t1 == t2, neither m1 nor m2 are "not"
- return array(empty($m1) ? $m2 : $m1, $t1);
- }
- /**
- * Compile import; returns true if the value was something that could be imported
- *
- * @param array $rawPath
- * @param array $out
- * @param boolean $once
- *
- * @return boolean
- */
- protected function compileImport($rawPath, $out, $once = false)
- {
- if ($rawPath[0] === Type::T_STRING) {
- $path = $this->compileStringContent($rawPath);
- if ($path = $this->findImport($path)) {
- if (!$once || !in_array($path, $this->importedFiles)) {
- $this->importFile($path, $out);
- $this->importedFiles[] = $path;
- }
- return true;
- }
- return false;
- }
- if ($rawPath[0] === Type::T_LIST) {
- // handle a list of strings
- if (count($rawPath[2]) === 0) {
- return false;
- }
- foreach ($rawPath[2] as $path) {
- if ($path[0] !== Type::T_STRING) {
- return false;
- }
- }
- foreach ($rawPath[2] as $path) {
- $this->compileImport($path, $out);
- }
- return true;
- }
- return false;
- }
- /**
- * Compile child; returns a value to halt execution
- *
- * @param array $child
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
- *
- * @return array
- */
- protected function compileChild($child, OutputBlock $out)
- {
- $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
- $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
- $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
- switch ($child[0]) {
- case Type::T_SCSSPHP_IMPORT_ONCE:
- list(, $rawPath) = $child;
- $rawPath = $this->reduce($rawPath);
- if (!$this->compileImport($rawPath, $out, true)) {
- $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
- }
- break;
- case Type::T_IMPORT:
- list(, $rawPath) = $child;
- $rawPath = $this->reduce($rawPath);
- if (!$this->compileImport($rawPath, $out)) {
- $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
- }
- break;
- case Type::T_DIRECTIVE:
- $this->compileDirective($child[1]);
- break;
- case Type::T_AT_ROOT:
- $this->compileAtRoot($child[1]);
- break;
- case Type::T_MEDIA:
- $this->compileMedia($child[1]);
- break;
- case Type::T_BLOCK:
- $this->compileBlock($child[1]);
- break;
- case Type::T_CHARSET:
- if (!$this->charsetSeen) {
- $this->charsetSeen = true;
- $out->lines[] = '@charset ' . $this->compileValue($child[1]) . ';';
- }
- break;
- case Type::T_ASSIGN:
- list(, $name, $value) = $child;
- if ($name[0] === Type::T_VARIABLE) {
- $flags = isset($child[3]) ? $child[3] : array();
- $isDefault = in_array('!default', $flags);
- $isGlobal = in_array('!global', $flags);
- if ($isGlobal) {
- $this->set($name[1], $this->reduce($value), false, $this->rootEnv);
- break;
- }
- $shouldSet = $isDefault && (($result = $this->get($name[1], false)) === null || $result === self::$null);
- if (!$isDefault || $shouldSet) {
- $this->set($name[1], $this->reduce($value));
- }
- break;
- }
- $compiledName = $this->compileValue($name);
- // handle shorthand syntax: size / line-height
- if ($compiledName === 'font') {
- if ($value[0] === Type::T_EXPRESSION && $value[1] === '/') {
- $value = $this->expToString($value);
- } elseif ($value[0] === Type::T_LIST) {
- foreach ($value[2] as &$item) {
- if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
- $item = $this->expToString($item);
- }
- }
- }
- }
- // if the value reduces to null from something else then
- // the property should be discarded
- if ($value[0] !== Type::T_NULL) {
- $value = $this->reduce($value);
- if ($value[0] === Type::T_NULL || $value === self::$nullString) {
- break;
- }
- }
- $compiledValue = $this->compileValue($value);
- $out->lines[] = $this->formatter->property($compiledName, $compiledValue);
- break;
- case Type::T_COMMENT:
- if ($out->type === Type::T_ROOT) {
- $this->compileComment($child);
- break;
- }
- $out->lines[] = $child[1];
- break;
- case Type::T_MIXIN:
- case Type::T_FUNCTION:
- list(, $block) = $child;
- $this->set(self::$namespaces[$block->type] . $block->name, $block);
- break;
- case Type::T_EXTEND:
- list(, $selectors) = $child;
- foreach ($selectors as $sel) {
- $results = $this->evalSelectors(array($sel));
- foreach ($results as $result) {
- // only use the first one
- $result = current($result);
- $this->pushExtends($result, $out->selectors, $child);
- }
- }
- break;
- case Type::T_IF:
- list(, $if) = $child;
- if ($this->isTruthy($this->reduce($if->cond, true))) {
- return $this->compileChildren($if->children, $out);
- }
- foreach ($if->cases as $case) {
- if ($case->type === Type::T_ELSE || $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond))) {
- return $this->compileChildren($case->children, $out);
- }
- }
- break;
- case Type::T_EACH:
- list(, $each) = $child;
- $list = $this->coerceList($this->reduce($each->list));
- $this->pushEnv();
- foreach ($list[2] as $item) {
- if (count($each->vars) === 1) {
- $this->set($each->vars[0], $item, true);
- } else {
- list(, , $values) = $this->coerceList($item);
- foreach ($each->vars as $i => $var) {
- $this->set($var, isset($values[$i]) ? $values[$i] : self::$null, true);
- }
- }
- $ret = $this->compileChildren($each->children, $out);
- if ($ret) {
- if ($ret[0] !== Type::T_CONTROL) {
- $this->popEnv();
- return $ret;
- }
- if ($ret[1]) {
- break;
- }
- }
- }
- $this->popEnv();
- break;
- case Type::T_WHILE:
- list(, $while) = $child;
- while ($this->isTruthy($this->reduce($while->cond, true))) {
- $ret = $this->compileChildren($while->children, $out);
- if ($ret) {
- if ($ret[0] !== Type::T_CONTROL) {
- return $ret;
- }
- if ($ret[1]) {
- break;
- }
- }
- }
- break;
- case Type::T_FOR:
- list(, $for) = $child;
- $start = $this->reduce($for->start, true);
- $start = $start[1];
- $end = $this->reduce($for->end, true);
- $end = $end[1];
- $d = $start < $end ? 1 : -1;
- while (true) {
- if (!$for->until && $start - $d == $end || $for->until && $start == $end) {
- break;
- }
- $this->set($for->var, new Node\Number($start, ''));
- $start += $d;
- $ret = $this->compileChildren($for->children, $out);
- if ($ret) {
- if ($ret[0] !== Type::T_CONTROL) {
- return $ret;
- }
- if ($ret[1]) {
- break;
- }
- }
- }
- break;
- case Type::T_BREAK:
- return array(Type::T_CONTROL, true);
- case Type::T_CONTINUE:
- return array(Type::T_CONTROL, false);
- case Type::T_RETURN:
- return $this->reduce($child[1], true);
- case Type::T_NESTED_PROPERTY:
- list(, $prop) = $child;
- $prefixed = array();
- $prefix = $this->compileValue($prop->prefix) . '-';
- foreach ($prop->children as $child) {
- switch ($child[0]) {
- case Type::T_ASSIGN:
- array_unshift($child[1][2], $prefix);
- break;
- case Type::T_NESTED_PROPERTY:
- array_unshift($child[1]->prefix[2], $prefix);
- break;
- }
- $prefixed[] = $child;
- }
- $this->compileChildrenNoReturn($prefixed, $out);
- break;
- case Type::T_INCLUDE:
- // including a mixin
- list(, $name, $argValues, $content) = $child;
- $mixin = $this->get(self::$namespaces['mixin'] . $name, false);
- if (!$mixin) {
- $this->throwError("Undefined mixin {$name}");
- break;
- }
- $callingScope = $this->getStoreEnv();
- // push scope, apply args
- $this->pushEnv();
- $this->env->depth--;
- if (isset($content)) {
- $content->scope = $callingScope;
- $this->setRaw(self::$namespaces['special'] . 'content', $content, $this->env);
- }
- if (isset($mixin->args)) {
- $this->applyArguments($mixin->args, $argValues);
- }
- $this->env->marker = 'mixin';
- $this->compileChildrenNoReturn($mixin->children, $out);
- $this->popEnv();
- break;
- case Type::T_MIXIN_CONTENT:
- $content = $this->get(self::$namespaces['special'] . 'content', false, $this->getStoreEnv()) ?: $this->get(self::$namespaces['special'] . 'content', false, $this->env);
- if (!$content) {
- $this->throwError('Expected @content inside of mixin');
- break;
- }
- $storeEnv = $this->storeEnv;
- $this->storeEnv = $content->scope;
- $this->compileChildrenNoReturn($content->children, $out);
- $this->storeEnv = $storeEnv;
- break;
- case Type::T_DEBUG:
- list(, $value) = $child;
- $line = $this->sourceLine;
- $value = $this->compileValue($this->reduce($value, true));
- fwrite($this->stderr, "Line {$line} DEBUG: {$value}\n");
- break;
- case Type::T_WARN:
- list(, $value) = $child;
- $line = $this->sourceLine;
- $value = $this->compileValue($this->reduce($value, true));
- fwrite($this->stderr, "Line {$line} WARN: {$value}\n");
- break;
- case Type::T_ERROR:
- list(, $value) = $child;
- $line = $this->sourceLine;
- $value = $this->compileValue($this->reduce($value, true));
- $this->throwError("Line {$line} ERROR: {$value}\n");
- break;
- case Type::T_CONTROL:
- $this->throwError('@break/@continue not permitted in this scope');
- break;
- default:
- $this->throwError("unknown child type: {$child['0']}");
- }
- }
- /**
- * Reduce expression to string
- *
- * @param array $exp
- *
- * @return array
- */
- protected function expToString($exp)
- {
- list(, $op, $left, $right, , $whiteLeft, $whiteRight) = $exp;
- $content = array($this->reduce($left));
- if ($whiteLeft) {
- $content[] = ' ';
- }
- $content[] = $op;
- if ($whiteRight) {
- $content[] = ' ';
- }
- $content[] = $this->reduce($right);
- return array(Type::T_STRING, '', $content);
- }
- /**
- * Is truthy?
- *
- * @param array $value
- *
- * @return array
- */
- protected function isTruthy($value)
- {
- return $value !== self::$false && $value !== self::$null;
- }
- /**
- * Should $value cause its operand to eval
- *
- * @param array $value
- *
- * @return boolean
- */
- protected function shouldEval($value)
- {
- switch ($value[0]) {
- case Type::T_EXPRESSION:
- if ($value[1] === '/') {
- return $this->shouldEval($value[2], $value[3]);
- }
- // fall-thru
- case Type::T_VARIABLE:
- case Type::T_FUNCTION_CALL:
- return true;
- }
- return false;
- }
- /**
- * Reduce value
- *
- * @param array $value
- * @param boolean $inExp
- *
- * @return array
- */
- protected function reduce($value, $inExp = false)
- {
- list($type) = $value;
- switch ($type) {
- case Type::T_EXPRESSION:
- list(, $op, $left, $right, $inParens) = $value;
- $opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op;
- $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
- $left = $this->reduce($left, true);
- if ($op !== 'and' && $op !== 'or') {
- $right = $this->reduce($right, true);
- }
- // special case: looks like css shorthand
- if ($opName == 'div' && !$inParens && !$inExp && isset($right[2]) && ($right[0] !== Type::T_NUMBER && $right[2] != '' || $right[0] === Type::T_NUMBER && !$right->unitless())) {
- return $this->expToString($value);
- }
- $left = $this->coerceForExpression($left);
- $right = $this->coerceForExpression($right);
- $ltype = $left[0];
- $rtype = $right[0];
- $ucOpName = ucfirst($opName);
- $ucLType = ucfirst($ltype);
- $ucRType = ucfirst($rtype);
- // this tries:
- // 1. op[op name][left type][right type]
- // 2. op[left type][right type] (passing the op as first arg
- // 3. op[op name]
- $fn = "op{$ucOpName}{$ucLType}{$ucRType}";
- if (is_callable(array($this, $fn)) || ($fn = "op{$ucLType}{$ucRType}") && is_callable(array($this, $fn)) && ($passOp = true) || ($fn = "op{$ucOpName}") && is_callable(array($this, $fn)) && ($genOp = true)) {
- $coerceUnit = false;
- if (!isset($genOp) && $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER) {
- $coerceUnit = true;
- switch ($opName) {
- case 'mul':
- $targetUnit = $left[2];
- foreach ($right[2] as $unit => $exp) {
- $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) + $exp;
- }
- break;
- case 'div':
- $targetUnit = $left[2];
- foreach ($right[2] as $unit => $exp) {
- $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) - $exp;
- }
- break;
- case 'mod':
- $targetUnit = $left[2];
- break;
- default:
- $targetUnit = $left->unitless() ? $right[2] : $left[2];
- }
- if (!$left->unitless() && !$right->unitless()) {
- $left = $left->normalize();
- $right = $right->normalize();
- }
- }
- $shouldEval = $inParens || $inExp;
- if (isset($passOp)) {
- $out = $this->{$fn}($op, $left, $right, $shouldEval);
- } else {
- $out = $this->{$fn}($left, $right, $shouldEval);
- }
- if (isset($out)) {
- if ($coerceUnit && $out[0] === Type::T_NUMBER) {
- $out = $out->coerce($targetUnit);
- }
- return $out;
- }
- }
- return $this->expToString($value);
- case Type::T_UNARY:
- list(, $op, $exp, $inParens) = $value;
- $inExp = $inExp || $this->shouldEval($exp);
- $exp = $this->reduce($exp);
- if ($exp[0] === Type::T_NUMBER) {
- switch ($op) {
- case '+':
- return new Node\Number($exp[1], $exp[2]);
- case '-':
- return new Node\Number(-$exp[1], $exp[2]);
- }
- }
- if ($op === 'not') {
- if ($inExp || $inParens) {
- if ($exp === self::$false || $exp === self::$null) {
- return self::$true;
- }
- return self::$false;
- }
- $op = $op . ' ';
- }
- return array(Type::T_STRING, '', array($op, $exp));
- case Type::T_VARIABLE:
- list(, $name) = $value;
- return $this->reduce($this->get($name));
- case Type::T_LIST:
- foreach ($value[2] as &$item) {
- $item = $this->reduce($item);
- }
- return $value;
- case Type::T_MAP:
- foreach ($value[1] as &$item) {
- $item = $this->reduce($item);
- }
- foreach ($value[2] as &$item) {
- $item = $this->reduce($item);
- }
- return $value;
- case Type::T_STRING:
- foreach ($value[2] as &$item) {
- if (is_array($item) || $item instanceof \ArrayAccess) {
- $item = $this->reduce($item);
- }
- }
- return $value;
- case Type::T_INTERPOLATE:
- $value[1] = $this->reduce($value[1]);
- return $value;
- case Type::T_FUNCTION_CALL:
- list(, $name, $argValues) = $value;
- return $this->fncall($name, $argValues);
- default:
- return $value;
- }
- }
- /**
- * Function caller
- *
- * @param string $name
- * @param array $argValues
- *
- * @return array|null
- */
- private function fncall($name, $argValues)
- {
- // SCSS @function
- if ($this->callScssFunction($name, $argValues, $returnValue)) {
- return $returnValue;
- }
- // native PHP functions
- if ($this->callNativeFunction($name, $argValues, $returnValue)) {
- return $returnValue;
- }
- // for CSS functions, simply flatten the arguments into a list
- $listArgs = array();
- foreach ((array) $argValues as $arg) {
- if (empty($arg[0])) {
- $listArgs[] = $this->reduce($arg[1]);
- }
- }
- return array(Type::T_FUNCTION, $name, array(Type::T_LIST, ',', $listArgs));
- }
- /**
- * Normalize name
- *
- * @param string $name
- *
- * @return string
- */
- protected function normalizeName($name)
- {
- return str_replace('-', '_', $name);
- }
- /**
- * Normalize value
- *
- * @param array $value
- *
- * @return array
- */
- public function normalizeValue($value)
- {
- $value = $this->coerceForExpression($this->reduce($value));
- list($type) = $value;
- switch ($type) {
- case Type::T_LIST:
- $value = $this->extractInterpolation($value);
- if ($value[0] !== Type::T_LIST) {
- return array(Type::T_KEYWORD, $this->compileValue($value));
- }
- foreach ($value[2] as $key => $item) {
- $value[2][$key] = $this->normalizeValue($item);
- }
- return $value;
- case Type::T_STRING:
- return array($type, '"', array($this->compileStringContent($value)));
- case Type::T_NUMBER:
- return $value->normalize();
- case Type::T_INTERPOLATE:
- return array(Type::T_KEYWORD, $this->compileValue($value));
- default:
- return $value;
- }
- }
- /**
- * Add numbers
- *
- * @param array $left
- * @param array $right
- *
- * @return array
- */
- protected function opAddNumberNumber($left, $right)
- {
- return new Node\Number($left[1] + $right[1], $left[2]);
- }
- /**
- * Multiply numbers
- *
- * @param array $left
- * @param array $right
- *
- * @return array
- */
- protected function opMulNumberNumber($left, $right)
- {
- return new Node\Number($left[1] * $right[1], $left[2]);
- }
- /**
- * Subtract numbers
- *
- * @param array $left
- * @param array $right
- *
- * @return array
- */
- protected function opSubNumberNumber($left, $right)
- {
- return new Node\Number($left[1] - $right[1], $left[2]);
- }
- /**
- * Divide numbers
- *
- * @param array $left
- * @param array $right
- *
- * @return array
- */
- protected function opDivNumberNumber($left, $right)
- {
- if ($right[1] == 0) {
- return array(Type::T_STRING, '', array($left[1] . $left[2] . '/' . $right[1] . $right[2]));
- }
- return new Node\Number($left[1] / $right[1], $left[2]);
- }
- /**
- * Mod numbers
- *
- * @param array $left
- * @param array $right
- *
- * @return array
- */
- protected function opModNumberNumber($left, $right)
- {
- return new Node\Number($left[1] % $right[1], $left[2]);
- }
- /**
- * Add strings
- *
- * @param array $left
- * @param array $right
- *
- * @return array
- */
- protected function opAdd($left, $right)
- {
- if ($strLeft = $this->coerceString($left)) {
- if ($right[0] === Type::T_STRING) {
- $right[1] = '';
- }
- $strLeft[2][] = $right;
- return $strLeft;
- }
- if ($strRight = $this->coerceString($right)) {
- if ($left[0] === Type::T_STRING) {
- $left[1] = '';
- }
- array_unshift($strRight[2], $left);
- return $strRight;
- }
- }
- /**
- * Boolean and
- *
- * @param array $left
- * @param array $right
- * @param boolean $shouldEval
- *
- * @return array
- */
- protected function opAnd($left, $right, $shouldEval)
- {
- if (!$shouldEval) {
- return;
- }
- if ($left !== self::$false and $left !== self::$null) {
- return $this->reduce($right, true);
- }
- return $left;
- }
- /**
- * Boolean or
- *
- * @param array $left
- * @param array $right
- * @param boolean $shouldEval
- *
- * @return array
- */
- protected function opOr($left, $right, $shouldEval)
- {
- if (!$shouldEval) {
- return;
- }
- if ($left !== self::$false and $left !== self::$null) {
- return $left;
- }
- return $this->reduce($right, true);
- }
- /**
- * Compare colors
- *
- * @param string $op
- * @param array $left
- * @param array $right
- *
- * @return array
- */
- protected function opColorColor($op, $left, $right)
- {
- $out = array(Type::T_COLOR);
- foreach (array(1, 2, 3) as $i) {
- $lval = isset($left[$i]) ? $left[$i] : 0;
- $rval = isset($right[$i]) ? $right[$i] : 0;
- switch ($op) {
- case '+':
- $out[] = $lval + $rval;
- break;
- case '-':
- $out[] = $lval - $rval;
- break;
- case '*':
- $out[] = $lval * $rval;
- break;
- case '%':
- $out[] = $lval % $rval;
- break;
- case '/':
- if ($rval == 0) {
- $this->throwError('color: Can\'t divide by zero');
- break 2;
- }
- $out[] = (int) ($lval / $rval);
- break;
- case '==':
- return $this->opEq($left, $right);
- case '!=':
- return $this->opNeq($left, $right);
- default:
- $this->throwError("color: unknown op {$op}");
- break 2;
- }
- }
- if (isset($left[4])) {
- $out[4] = $left[4];
- } elseif (isset($right[4])) {
- $out[4] = $right[4];
- }
- return $this->fixColor($out);
- }
- /**
- * Compare color and number
- *
- * @param string $op
- * @param array $left
- * @param array $right
- *
- * @return array
- */
- protected function opColorNumber($op, $left, $right)
- {
- $value = $right[1];
- return $this->opColorColor($op, $left, array(Type::T_COLOR, $value, $value, $value));
- }
- /**
- * Compare number and color
- *
- * @param string $op
- * @param array $left
- * @param array $right
- *
- * @return array
- */
- protected function opNumberColor($op, $left, $right)
- {
- $value = $left[1];
- return $this->opColorColor($op, array(Type::T_COLOR, $value, $value, $value), $right);
- }
- /**
- * Compare number1 == number2
- *
- * @param array $left
- * @param array $right
- *
- * @return array
- */
- protected function opEq($left, $right)
- {
- if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
- $lStr[1] = '';
- $rStr[1] = '';
- $left = $this->compileValue($lStr);
- $right = $this->compileValue($rStr);
- }
- return $this->toBool($left === $right);
- }
- /**
- * Compare number1 != number2
- *
- * @param array $left
- * @param array $right
- *
- * @return array
- */
- protected function opNeq($left, $right)
- {
- if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
- $lStr[1] = '';
- $rStr[1] = '';
- $left = $this->compileValue($lStr);
- $right = $this->compileValue($rStr);
- }
- return $this->toBool($left !== $right);
- }
- /**
- * Compare number1 >= number2
- *
- * @param array $left
- * @param array $right
- *
- * @return array
- */
- protected function opGteNumberNumber($left, $right)
- {
- return $this->toBool($left[1] >= $right[1]);
- }
- /**
- * Compare number1 > number2
- *
- * @param array $left
- * @param array $right
- *
- * @return array
- */
- protected function opGtNumberNumber($left, $right)
- {
- return $this->toBool($left[1] > $right[1]);
- }
- /**
- * Compare number1 <= number2
- *
- * @param array $left
- * @param array $right
- *
- * @return array
- */
- protected function opLteNumberNumber($left, $right)
- {
- return $this->toBool($left[1] <= $right[1]);
- }
- /**
- * Compare number1 < number2
- *
- * @param array $left
- * @param array $right
- *
- * @return array
- */
- protected function opLtNumberNumber($left, $right)
- {
- return $this->toBool($left[1] < $right[1]);
- }
- /**
- * Three-way comparison, aka spaceship operator
- *
- * @param array $left
- * @param array $right
- *
- * @return array
- */
- protected function opCmpNumberNumber($left, $right)
- {
- $n = $left[1] - $right[1];
- return new Node\Number($n ? $n / abs($n) : 0, '');
- }
- /**
- * Cast to boolean
- *
- * @api
- *
- * @param mixed $thing
- *
- * @return array
- */
- public function toBool($thing)
- {
- return $thing ? self::$true : self::$false;
- }
- /**
- * Compiles a primitive value into a CSS property value.
- *
- * Values in scssphp are typed by being wrapped in arrays, their format is
- * typically:
- *
- * array(type, contents [, additional_contents]*)
- *
- * The input is expected to be reduced. This function will not work on
- * things like expressions and variables.
- *
- * @api
- *
- * @param array $value
- *
- * @return string
- */
- public function compileValue($value)
- {
- $value = $this->reduce($value);
- list($type) = $value;
- switch ($type) {
- case Type::T_KEYWORD:
- return $value[1];
- case Type::T_COLOR:
- // [1] - red component (either number for a %)
- // [2] - green component
- // [3] - blue component
- // [4] - optional alpha component
- list(, $r, $g, $b) = $value;
- $r = round($r);
- $g = round($g);
- $b = round($b);
- if (count($value) === 5 && $value[4] !== 1) {
- // rgba
- return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $value[4] . ')';
- }
- $h = sprintf('#%02x%02x%02x', $r, $g, $b);
- // Converting hex color to short notation (e.g. #003399 to #039)
- if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
- $h = '#' . $h[1] . $h[3] . $h[5];
- }
- return $h;
- case Type::T_NUMBER:
- return $value->output($this);
- case Type::T_STRING:
- return $value[1] . $this->compileStringContent($value) . $value[1];
- case Type::T_FUNCTION:
- $args = !empty($value[2]) ? $this->compileValue($value[2]) : '';
- return "{$value['1']}({$args})";
- case Type::T_LIST:
- $value = $this->extractInterpolation($value);
- if ($value[0] !== Type::T_LIST) {
- return $this->compileValue($value);
- }
- list(, $delim, $items) = $value;
- if ($delim !== ' ') {
- $delim .= ' ';
- }
- $filtered = array();
- foreach ($items as $item) {
- if ($item[0] === Type::T_NULL) {
- continue;
- }
- $filtered[] = $this->compileValue($item);
- }
- return implode("{$delim}", $filtered);
- case Type::T_MAP:
- $keys = $value[1];
- $values = $value[2];
- $filtered = array();
- for ($i = 0, $s = count($keys); $i < $s; $i++) {
- $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]);
- }
- array_walk($filtered, function (&$value, $key) {
- $value = $key . ': ' . $value;
- });
- return '(' . implode(', ', $filtered) . ')';
- case Type::T_INTERPOLATED:
- // node created by extractInterpolation
- list(, $interpolate, $left, $right) = $value;
- list(, , $whiteLeft, $whiteRight) = $interpolate;
- $left = count($left[2]) > 0 ? $this->compileValue($left) . $whiteLeft : '';
- $right = count($right[2]) > 0 ? $whiteRight . $this->compileValue($right) : '';
- return $left . $this->compileValue($interpolate) . $right;
- case Type::T_INTERPOLATE:
- // raw parse node
- list(, $exp) = $value;
- // strip quotes if it's a string
- $reduced = $this->reduce($exp);
- switch ($reduced[0]) {
- case Type::T_STRING:
- $reduced = array(Type::T_KEYWORD, $this->compileStringContent($reduced));
- break;
- case Type::T_NULL:
- $reduced = array(Type::T_KEYWORD, '');
- }
- return $this->compileValue($reduced);
- case Type::T_NULL:
- return 'null';
- default:
- $this->throwError("unknown value type: {$type}");
- }
- }
- /**
- * Flatten list
- *
- * @param array $list
- *
- * @return string
- */
- protected function flattenList($list)
- {
- return $this->compileValue($list);
- }
- /**
- * Compile string content
- *
- * @param array $string
- *
- * @return string
- */
- protected function compileStringContent($string)
- {
- $parts = array();
- foreach ($string[2] as $part) {
- if (is_array($part) || $part instanceof \ArrayAccess) {
- $parts[] = $this->compileValue($part);
- } else {
- $parts[] = $part;
- }
- }
- return implode($parts);
- }
- /**
- * Extract interpolation; it doesn't need to be recursive, compileValue will handle that
- *
- * @param array $list
- *
- * @return array
- */
- protected function extractInterpolation($list)
- {
- $items = $list[2];
- foreach ($items as $i => $item) {
- if ($item[0] === Type::T_INTERPOLATE) {
- $before = array(Type::T_LIST, $list[1], array_slice($items, 0, $i));
- $after = array(Type::T_LIST, $list[1], array_slice($items, $i + 1));
- return array(Type::T_INTERPOLATED, $item, $before, $after);
- }
- }
- return $list;
- }
- /**
- * Find the final set of selectors
- *
- * @param \Leafo\ScssPhp\Compiler\Environment $env
- *
- * @return array
- */
- protected function multiplySelectors(Environment $env)
- {
- $envs = $this->compactEnv($env);
- $selectors = array();
- $parentSelectors = array(array());
- while ($env = array_pop($envs)) {
- if (empty($env->selectors)) {
- continue;
- }
- $selectors = array();
- foreach ($env->selectors as $selector) {
- foreach ($parentSelectors as $parent) {
- $selectors[] = $this->joinSelectors($parent, $selector);
- }
- }
- $parentSelectors = $selectors;
- }
- return $selectors;
- }
- /**
- * Join selectors; looks for & to replace, or append parent before child
- *
- * @param array $parent
- * @param array $child
- *
- * @return array
- */
- protected function joinSelectors($parent, $child)
- {
- $setSelf = false;
- $out = array();
- foreach ($child as $part) {
- $newPart = array();
- foreach ($part as $p) {
- if ($p === self::$selfSelector) {
- $setSelf = true;
- foreach ($parent as $i => $parentPart) {
- if ($i > 0) {
- $out[] = $newPart;
- $newPart = array();
- }
- foreach ($parentPart as $pp) {
- $newPart[] = $pp;
- }
- }
- } else {
- $newPart[] = $p;
- }
- }
- $out[] = $newPart;
- }
- return $setSelf ? $out : array_merge($parent, $child);
- }
- /**
- * Multiply media
- *
- * @param \Leafo\ScssPhp\Compiler\Environment $env
- * @param array $childQueries
- *
- * @return array
- */
- protected function multiplyMedia(Environment $env = null, $childQueries = null)
- {
- if (!isset($env) || !empty($env->block->type) && $env->block->type !== Type::T_MEDIA) {
- return $childQueries;
- }
- // plain old block, skip
- if (empty($env->block->type)) {
- return $this->multiplyMedia($env->parent, $childQueries);
- }
- $parentQueries = isset($env->block->queryList) ? $env->block->queryList : array(array(array(Type::T_MEDIA_VALUE, $env->block->value)));
- if ($childQueries === null) {
- $childQueries = $parentQueries;
- } else {
- $originalQueries = $childQueries;
- $childQueries = array();
- foreach ($parentQueries as $parentQuery) {
- foreach ($originalQueries as $childQuery) {
- $childQueries[] = array_merge($parentQuery, $childQuery);
- }
- }
- }
- return $this->multiplyMedia($env->parent, $childQueries);
- }
- /**
- * Convert env linked list to stack
- *
- * @param \Leafo\ScssPhp\Compiler\Environment $env
- *
- * @return array
- */
- private function compactEnv(Environment $env)
- {
- for ($envs = array(); $env; $env = $env->parent) {
- $envs[] = $env;
- }
- return $envs;
- }
- /**
- * Convert env stack to singly linked list
- *
- * @param array $envs
- *
- * @return \Leafo\ScssPhp\Compiler\Environment
- */
- private function extractEnv($envs)
- {
- for ($env = null; $e = array_pop($envs);) {
- $e->parent = $env;
- $env = $e;
- }
- return $env;
- }
- /**
- * Push environment
- *
- * @param \Leafo\ScssPhp\Block $block
- *
- * @return \Leafo\ScssPhp\Compiler\Environment
- */
- protected function pushEnv(Block $block = null)
- {
- $env = new Environment();
- $env->parent = $this->env;
- $env->store = array();
- $env->block = $block;
- $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
- $this->env = $env;
- return $env;
- }
- /**
- * Pop environment
- */
- protected function popEnv()
- {
- $this->env = $this->env->parent;
- }
- /**
- * Get store environment
- *
- * @return \Leafo\ScssPhp\Compiler\Environment
- */
- protected function getStoreEnv()
- {
- return isset($this->storeEnv) ? $this->storeEnv : $this->env;
- }
- /**
- * Set variable
- *
- * @param string $name
- * @param mixed $value
- * @param boolean $shadow
- * @param \Leafo\ScssPhp\Compiler\Environment $env
- */
- protected function set($name, $value, $shadow = false, Environment $env = null)
- {
- $name = $this->normalizeName($name);
- if (!isset($env)) {
- $env = $this->getStoreEnv();
- }
- if ($shadow) {
- $this->setRaw($name, $value, $env);
- } else {
- $this->setExisting($name, $value, $env);
- }
- }
- /**
- * Set existing variable
- *
- * @param string $name
- * @param mixed $value
- * @param \Leafo\ScssPhp\Compiler\Environment $env
- */
- protected function setExisting($name, $value, Environment $env)
- {
- $storeEnv = $env;
- $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
- for (;;) {
- if (array_key_exists($name, $env->store)) {
- break;
- }
- if (!$hasNamespace && isset($env->marker)) {
- $env = $storeEnv;
- break;
- }
- if (!isset($env->parent)) {
- $env = $storeEnv;
- break;
- }
- $env = $env->parent;
- }
- $env->store[$name] = $value;
- }
- /**
- * Set raw variable
- *
- * @param string $name
- * @param mixed $value
- * @param \Leafo\ScssPhp\Compiler\Environment $env
- */
- protected function setRaw($name, $value, Environment $env)
- {
- $env->store[$name] = $value;
- }
- /**
- * Get variable
- *
- * @api
- *
- * @param string $name
- * @param boolean $shouldThrow
- * @param \Leafo\ScssPhp\Compiler\Environment $env
- *
- * @return mixed
- */
- public function get($name, $shouldThrow = true, Environment $env = null)
- {
- $name = $this->normalizeName($name);
- if (!isset($env)) {
- $env = $this->getStoreEnv();
- }
- $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
- for (;;) {
- if (array_key_exists($name, $env->store)) {
- return $env->store[$name];
- }
- if (!$hasNamespace && isset($env->marker)) {
- $env = $this->rootEnv;
- continue;
- }
- if (!isset($env->parent)) {
- break;
- }
- $env = $env->parent;
- }
- if ($shouldThrow) {
- $this->throwError("Undefined variable \${$name}");
- }
- }
- /**
- * Has variable?
- *
- * @param string $name
- * @param \Leafo\ScssPhp\Compiler\Environment $env
- *
- * @return boolean
- */
- protected function has($name, Environment $env = null)
- {
- return $this->get($name, false, $env) !== null;
- }
- /**
- * Inject variables
- *
- * @param array $args
- */
- protected function injectVariables(array $args)
- {
- if (empty($args)) {
- return;
- }
- $parser = $this->parserFactory(__METHOD__);
- foreach ($args as $name => $strValue) {
- if ($name[0] === '$') {
- $name = substr($name, 1);
- }
- if (!$parser->parseValue($strValue, $value)) {
- $value = $this->coerceValue($strValue);
- }
- $this->set($name, $value);
- }
- }
- /**
- * Set variables
- *
- * @api
- *
- * @param array $variables
- */
- public function setVariables(array $variables)
- {
- $this->registeredVars = array_merge($this->registeredVars, $variables);
- }
- /**
- * Unset variable
- *
- * @api
- *
- * @param string $name
- */
- public function unsetVariable($name)
- {
- unset($this->registeredVars[$name]);
- }
- /**
- * Returns list of variables
- *
- * @api
- *
- * @return array
- */
- public function getVariables()
- {
- return $this->registeredVars;
- }
- /**
- * Adds to list of parsed files
- *
- * @api
- *
- * @param string $path
- */
- public function addParsedFile($path)
- {
- if (isset($path) && file_exists($path)) {
- $this->parsedFiles[realpath($path)] = filemtime($path);
- }
- }
- /**
- * Returns list of parsed files
- *
- * @api
- *
- * @return array
- */
- public function getParsedFiles()
- {
- return $this->parsedFiles;
- }
- /**
- * Add import path
- *
- * @api
- *
- * @param string $path
- */
- public function addImportPath($path)
- {
- if (!in_array($path, $this->importPaths)) {
- $this->importPaths[] = $path;
- }
- }
- /**
- * Set import paths
- *
- * @api
- *
- * @param string|array $path
- */
- public function setImportPaths($path)
- {
- $this->importPaths = (array) $path;
- }
- /**
- * Set number precision
- *
- * @api
- *
- * @param integer $numberPrecision
- */
- public function setNumberPrecision($numberPrecision)
- {
- Node\Number::$precision = $numberPrecision;
- }
- /**
- * Set formatter
- *
- * @api
- *
- * @param string $formatterName
- */
- public function setFormatter($formatterName)
- {
- $this->formatter = $formatterName;
- }
- /**
- * Set line number style
- *
- * @api
- *
- * @param string $lineNumberStyle
- */
- public function setLineNumberStyle($lineNumberStyle)
- {
- $this->lineNumberStyle = $lineNumberStyle;
- }
- /**
- * Register function
- *
- * @api
- *
- * @param string $name
- * @param callable $func
- * @param array $prototype
- */
- public function registerFunction($name, $func, $prototype = null)
- {
- $this->userFunctions[$this->normalizeName($name)] = array($func, $prototype);
- }
- /**
- * Unregister function
- *
- * @api
- *
- * @param string $name
- */
- public function unregisterFunction($name)
- {
- unset($this->userFunctions[$this->normalizeName($name)]);
- }
- /**
- * Add feature
- *
- * @api
- *
- * @param string $name
- */
- public function addFeature($name)
- {
- $this->registeredFeatures[$name] = true;
- }
- /**
- * Import file
- *
- * @param string $path
- * @param array $out
- */
- protected function importFile($path, $out)
- {
- // see if tree is cached
- $realPath = realpath($path);
- if (isset($this->importCache[$realPath])) {
- $this->handleImportLoop($realPath);
- $tree = $this->importCache[$realPath];
- } else {
- $code = file_get_contents($path);
- $parser = $this->parserFactory($path);
- $tree = $parser->parse($code);
- $this->importCache[$realPath] = $tree;
- }
- $pi = pathinfo($path);
- array_unshift($this->importPaths, $pi['dirname']);
- $this->compileChildrenNoReturn($tree->children, $out);
- array_shift($this->importPaths);
- }
- /**
- * Return the file path for an import url if it exists
- *
- * @api
- *
- * @param string $url
- *
- * @return string|null
- */
- public function findImport($url)
- {
- $urls = array();
- // for "normal" scss imports (ignore vanilla css and external requests)
- if (!preg_match('/\\.css$|^https?:\\/\\//', $url)) {
- // try both normal and the _partial filename
- $urls = array($url, preg_replace('/[^\\/]+$/', '_\\0', $url));
- }
- foreach ($this->importPaths as $dir) {
- if (is_string($dir)) {
- // check urls for normal import paths
- foreach ($urls as $full) {
- $full = $dir . (!empty($dir) && substr($dir, -1) !== '/' ? '/' : '') . $full;
- if ($this->fileExists($file = $full . '.scss') || $this->fileExists($file = $full)) {
- return $file;
- }
- }
- } elseif (is_callable($dir)) {
- // check custom callback for import path
- $file = call_user_func($dir, $url);
- if ($file !== null) {
- return $file;
- }
- }
- }
- return null;
- }
- /**
- * Set encoding
- *
- * @api
- *
- * @param string $encoding
- */
- public function setEncoding($encoding)
- {
- $this->encoding = $encoding;
- }
- /**
- * Ignore errors?
- *
- * @api
- *
- * @param boolean $ignoreErrors
- *
- * @return \Leafo\ScssPhp\Compiler
- */
- public function setIgnoreErrors($ignoreErrors)
- {
- $this->ignoreErrors = $ignoreErrors;
- }
- /**
- * Throw error (exception)
- *
- * @api
- *
- * @param string $msg Message with optional sprintf()-style vararg parameters
- *
- * @throws \Leafo\ScssPhp\Exception\CompilerException
- */
- public function throwError($msg)
- {
- if ($this->ignoreErrors) {
- return;
- }
- if (func_num_args() > 1) {
- $msg = call_user_func_array('sprintf', func_get_args());
- }
- $line = $this->sourceLine;
- $msg = "{$msg}: line: {$line}";
- throw new CompilerException($msg);
- }
- /**
- * Handle import loop
- *
- * @param string $name
- *
- * @throws \Exception
- */
- protected function handleImportLoop($name)
- {
- for ($env = $this->env; $env; $env = $env->parent) {
- $file = $this->sourceNames[$env->block->sourceIndex];
- if (realpath($file) === $name) {
- $this->throwError('An @import loop has been found: %s imports %s', $file, basename($file));
- break;
- }
- }
- }
- /**
- * Does file exist?
- *
- * @param string $name
- *
- * @return boolean
- */
- protected function fileExists($name)
- {
- return is_file($name);
- }
- /**
- * Call SCSS @function
- *
- * @param string $name
- * @param array $args
- * @param array $returnValue
- *
- * @return boolean Returns true if returnValue is set; otherwise, false
- */
- protected function callScssFunction($name, $argValues, &$returnValue)
- {
- $func = $this->get(self::$namespaces['function'] . $name, false);
- if (!$func) {
- return false;
- }
- $this->pushEnv();
- // set the args
- if (isset($func->args)) {
- $this->applyArguments($func->args, $argValues);
- }
- // throw away lines and children
- $tmp = new OutputBlock();
- $tmp->lines = array();
- $tmp->children = array();
- $this->env->marker = 'function';
- $ret = $this->compileChildren($func->children, $tmp);
- $this->popEnv();
- $returnValue = !isset($ret) ? self::$defaultValue : $ret;
- return true;
- }
- /**
- * Call built-in and registered (PHP) functions
- *
- * @param string $name
- * @param array $args
- * @param array $returnValue
- *
- * @return boolean Returns true if returnValue is set; otherwise, false
- */
- protected function callNativeFunction($name, $args, &$returnValue)
- {
- // try a lib function
- $name = $this->normalizeName($name);
- if (isset($this->userFunctions[$name])) {
- // see if we can find a user function
- list($f, $prototype) = $this->userFunctions[$name];
- } elseif (($f = $this->getBuiltinFunction($name)) && is_callable($f)) {
- $libName = $f[1];
- $prototype = isset(self::${$libName}) ? self::${$libName} : null;
- } else {
- return false;
- }
- list($sorted, $kwargs) = $this->sortArgs($prototype, $args);
- if ($name !== 'if' && $name !== 'call') {
- foreach ($sorted as &$val) {
- $val = $this->reduce($val, true);
- }
- }
- $returnValue = call_user_func($f, $sorted, $kwargs);
- if (!isset($returnValue)) {
- return false;
- }
- $returnValue = $this->coerceValue($returnValue);
- return true;
- }
- /**
- * Get built-in function
- *
- * @param string $name Normalized name
- *
- * @return array
- */
- protected function getBuiltinFunction($name)
- {
- $libName = 'lib' . preg_replace_callback('/_(.)/', function ($m) {
- return ucfirst($m[1]);
- }, ucfirst($name));
- return array($this, $libName);
- }
- /**
- * Sorts keyword arguments
- *
- * @param array $prototype
- * @param array $args
- *
- * @return array
- */
- protected function sortArgs($prototype, $args)
- {
- $keyArgs = array();
- $posArgs = array();
- // separate positional and keyword arguments
- foreach ($args as $arg) {
- list($key, $value) = $arg;
- $key = $key[1];
- if (empty($key)) {
- $posArgs[] = $value;
- } else {
- $keyArgs[$key] = $value;
- }
- }
- if (!isset($prototype)) {
- return array($posArgs, $keyArgs);
- }
- // copy positional args
- $finalArgs = array_pad($posArgs, count($prototype), null);
- // overwrite positional args with keyword args
- foreach ($prototype as $i => $names) {
- foreach ((array) $names as $name) {
- if (isset($keyArgs[$name])) {
- $finalArgs[$i] = $keyArgs[$name];
- }
- }
- }
- return array($finalArgs, $keyArgs);
- }
- /**
- * Apply argument values per definition
- *
- * @param array $argDef
- * @param array $argValues
- *
- * @throws \Exception
- */
- protected function applyArguments($argDef, $argValues)
- {
- $storeEnv = $this->getStoreEnv();
- $env = new Environment();
- $env->store = $storeEnv->store;
- $hasVariable = false;
- $args = array();
- foreach ($argDef as $i => $arg) {
- list($name, $default, $isVariable) = $argDef[$i];
- $args[$name] = array($i, $name, $default, $isVariable);
- $hasVariable |= $isVariable;
- }
- $keywordArgs = array();
- $deferredKeywordArgs = array();
- $remaining = array();
- // assign the keyword args
- foreach ((array) $argValues as $arg) {
- if (!empty($arg[0])) {
- if (!isset($args[$arg[0][1]])) {
- if ($hasVariable) {
- $deferredKeywordArgs[$arg[0][1]] = $arg[1];
- } else {
- $this->throwError('Mixin or function doesn\'t have an argument named $%s.', $arg[0][1]);
- break;
- }
- } elseif ($args[$arg[0][1]][0] < count($remaining)) {
- $this->throwError('The argument $%s was passed both by position and by name.', $arg[0][1]);
- break;
- } else {
- $keywordArgs[$arg[0][1]] = $arg[1];
- }
- } elseif (count($keywordArgs)) {
- $this->throwError('Positional arguments must come before keyword arguments.');
- break;
- } elseif ($arg[2] === true) {
- $val = $this->reduce($arg[1], true);
- if ($val[0] === Type::T_LIST) {
- foreach ($val[2] as $name => $item) {
- if (!is_numeric($name)) {
- $keywordArgs[$name] = $item;
- } else {
- $remaining[] = $item;
- }
- }
- } elseif ($val[0] === Type::T_MAP) {
- foreach ($val[1] as $i => $name) {
- $name = $this->compileStringContent($this->coerceString($name));
- $item = $val[2][$i];
- if (!is_numeric($name)) {
- $keywordArgs[$name] = $item;
- } else {
- $remaining[] = $item;
- }
- }
- } else {
- $remaining[] = $val;
- }
- } else {
- $remaining[] = $arg[1];
- }
- }
- foreach ($args as $arg) {
- list($i, $name, $default, $isVariable) = $arg;
- if ($isVariable) {
- $val = array(Type::T_LIST, ',', array(), $isVariable);
- for ($count = count($remaining); $i < $count; $i++) {
- $val[2][] = $remaining[$i];
- }
- foreach ($deferredKeywordArgs as $itemName => $item) {
- $val[2][$itemName] = $item;
- }
- } elseif (isset($remaining[$i])) {
- $val = $remaining[$i];
- } elseif (isset($keywordArgs[$name])) {
- $val = $keywordArgs[$name];
- } elseif (!empty($default)) {
- continue;
- } else {
- $this->throwError("Missing argument {$name}");
- break;
- }
- $this->set($name, $this->reduce($val, true), true, $env);
- }
- $storeEnv->store = $env->store;
- foreach ($args as $arg) {
- list($i, $name, $default, $isVariable) = $arg;
- if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) {
- continue;
- }
- $this->set($name, $this->reduce($default, true), true);
- }
- }
- /**
- * Coerce a php value into a scss one
- *
- * @param mixed $value
- *
- * @return array
- */
- private function coerceValue($value)
- {
- if (is_array($value) || $value instanceof \ArrayAccess) {
- return $value;
- }
- if (is_bool($value)) {
- return $this->toBool($value);
- }
- if ($value === null) {
- $value = self::$null;
- }
- if (is_numeric($value)) {
- return new Node\Number($value, '');
- }
- if ($value === '') {
- return self::$emptyString;
- }
- return array(Type::T_KEYWORD, $value);
- }
- /**
- * Coerce something to map
- *
- * @param array $item
- *
- * @return array
- */
- protected function coerceMap($item)
- {
- if ($item[0] === Type::T_MAP) {
- return $item;
- }
- if ($item === self::$emptyList) {
- return self::$emptyMap;
- }
- return array(Type::T_MAP, array($item), array(self::$null));
- }
- /**
- * Coerce something to list
- *
- * @param array $item
- *
- * @return array
- */
- protected function coerceList($item, $delim = ',')
- {
- if (isset($item) && $item[0] === Type::T_LIST) {
- return $item;
- }
- if (isset($item) && $item[0] === Type::T_MAP) {
- $keys = $item[1];
- $values = $item[2];
- $list = array();
- for ($i = 0, $s = count($keys); $i < $s; $i++) {
- $key = $keys[$i];
- $value = $values[$i];
- $list[] = array(Type::T_LIST, '', array(array(Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))), $value));
- }
- return array(Type::T_LIST, ',', $list);
- }
- return array(Type::T_LIST, $delim, !isset($item) ? array() : array($item));
- }
- /**
- * Coerce color for expression
- *
- * @param array $value
- *
- * @return array|null
- */
- protected function coerceForExpression($value)
- {
- if ($color = $this->coerceColor($value)) {
- return $color;
- }
- return $value;
- }
- /**
- * Coerce value to color
- *
- * @param array $value
- *
- * @return array|null
- */
- protected function coerceColor($value)
- {
- switch ($value[0]) {
- case Type::T_COLOR:
- return $value;
- case Type::T_KEYWORD:
- $name = strtolower($value[1]);
- if (isset(Colors::$cssColors[$name])) {
- $rgba = explode(',', Colors::$cssColors[$name]);
- return isset($rgba[3]) ? array(Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3]) : array(Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]);
- }
- return null;
- }
- return null;
- }
- /**
- * Coerce value to string
- *
- * @param array $value
- *
- * @return array|null
- */
- protected function coerceString($value)
- {
- if ($value[0] === Type::T_STRING) {
- return $value;
- }
- return array(Type::T_STRING, '', array($this->compileValue($value)));
- }
- /**
- * Coerce value to a percentage
- *
- * @param array $value
- *
- * @return integer|float
- */
- protected function coercePercent($value)
- {
- if ($value[0] === Type::T_NUMBER) {
- if (!empty($value[2]['%'])) {
- return $value[1] / 100;
- }
- return $value[1];
- }
- return 0;
- }
- /**
- * Assert value is a map
- *
- * @api
- *
- * @param array $value
- *
- * @return array
- *
- * @throws \Exception
- */
- public function assertMap($value)
- {
- $value = $this->coerceMap($value);
- if ($value[0] !== Type::T_MAP) {
- $this->throwError('expecting map');
- }
- return $value;
- }
- /**
- * Assert value is a list
- *
- * @api
- *
- * @param array $value
- *
- * @return array
- *
- * @throws \Exception
- */
- public function assertList($value)
- {
- if ($value[0] !== Type::T_LIST) {
- $this->throwError('expecting list');
- }
- return $value;
- }
- /**
- * Assert value is a color
- *
- * @api
- *
- * @param array $value
- *
- * @return array
- *
- * @throws \Exception
- */
- public function assertColor($value)
- {
- if ($color = $this->coerceColor($value)) {
- return $color;
- }
- $this->throwError('expecting color');
- }
- /**
- * Assert value is a number
- *
- * @api
- *
- * @param array $value
- *
- * @return integer|float
- *
- * @throws \Exception
- */
- public function assertNumber($value)
- {
- if ($value[0] !== Type::T_NUMBER) {
- $this->throwError('expecting number');
- }
- return $value[1];
- }
- /**
- * Make sure a color's components don't go out of bounds
- *
- * @param array $c
- *
- * @return array
- */
- protected function fixColor($c)
- {
- foreach (array(1, 2, 3) as $i) {
- if ($c[$i] < 0) {
- $c[$i] = 0;
- }
- if ($c[$i] > 255) {
- $c[$i] = 255;
- }
- }
- return $c;
- }
- /**
- * Convert RGB to HSL
- *
- * @api
- *
- * @param integer $red
- * @param integer $green
- * @param integer $blue
- *
- * @return array
- */
- public function toHSL($red, $green, $blue)
- {
- $min = min($red, $green, $blue);
- $max = max($red, $green, $blue);
- $l = $min + $max;
- $d = $max - $min;
- if ((int) $d === 0) {
- $h = $s = 0;
- } else {
- if ($l < 255) {
- $s = $d / $l;
- } else {
- $s = $d / (510 - $l);
- }
- if ($red == $max) {
- $h = 60 * ($green - $blue) / $d;
- } elseif ($green == $max) {
- $h = 60 * ($blue - $red) / $d + 120;
- } elseif ($blue == $max) {
- $h = 60 * ($red - $green) / $d + 240;
- }
- }
- return array(Type::T_HSL, fmod($h, 360), $s * 100, $l / 5.1);
- }
- /**
- * Hue to RGB helper
- *
- * @param float $m1
- * @param float $m2
- * @param float $h
- *
- * @return float
- */
- private function hueToRGB($m1, $m2, $h)
- {
- if ($h < 0) {
- $h += 1;
- } elseif ($h > 1) {
- $h -= 1;
- }
- if ($h * 6 < 1) {
- return $m1 + ($m2 - $m1) * $h * 6;
- }
- if ($h * 2 < 1) {
- return $m2;
- }
- if ($h * 3 < 2) {
- return $m1 + ($m2 - $m1) * (2 / 3 - $h) * 6;
- }
- return $m1;
- }
- /**
- * Convert HSL to RGB
- *
- * @api
- *
- * @param integer $hue H from 0 to 360
- * @param integer $saturation S from 0 to 100
- * @param integer $lightness L from 0 to 100
- *
- * @return array
- */
- public function toRGB($hue, $saturation, $lightness)
- {
- if ($hue < 0) {
- $hue += 360;
- }
- $h = $hue / 360;
- $s = min(100, max(0, $saturation)) / 100;
- $l = min(100, max(0, $lightness)) / 100;
- $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
- $m1 = $l * 2 - $m2;
- $r = $this->hueToRGB($m1, $m2, $h + 1 / 3) * 255;
- $g = $this->hueToRGB($m1, $m2, $h) * 255;
- $b = $this->hueToRGB($m1, $m2, $h - 1 / 3) * 255;
- $out = array(Type::T_COLOR, $r, $g, $b);
- return $out;
- }
- // Built in functions
- //protected static $libCall = ['name', 'args...'];
- protected function libCall($args, $kwargs)
- {
- $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
- $args = array_map(function ($a) {
- return array(null, $a, false);
- }, $args);
- if (count($kwargs)) {
- foreach ($kwargs as $key => $value) {
- $args[] = array(array(Type::T_VARIABLE, $key), $value, false);
- }
- }
- return $this->reduce(array(Type::T_FUNCTION_CALL, $name, $args));
- }
- protected static $libIf = array('condition', 'if-true', 'if-false');
- protected function libIf($args)
- {
- list($cond, $t, $f) = $args;
- if (!$this->isTruthy($this->reduce($cond, true))) {
- return $this->reduce($f, true);
- }
- return $this->reduce($t, true);
- }
- protected static $libIndex = array('list', 'value');
- protected function libIndex($args)
- {
- list($list, $value) = $args;
- if ($value[0] === Type::T_MAP) {
- return self::$null;
- }
- if ($list[0] === Type::T_MAP || $list[0] === Type::T_STRING || $list[0] === Type::T_KEYWORD || $list[0] === Type::T_INTERPOLATE) {
- $list = $this->coerceList($list, ' ');
- }
- if ($list[0] !== Type::T_LIST) {
- return self::$null;
- }
- $values = array();
- foreach ($list[2] as $item) {
- $values[] = $this->normalizeValue($item);
- }
- $key = array_search($this->normalizeValue($value), $values);
- return false === $key ? self::$null : $key + 1;
- }
- protected static $libRgb = array('red', 'green', 'blue');
- protected function libRgb($args)
- {
- list($r, $g, $b) = $args;
- return array(Type::T_COLOR, $r[1], $g[1], $b[1]);
- }
- protected static $libRgba = array(array('red', 'color'), 'green', 'blue', 'alpha');
- protected function libRgba($args)
- {
- if ($color = $this->coerceColor($args[0])) {
- $num = !isset($args[1]) ? $args[3] : $args[1];
- $alpha = $this->assertNumber($num);
- $color[4] = $alpha;
- return $color;
- }
- list($r, $g, $b, $a) = $args;
- return array(Type::T_COLOR, $r[1], $g[1], $b[1], $a[1]);
- }
- // helper function for adjust_color, change_color, and scale_color
- protected function alterColor($args, $fn)
- {
- $color = $this->assertColor($args[0]);
- foreach (array(1, 2, 3, 7) as $i) {
- if (isset($args[$i])) {
- $val = $this->assertNumber($args[$i]);
- $ii = $i === 7 ? 4 : $i;
- // alpha
- $color[$ii] = call_user_func($fn, isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
- }
- }
- if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
- foreach (array(4, 5, 6) as $i) {
- if (isset($args[$i])) {
- $val = $this->assertNumber($args[$i]);
- $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i);
- }
- }
- $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
- if (isset($color[4])) {
- $rgb[4] = $color[4];
- }
- $color = $rgb;
- }
- return $color;
- }
- protected static $libAdjustColor = array('color', 'red', 'green', 'blue', 'hue', 'saturation', 'lightness', 'alpha');
- protected function libAdjustColor($args)
- {
- return $this->alterColor($args, function ($base, $alter, $i) {
- return $base + $alter;
- });
- }
- protected static $libChangeColor = array('color', 'red', 'green', 'blue', 'hue', 'saturation', 'lightness', 'alpha');
- protected function libChangeColor($args)
- {
- return $this->alterColor($args, function ($base, $alter, $i) {
- return $alter;
- });
- }
- protected static $libScaleColor = array('color', 'red', 'green', 'blue', 'hue', 'saturation', 'lightness', 'alpha');
- protected function libScaleColor($args)
- {
- return $this->alterColor($args, function ($base, $scale, $i) {
- // 1, 2, 3 - rgb
- // 4, 5, 6 - hsl
- // 7 - a
- switch ($i) {
- case 1:
- case 2:
- case 3:
- $max = 255;
- break;
- case 4:
- $max = 360;
- break;
- case 7:
- $max = 1;
- break;
- default:
- $max = 100;
- }
- $scale = $scale / 100;
- if ($scale < 0) {
- return $base * $scale + $base;
- }
- return ($max - $base) * $scale + $base;
- });
- }
- protected static $libIeHexStr = array('color');
- protected function libIeHexStr($args)
- {
- $color = $this->coerceColor($args[0]);
- $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
- return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
- }
- protected static $libRed = array('color');
- protected function libRed($args)
- {
- $color = $this->coerceColor($args[0]);
- return $color[1];
- }
- protected static $libGreen = array('color');
- protected function libGreen($args)
- {
- $color = $this->coerceColor($args[0]);
- return $color[2];
- }
- protected static $libBlue = array('color');
- protected function libBlue($args)
- {
- $color = $this->coerceColor($args[0]);
- return $color[3];
- }
- protected static $libAlpha = array('color');
- protected function libAlpha($args)
- {
- if ($color = $this->coerceColor($args[0])) {
- return isset($color[4]) ? $color[4] : 1;
- }
- // this might be the IE function, so return value unchanged
- return null;
- }
- protected static $libOpacity = array('color');
- protected function libOpacity($args)
- {
- $value = $args[0];
- if ($value[0] === Type::T_NUMBER) {
- return null;
- }
- return $this->libAlpha($args);
- }
- // mix two colors
- protected static $libMix = array('color-1', 'color-2', 'weight');
- protected function libMix($args)
- {
- list($first, $second, $weight) = $args;
- $first = $this->assertColor($first);
- $second = $this->assertColor($second);
- if (!isset($weight)) {
- $weight = 0.5;
- } else {
- $weight = $this->coercePercent($weight);
- }
- $firstAlpha = isset($first[4]) ? $first[4] : 1;
- $secondAlpha = isset($second[4]) ? $second[4] : 1;
- $w = $weight * 2 - 1;
- $a = $firstAlpha - $secondAlpha;
- $w1 = (($w * $a === -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0;
- $w2 = 1.0 - $w1;
- $new = array(Type::T_COLOR, $w1 * $first[1] + $w2 * $second[1], $w1 * $first[2] + $w2 * $second[2], $w1 * $first[3] + $w2 * $second[3]);
- if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
- $new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1);
- }
- return $this->fixColor($new);
- }
- protected static $libHsl = array('hue', 'saturation', 'lightness');
- protected function libHsl($args)
- {
- list($h, $s, $l) = $args;
- return $this->toRGB($h[1], $s[1], $l[1]);
- }
- protected static $libHsla = array('hue', 'saturation', 'lightness', 'alpha');
- protected function libHsla($args)
- {
- list($h, $s, $l, $a) = $args;
- $color = $this->toRGB($h[1], $s[1], $l[1]);
- $color[4] = $a[1];
- return $color;
- }
- protected static $libHue = array('color');
- protected function libHue($args)
- {
- $color = $this->assertColor($args[0]);
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
- return new Node\Number($hsl[1], 'deg');
- }
- protected static $libSaturation = array('color');
- protected function libSaturation($args)
- {
- $color = $this->assertColor($args[0]);
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
- return new Node\Number($hsl[2], '%');
- }
- protected static $libLightness = array('color');
- protected function libLightness($args)
- {
- $color = $this->assertColor($args[0]);
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
- return new Node\Number($hsl[3], '%');
- }
- protected function adjustHsl($color, $idx, $amount)
- {
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
- $hsl[$idx] += $amount;
- $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
- if (isset($color[4])) {
- $out[4] = $color[4];
- }
- return $out;
- }
- protected static $libAdjustHue = array('color', 'degrees');
- protected function libAdjustHue($args)
- {
- $color = $this->assertColor($args[0]);
- $degrees = $this->assertNumber($args[1]);
- return $this->adjustHsl($color, 1, $degrees);
- }
- protected static $libLighten = array('color', 'amount');
- protected function libLighten($args)
- {
- $color = $this->assertColor($args[0]);
- $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
- return $this->adjustHsl($color, 3, $amount);
- }
- protected static $libDarken = array('color', 'amount');
- protected function libDarken($args)
- {
- $color = $this->assertColor($args[0]);
- $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
- return $this->adjustHsl($color, 3, -$amount);
- }
- protected static $libSaturate = array('color', 'amount');
- protected function libSaturate($args)
- {
- $value = $args[0];
- if ($value[0] === Type::T_NUMBER) {
- return null;
- }
- $color = $this->assertColor($value);
- $amount = 100 * $this->coercePercent($args[1]);
- return $this->adjustHsl($color, 2, $amount);
- }
- protected static $libDesaturate = array('color', 'amount');
- protected function libDesaturate($args)
- {
- $color = $this->assertColor($args[0]);
- $amount = 100 * $this->coercePercent($args[1]);
- return $this->adjustHsl($color, 2, -$amount);
- }
- protected static $libGrayscale = array('color');
- protected function libGrayscale($args)
- {
- $value = $args[0];
- if ($value[0] === Type::T_NUMBER) {
- return null;
- }
- return $this->adjustHsl($this->assertColor($value), 2, -100);
- }
- protected static $libComplement = array('color');
- protected function libComplement($args)
- {
- return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
- }
- protected static $libInvert = array('color');
- protected function libInvert($args)
- {
- $value = $args[0];
- if ($value[0] === Type::T_NUMBER) {
- return null;
- }
- $color = $this->assertColor($value);
- $color[1] = 255 - $color[1];
- $color[2] = 255 - $color[2];
- $color[3] = 255 - $color[3];
- return $color;
- }
- // increases opacity by amount
- protected static $libOpacify = array('color', 'amount');
- protected function libOpacify($args)
- {
- $color = $this->assertColor($args[0]);
- $amount = $this->coercePercent($args[1]);
- $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
- $color[4] = min(1, max(0, $color[4]));
- return $color;
- }
- protected static $libFadeIn = array('color', 'amount');
- protected function libFadeIn($args)
- {
- return $this->libOpacify($args);
- }
- // decreases opacity by amount
- protected static $libTransparentize = array('color', 'amount');
- protected function libTransparentize($args)
- {
- $color = $this->assertColor($args[0]);
- $amount = $this->coercePercent($args[1]);
- $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
- $color[4] = min(1, max(0, $color[4]));
- return $color;
- }
- protected static $libFadeOut = array('color', 'amount');
- protected function libFadeOut($args)
- {
- return $this->libTransparentize($args);
- }
- protected static $libUnquote = array('string');
- protected function libUnquote($args)
- {
- $str = $args[0];
- if ($str[0] === Type::T_STRING) {
- $str[1] = '';
- }
- return $str;
- }
- protected static $libQuote = array('string');
- protected function libQuote($args)
- {
- $value = $args[0];
- if ($value[0] === Type::T_STRING && !empty($value[1])) {
- return $value;
- }
- return array(Type::T_STRING, '"', array($value));
- }
- protected static $libPercentage = array('value');
- protected function libPercentage($args)
- {
- return new Node\Number($this->coercePercent($args[0]) * 100, '%');
- }
- protected static $libRound = array('value');
- protected function libRound($args)
- {
- $num = $args[0];
- $num[1] = round($num[1]);
- return $num;
- }
- protected static $libFloor = array('value');
- protected function libFloor($args)
- {
- $num = $args[0];
- $num[1] = floor($num[1]);
- return $num;
- }
- protected static $libCeil = array('value');
- protected function libCeil($args)
- {
- $num = $args[0];
- $num[1] = ceil($num[1]);
- return $num;
- }
- protected static $libAbs = array('value');
- protected function libAbs($args)
- {
- $num = $args[0];
- $num[1] = abs($num[1]);
- return $num;
- }
- protected function libMin($args)
- {
- $numbers = $this->getNormalizedNumbers($args);
- $min = null;
- foreach ($numbers as $key => $number) {
- if (null === $min || $number[1] <= $min[1]) {
- $min = array($key, $number[1]);
- }
- }
- return $args[$min[0]];
- }
- protected function libMax($args)
- {
- $numbers = $this->getNormalizedNumbers($args);
- $max = null;
- foreach ($numbers as $key => $number) {
- if (null === $max || $number[1] >= $max[1]) {
- $max = array($key, $number[1]);
- }
- }
- return $args[$max[0]];
- }
- /**
- * Helper to normalize args containing numbers
- *
- * @param array $args
- *
- * @return array
- */
- protected function getNormalizedNumbers($args)
- {
- $unit = null;
- $originalUnit = null;
- $numbers = array();
- foreach ($args as $key => $item) {
- if ($item[0] !== Type::T_NUMBER) {
- $this->throwError('%s is not a number', $item[0]);
- break;
- }
- $number = $item->normalize();
- if (null === $unit) {
- $unit = $number[2];
- $originalUnit = $item->unitStr();
- } elseif ($unit !== $number[2]) {
- $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr());
- break;
- }
- $numbers[$key] = $number;
- }
- return $numbers;
- }
- protected static $libLength = array('list');
- protected function libLength($args)
- {
- $list = $this->coerceList($args[0]);
- return count($list[2]);
- }
- //protected static $libListSeparator = ['list...'];
- protected function libListSeparator($args)
- {
- if (count($args) > 1) {
- return 'comma';
- }
- $list = $this->coerceList($args[0]);
- if (count($list[2]) <= 1) {
- return 'space';
- }
- if ($list[1] === ',') {
- return 'comma';
- }
- return 'space';
- }
- protected static $libNth = array('list', 'n');
- protected function libNth($args)
- {
- $list = $this->coerceList($args[0]);
- $n = $this->assertNumber($args[1]);
- if ($n > 0) {
- $n--;
- } elseif ($n < 0) {
- $n += count($list[2]);
- }
- return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue;
- }
- protected static $libSetNth = array('list', 'n', 'value');
- protected function libSetNth($args)
- {
- $list = $this->coerceList($args[0]);
- $n = $this->assertNumber($args[1]);
- if ($n > 0) {
- $n--;
- } elseif ($n < 0) {
- $n += count($list[2]);
- }
- if (!isset($list[2][$n])) {
- $this->throwError('Invalid argument for "n"');
- return;
- }
- $list[2][$n] = $args[2];
- return $list;
- }
- protected static $libMapGet = array('map', 'key');
- protected function libMapGet($args)
- {
- $map = $this->assertMap($args[0]);
- $key = $this->compileStringContent($this->coerceString($args[1]));
- for ($i = count($map[1]) - 1; $i >= 0; $i--) {
- if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
- return $map[2][$i];
- }
- }
- return self::$null;
- }
- protected static $libMapKeys = array('map');
- protected function libMapKeys($args)
- {
- $map = $this->assertMap($args[0]);
- $keys = $map[1];
- return array(Type::T_LIST, ',', $keys);
- }
- protected static $libMapValues = array('map');
- protected function libMapValues($args)
- {
- $map = $this->assertMap($args[0]);
- $values = $map[2];
- return array(Type::T_LIST, ',', $values);
- }
- protected static $libMapRemove = array('map', 'key');
- protected function libMapRemove($args)
- {
- $map = $this->assertMap($args[0]);
- $key = $this->compileStringContent($this->coerceString($args[1]));
- for ($i = count($map[1]) - 1; $i >= 0; $i--) {
- if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
- array_splice($map[1], $i, 1);
- array_splice($map[2], $i, 1);
- }
- }
- return $map;
- }
- protected static $libMapHasKey = array('map', 'key');
- protected function libMapHasKey($args)
- {
- $map = $this->assertMap($args[0]);
- $key = $this->compileStringContent($this->coerceString($args[1]));
- for ($i = count($map[1]) - 1; $i >= 0; $i--) {
- if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
- return true;
- }
- }
- return false;
- }
- protected static $libMapMerge = array('map-1', 'map-2');
- protected function libMapMerge($args)
- {
- $map1 = $this->assertMap($args[0]);
- $map2 = $this->assertMap($args[1]);
- return array(Type::T_MAP, array_merge($map1[1], $map2[1]), array_merge($map1[2], $map2[2]));
- }
- protected static $libKeywords = array('args');
- protected function libKeywords($args)
- {
- $this->assertList($args[0]);
- $keys = array();
- $values = array();
- foreach ($args[0][2] as $name => $arg) {
- $keys[] = array(Type::T_KEYWORD, $name);
- $values[] = $arg;
- }
- return array(Type::T_MAP, $keys, $values);
- }
- protected function listSeparatorForJoin($list1, $sep)
- {
- if (!isset($sep)) {
- return $list1[1];
- }
- switch ($this->compileValue($sep)) {
- case 'comma':
- return ',';
- case 'space':
- return '';
- default:
- return $list1[1];
- }
- }
- protected static $libJoin = array('list1', 'list2', 'separator');
- protected function libJoin($args)
- {
- list($list1, $list2, $sep) = $args;
- $list1 = $this->coerceList($list1, ' ');
- $list2 = $this->coerceList($list2, ' ');
- $sep = $this->listSeparatorForJoin($list1, $sep);
- return array(Type::T_LIST, $sep, array_merge($list1[2], $list2[2]));
- }
- protected static $libAppend = array('list', 'val', 'separator');
- protected function libAppend($args)
- {
- list($list1, $value, $sep) = $args;
- $list1 = $this->coerceList($list1, ' ');
- $sep = $this->listSeparatorForJoin($list1, $sep);
- return array(Type::T_LIST, $sep, array_merge($list1[2], array($value)));
- }
- protected function libZip($args)
- {
- foreach ($args as $arg) {
- $this->assertList($arg);
- }
- $lists = array();
- $firstList = array_shift($args);
- foreach ($firstList[2] as $key => $item) {
- $list = array(Type::T_LIST, '', array($item));
- foreach ($args as $arg) {
- if (isset($arg[2][$key])) {
- $list[2][] = $arg[2][$key];
- } else {
- break 2;
- }
- }
- $lists[] = $list;
- }
- return array(Type::T_LIST, ',', $lists);
- }
- protected static $libTypeOf = array('value');
- protected function libTypeOf($args)
- {
- $value = $args[0];
- switch ($value[0]) {
- case Type::T_KEYWORD:
- if ($value === self::$true || $value === self::$false) {
- return 'bool';
- }
- if ($this->coerceColor($value)) {
- return 'color';
- }
- // fall-thru
- case Type::T_FUNCTION:
- return 'string';
- case Type::T_LIST:
- if (isset($value[3]) && $value[3]) {
- return 'arglist';
- }
- // fall-thru
- default:
- return $value[0];
- }
- }
- protected static $libUnit = array('number');
- protected function libUnit($args)
- {
- $num = $args[0];
- if ($num[0] === Type::T_NUMBER) {
- return array(Type::T_STRING, '"', array($num->unitStr()));
- }
- return '';
- }
- protected static $libUnitless = array('number');
- protected function libUnitless($args)
- {
- $value = $args[0];
- return $value[0] === Type::T_NUMBER && $value->unitless();
- }
- protected static $libComparable = array('number-1', 'number-2');
- protected function libComparable($args)
- {
- list($number1, $number2) = $args;
- if (!isset($number1[0]) || $number1[0] !== Type::T_NUMBER || !isset($number2[0]) || $number2[0] !== Type::T_NUMBER) {
- $this->throwError('Invalid argument(s) for "comparable"');
- return;
- }
- $number1 = $number1->normalize();
- $number2 = $number2->normalize();
- return $number1[2] === $number2[2] || $number1->unitless() || $number2->unitless();
- }
- protected static $libStrIndex = array('string', 'substring');
- protected function libStrIndex($args)
- {
- $string = $this->coerceString($args[0]);
- $stringContent = $this->compileStringContent($string);
- $substring = $this->coerceString($args[1]);
- $substringContent = $this->compileStringContent($substring);
- $result = strpos($stringContent, $substringContent);
- return $result === false ? self::$null : new Node\Number($result + 1, '');
- }
- protected static $libStrInsert = array('string', 'insert', 'index');
- protected function libStrInsert($args)
- {
- $string = $this->coerceString($args[0]);
- $stringContent = $this->compileStringContent($string);
- $insert = $this->coerceString($args[1]);
- $insertContent = $this->compileStringContent($insert);
- list(, $index) = $args[2];
- $string[2] = array(substr_replace($stringContent, $insertContent, $index - 1, 0));
- return $string;
- }
- protected static $libStrLength = array('string');
- protected function libStrLength($args)
- {
- $string = $this->coerceString($args[0]);
- $stringContent = $this->compileStringContent($string);
- return new Node\Number(strlen($stringContent), '');
- }
- protected static $libStrSlice = array('string', 'start-at', 'end-at');
- protected function libStrSlice($args)
- {
- if (isset($args[2]) && $args[2][1] == 0) {
- return self::$nullString;
- }
- $string = $this->coerceString($args[0]);
- $stringContent = $this->compileStringContent($string);
- $start = (int) $args[1][1];
- if ($start > 0) {
- $start--;
- }
- $end = (int) $args[2][1];
- $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end);
- $string[2] = $length ? array(substr($stringContent, $start, $length)) : array(substr($stringContent, $start));
- return $string;
- }
- protected static $libToLowerCase = array('string');
- protected function libToLowerCase($args)
- {
- $string = $this->coerceString($args[0]);
- $stringContent = $this->compileStringContent($string);
- $string[2] = array(mb_strtolower($stringContent));
- return $string;
- }
- protected static $libToUpperCase = array('string');
- protected function libToUpperCase($args)
- {
- $string = $this->coerceString($args[0]);
- $stringContent = $this->compileStringContent($string);
- $string[2] = array(mb_strtoupper($stringContent));
- return $string;
- }
- protected static $libFeatureExists = array('feature');
- protected function libFeatureExists($args)
- {
- $string = $this->coerceString($args[0]);
- $name = $this->compileStringContent($string);
- return $this->toBool(array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false);
- }
- protected static $libFunctionExists = array('name');
- protected function libFunctionExists($args)
- {
- $string = $this->coerceString($args[0]);
- $name = $this->compileStringContent($string);
- // user defined functions
- if ($this->has(self::$namespaces['function'] . $name)) {
- return true;
- }
- $name = $this->normalizeName($name);
- if (isset($this->userFunctions[$name])) {
- return true;
- }
- // built-in functions
- $f = $this->getBuiltinFunction($name);
- return $this->toBool(is_callable($f));
- }
- protected static $libGlobalVariableExists = array('name');
- protected function libGlobalVariableExists($args)
- {
- $string = $this->coerceString($args[0]);
- $name = $this->compileStringContent($string);
- return $this->has($name, $this->rootEnv);
- }
- protected static $libMixinExists = array('name');
- protected function libMixinExists($args)
- {
- $string = $this->coerceString($args[0]);
- $name = $this->compileStringContent($string);
- return $this->has(self::$namespaces['mixin'] . $name);
- }
- protected static $libVariableExists = array('name');
- protected function libVariableExists($args)
- {
- $string = $this->coerceString($args[0]);
- $name = $this->compileStringContent($string);
- return $this->has($name);
- }
- /**
- * Workaround IE7's content counter bug.
- *
- * @param array $args
- */
- protected function libCounter($args)
- {
- $list = array_map(array($this, 'compileValue'), $args);
- return array(Type::T_STRING, '', array('counter(' . implode(',', $list) . ')'));
- }
- protected static $libRandom = array('limit');
- protected function libRandom($args)
- {
- if (isset($args[0])) {
- $n = $this->assertNumber($args[0]);
- if ($n < 1) {
- $this->throwError('limit must be greater than or equal to 1');
- return;
- }
- return new Node\Number(mt_rand(1, $n), '');
- }
- return new Node\Number(mt_rand(1, mt_getrandmax()), '');
- }
- protected function libUniqueId()
- {
- static $id;
- if (!isset($id)) {
- $id = mt_rand(0, pow(36, 8));
- }
- $id += mt_rand(0, 10) + 1;
- return array(Type::T_STRING, '', array('u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)));
- }
- protected static $libInspect = array('value');
- protected function libInspect($args)
- {
- if ($args[0] === self::$null) {
- return array(Type::T_KEYWORD, 'null');
- }
- return $args[0];
- }
- }
|