PageRenderTime 221ms CodeModel.GetById 4ms app.highlight 69ms RepoModel.GetById 107ms app.codeStats 3ms

/scss.inc.php

https://bitbucket.org/Pathou/template
PHP | 3759 lines | 3079 code | 565 blank | 115 comment | 633 complexity | 4ea5a15e81cc8d4b90a4177aaf6f3605 MD5 | raw file

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

   1<?php
   2// http://leafo.net/scssphp/docs/
   3class scssc {
   4	static public $VERSION = "v0.0.4";
   5
   6	static protected $operatorNames = array(
   7		'+' => "add",
   8		'-' => "sub",
   9		'*' => "mul",
  10		'/' => "div",
  11		'%' => "mod",
  12
  13		'==' => "eq",
  14		'!=' => "neq",
  15		'<' => "lt",
  16		'>' => "gt",
  17
  18		'<=' => "lte",
  19		'>=' => "gte",
  20	);
  21
  22	static protected $namespaces = array(
  23		"special" => "%",
  24		"mixin" => "@",
  25		"function" => "^",
  26	);
  27
  28	static protected $numberPrecision = 3;
  29	static protected $unitTable = array(
  30		"in" => array(
  31			"in" => 1,
  32			"pt" => 72,
  33			"pc" => 6,
  34			"cm" => 2.54,
  35			"mm" => 25.4,
  36			"px" => 96,
  37		)
  38	);
  39
  40	static public $true = array("keyword", "true");
  41	static public $false = array("keyword", "false");
  42
  43	static public $defaultValue = array("keyword", "");
  44	static public $selfSelector = array("self");
  45
  46	protected $importPaths = array("");
  47	protected $importCache = array();
  48
  49	protected $userFunctions = array();
  50
  51	protected $formatter = "scss_formatter_nested";
  52
  53	function compile($code, $name=null) {
  54		$this->indentLevel = -1;
  55		$this->commentsSeen = array();
  56		$this->extends = array();
  57		$this->extendsMap = array();
  58
  59		$locale = setlocale(LC_NUMERIC, 0);
  60		setlocale(LC_NUMERIC, "C");
  61
  62		$this->parsedFiles = array();
  63		$this->parser = new scss_parser($name);
  64		$tree = $this->parser->parse($code);
  65
  66		$this->formatter = new $this->formatter();
  67
  68		$this->env = null;
  69		$this->scope = null;
  70
  71		$this->compileRoot($tree);
  72		$this->flattenSelectors($this->scope);
  73
  74		ob_start();
  75		$this->formatter->block($this->scope);
  76		$out = ob_get_clean();
  77
  78		setlocale(LC_NUMERIC, $locale);
  79		return $out;
  80	}
  81
  82	protected function pushExtends($target, $origin) {
  83		$i = count($this->extends);
  84		$this->extends[] = array($target, $origin);
  85
  86		foreach ($target as $part) {
  87			if (isset($this->extendsMap[$part])) {
  88				$this->extendsMap[$part][] = $i;
  89			} else {
  90				$this->extendsMap[$part] = array($i);
  91			}
  92		}
  93	}
  94
  95	protected function makeOutputBlock($type, $selectors = null) {
  96		$out = new stdclass;
  97		$out->type = $type;
  98		$out->lines = array();
  99		$out->children = array();
 100		$out->parent = $this->scope;
 101		$out->selectors = $selectors;
 102		$out->depth = $this->env->depth;
 103
 104		return $out;
 105	}
 106
 107	protected function matchExtendsSingle($single, &$out_origin, &$out_rem) {
 108		$counts = array();
 109		foreach ($single as $part) {
 110			if (!is_string($part)) return false; // hmm
 111
 112			if (isset($this->extendsMap[$part])) {
 113				foreach ($this->extendsMap[$part] as $idx) {
 114					$counts[$idx] =
 115						isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
 116				}
 117			}
 118		}
 119
 120		foreach ($counts as $idx => $count) {
 121			list($target, $origin) = $this->extends[$idx];
 122			// check count
 123			if ($count != count($target)) continue;
 124			// check if target is subset of single
 125			if (array_diff(array_intersect($single, $target), $target)) continue;
 126
 127			$out_origin = $origin;
 128			$out_rem = array_diff($single, $target);
 129
 130			return true;
 131		}
 132
 133		return false;
 134	}
 135
 136	protected function combineSelectorSingle($base, $other) {
 137		$tag = null;
 138		$out = array();
 139
 140		foreach (array($base, $other) as $single) {
 141			foreach ($single as $part) {
 142				if (preg_match('/^[^.#:]/', $part)) {
 143					$tag = $part;
 144				} else {
 145					$out[] = $part;
 146				}
 147			}
 148		}
 149
 150		if ($tag) {
 151			array_unshift($out, $tag);
 152		}
 153
 154		return $out;
 155	}
 156
 157	protected function matchExtends($selector, &$out, $from = 0, $initial=true) {
 158		foreach ($selector as $i => $part) {
 159			if ($i < $from) continue;
 160
 161			if ($this->matchExtendsSingle($part, $origin, $rem)) {
 162				$before = array_slice($selector, 0, $i);
 163				$after = array_slice($selector, $i + 1);
 164
 165				foreach ($origin as $new) {
 166					$new[count($new) - 1] =
 167						$this->combineSelectorSingle(end($new), $rem);
 168
 169					$k = 0;
 170					// remove shared parts
 171					if ($initial) {
 172						foreach ($before as $k => $val) {
 173							if (!isset($new[$k]) || $val != $new[$k]) {
 174								break;
 175							}
 176						}
 177					}
 178
 179					$result = array_merge(
 180						$before,
 181						$k > 0 ? array_slice($new, $k) : $new,
 182						$after);
 183
 184
 185					if ($result == $selector) continue;
 186					$out[] = $result;
 187
 188					// recursively check for more matches
 189					$this->matchExtends($result, $out, $i, false);
 190
 191					// selector sequence merging
 192					if (!empty($before) && count($new) > 1) {
 193						$result2 = array_merge(
 194							array_slice($new, 0, -1),
 195							$k > 0 ? array_slice($before, $k) : $before,
 196							array_slice($new, -1),
 197							$after);
 198
 199						$out[] = $result2;
 200					}
 201				}
 202			}
 203		}
 204	}
 205
 206	protected function flattenSelectors($block) {
 207		if ($block->selectors) {
 208			$selectors = array();
 209			foreach ($block->selectors as $s) {
 210				$selectors[] = $s;
 211				if (!is_array($s)) continue;
 212				// check extends
 213				if (!empty($this->extendsMap)) {
 214					$this->matchExtends($s, $selectors);
 215				}
 216			}
 217
 218			$selectors = array_map(array($this, "compileSelector"), $selectors);
 219			$block->selectors = $selectors;
 220		}
 221
 222		foreach ($block->children as $child) {
 223			$this->flattenSelectors($child);
 224		}
 225	}
 226
 227	protected function compileRoot($rootBlock) {
 228		$this->pushEnv($rootBlock);
 229		$this->scope = $this->makeOutputBlock("root");
 230		$this->compileChildren($rootBlock->children, $this->scope);
 231		$this->popEnv();
 232	}
 233
 234	protected function compileMedia($media) {
 235		$this->pushEnv($media);
 236		$parentScope = $this->mediaParent($this->scope);
 237
 238		$this->scope = $this->makeOutputBlock("media", array(
 239			$this->compileMediaQuery($this->multiplyMedia($this->env)))
 240		);
 241
 242		$parentScope->children[] = $this->scope;
 243
 244		$this->compileChildren($media->children, $this->scope);
 245
 246		$this->scope = $this->scope->parent;
 247		$this->popEnv();
 248	}
 249
 250	protected function mediaParent($scope) {
 251		while (!empty($scope->parent)) {
 252			if (!empty($scope->type) && $scope->type != "media") {
 253				break;
 254			}
 255			$scope = $scope->parent;
 256		}
 257
 258		return $scope;
 259	}
 260
 261	// TODO refactor compileNestedBlock and compileMedia into same thing
 262	protected function compileNestedBlock($block, $selectors) {
 263		$this->pushEnv($block);
 264
 265		$this->scope = $this->makeOutputBlock($block->type, $selectors);
 266		$this->scope->parent->children[] = $this->scope;
 267		$this->compileChildren($block->children, $this->scope);
 268
 269		$this->scope = $this->scope->parent;
 270		$this->popEnv();
 271	}
 272
 273	protected function compileBlock($block) {
 274		$env = $this->pushEnv($block);
 275
 276		$env->selectors =
 277			array_map(array($this, "evalSelector"), $block->selectors);
 278
 279		$out = $this->makeOutputBlock(null, $this->multiplySelectors($env));
 280		$this->scope->children[] = $out;
 281		$this->compileChildren($block->children, $out);
 282
 283		$this->popEnv();
 284	}
 285
 286	// joins together .classes and #ids
 287	protected function flattenSelectorSingle($single) {
 288		$joined = array();
 289		foreach ($single as $part) {
 290			if (empty($joined) ||
 291				!is_string($part) ||
 292				preg_match('/[.:#]/', $part))
 293			{
 294				$joined[] = $part;
 295				continue;
 296			}
 297
 298			if (is_array(end($joined))) {
 299				$joined[] = $part;
 300			} else {
 301				$joined[count($joined) - 1] .= $part;
 302			}
 303		}
 304
 305		return $joined;
 306	}
 307
 308	// replaces all the interpolates
 309	protected function evalSelector($selector) {
 310		return array_map(array($this, "evalSelectorPart"), $selector);
 311	}
 312
 313	protected function evalSelectorPart($piece) {
 314		foreach ($piece as &$p) {
 315			if (!is_array($p)) continue;
 316
 317			switch ($p[0]) {
 318			case "interpolate":
 319				$p = $this->compileValue($p);
 320				break;
 321			}
 322		}
 323
 324		return $this->flattenSelectorSingle($piece);
 325	}
 326
 327	// compiles to string
 328	// self(&) should have been replaced by now
 329	protected function compileSelector($selector) {
 330		if (!is_array($selector)) return $selector; // media and the like
 331
 332		return implode(" ", array_map(
 333			array($this, "compileSelectorPart"), $selector));
 334	}
 335
 336	protected function compileSelectorPart($piece) {
 337		foreach ($piece as &$p) {
 338			if (!is_array($p)) continue;
 339
 340			switch ($p[0]) {
 341			case "self":
 342				$p = "&";
 343				break;
 344			default:
 345				$p = $this->compileValue($p);
 346				break;
 347			}
 348		}
 349
 350		return implode($piece);
 351	}
 352
 353	protected function compileChildren($stms, $out) {
 354		foreach ($stms as $stm) {
 355			$ret = $this->compileChild($stm, $out);
 356			if (!is_null($ret)) return $ret;
 357		}
 358	}
 359
 360	protected function compileMediaQuery($queryList) {
 361		$out = "@media";
 362		$first = true;
 363		foreach ($queryList as $query){
 364			$parts = array();
 365			foreach ($query as $q) {
 366				switch ($q[0]) {
 367					case "mediaType":
 368						$parts[] = implode(" ", array_slice($q, 1));
 369						break;
 370					case "mediaExp":
 371						if (isset($q[2])) {
 372							$parts[] = "($q[1]" . $this->formatter->assignSeparator . $this->compileValue($q[2]) . ")";
 373						} else {
 374							$parts[] = "($q[1])";
 375						}
 376						break;
 377				}
 378			}
 379			if (!empty($parts)) {
 380				if ($first) {
 381					$first = false;
 382					$out .= " ";
 383				} else {
 384					$out .= $this->formatter->tagSeparator;
 385				}
 386				$out .= implode(" and ", $parts);
 387			}
 388		}
 389		return $out;
 390	}
 391
 392	// returns true if the value was something that could be imported
 393	protected function compileImport($rawPath, $out) {
 394		if ($rawPath[0] == "string") {
 395			$path = $this->compileStringContent($rawPath);
 396			if ($path = $this->findImport($path)) {
 397				$this->importFile($path, $out);
 398				return true;
 399			}
 400			return false;
 401		} if ($rawPath[0] == "list") {
 402			// handle a list of strings
 403			if (count($rawPath[2]) == 0) return false;
 404			foreach ($rawPath[2] as $path) {
 405				if ($path[0] != "string") return false;
 406			}
 407
 408			foreach ($rawPath[2] as $path) {
 409				$this->compileImport($path, $out);
 410			}
 411
 412			return true;
 413		}
 414
 415		return false;
 416	}
 417
 418	// return a value to halt execution
 419	protected function compileChild($child, $out) {
 420		switch ($child[0]) {
 421		case "import":
 422			list(,$rawPath) = $child;
 423			$rawPath = $this->reduce($rawPath);
 424			if (!$this->compileImport($rawPath, $out)) {
 425				$out->lines[] = "@import " . $this->compileValue($rawPath) . ";";
 426			}
 427			break;
 428		case "directive":
 429			list(, $directive) = $child;
 430			$s = "@" . $directive->name;
 431			if (!empty($directive->value)) {
 432				$s .= " " . $this->compileValue($directive->value);
 433			}
 434			$this->compileNestedBlock($directive, array($s));
 435			break;
 436		case "media":
 437			$this->compileMedia($child[1]);
 438			break;
 439		case "block":
 440			$this->compileBlock($child[1]);
 441			break;
 442		case "charset":
 443			$out->lines[] = "@charset ".$this->compileValue($child[1]).";";
 444			break;
 445		case "assign":
 446			list(,$name, $value) = $child;
 447			if ($name[0] == "var") {
 448				$isDefault = !empty($child[3]);
 449				if (!$isDefault || $this->get($name[1], true) === true) {
 450					$this->set($name[1], $this->reduce($value));
 451				}
 452				break;
 453			}
 454
 455			$out->lines[] = $this->formatter->property(
 456				$this->compileValue($child[1]),
 457				$this->compileValue($child[2]));
 458			break;
 459		case "comment":
 460			$out->lines[] = $child[1];
 461			break;
 462		case "mixin":
 463		case "function":
 464			list(,$block) = $child;
 465			$this->set(self::$namespaces[$block->type] . $block->name, $block);
 466			break;
 467		case "extend":
 468			list(, $selectors) = $child;
 469			foreach ($selectors as $sel) {
 470				// only use the first one
 471				$sel = current($this->evalSelector($sel));
 472				$this->pushExtends($sel, $out->selectors);
 473			}
 474			break;
 475		case "if":
 476			list(, $if) = $child;
 477			if ($this->reduce($if->cond, true) != self::$false) {
 478				return $this->compileChildren($if->children, $out);
 479			} else {
 480				foreach ($if->cases as $case) {
 481					if ($case->type == "else" ||
 482						$case->type == "elseif" && ($this->reduce($case->cond) != self::$false))
 483					{
 484						return $this->compileChildren($case->children, $out);
 485					}
 486				}
 487			}
 488			break;
 489		case "return":
 490			return $this->reduce($child[1], true);
 491		case "each":
 492			list(,$each) = $child;
 493			$list = $this->reduce($this->coerceList($each->list));
 494			foreach ($list[2] as $item) {
 495				$this->pushEnv();
 496				$this->set($each->var, $item);
 497				// TODO: allow return from here
 498				$this->compileChildren($each->children, $out);
 499				$this->popEnv();
 500			}
 501			break;
 502		case "while":
 503			list(,$while) = $child;
 504			while ($this->reduce($while->cond, true) != self::$false) {
 505				$ret = $this->compileChildren($while->children, $out);
 506				if ($ret) return $ret;
 507			}
 508			break;
 509		case "for":
 510			list(,$for) = $child;
 511			$start = $this->reduce($for->start, true);
 512			$start = $start[1];
 513			$end = $this->reduce($for->end, true);
 514			$end = $end[1];
 515			$d = $start < $end ? 1 : -1;
 516
 517			while (true) {
 518				if ((!$for->until && $start - $d == $end) ||
 519					($for->until && $start == $end))
 520				{
 521					break;
 522				}
 523
 524				$this->set($for->var, array("number", $start, ""));
 525				$start += $d;
 526
 527				$ret = $this->compileChildren($for->children, $out);
 528				if ($ret) return $ret;
 529			}
 530
 531			break;
 532		case "nestedprop":
 533			list(,$prop) = $child;
 534			$prefixed = array();
 535			$prefix = $this->compileValue($prop->prefix) . "-";
 536			foreach ($prop->children as $child) {
 537				if ($child[0] == "assign") {
 538					array_unshift($child[1][2], $prefix);
 539				}
 540				if ($child[0] == "nestedprop") {
 541					array_unshift($child[1]->prefix[2], $prefix);
 542				}
 543				$prefixed[] = $child;
 544			}
 545			$this->compileChildren($prefixed, $out);
 546			break;
 547		case "include": // including a mixin
 548			list(,$name, $argValues, $content) = $child;
 549			$mixin = $this->get(self::$namespaces["mixin"] . $name, false);
 550			if (!$mixin) break; // throw error?
 551
 552			$callingScope = $this->env;
 553
 554			// push scope, apply args
 555			$this->pushEnv();
 556
 557			if (!is_null($content)) {
 558				$content->scope = $callingScope;
 559				$this->setRaw(self::$namespaces["special"] . "content", $content);
 560			}
 561
 562			if (!is_null($mixin->args)) {
 563				$this->applyArguments($mixin->args, $argValues);
 564			}
 565
 566			foreach ($mixin->children as $child) {
 567				$this->compileChild($child, $out);
 568			}
 569
 570			$this->popEnv();
 571
 572			break;
 573		case "mixin_content":
 574			$content = $this->get(self::$namespaces["special"] . "content");
 575			if (is_null($content)) {
 576				throw new Exception("Unexpected @content inside of mixin");
 577			}
 578
 579			$this->storeEnv = $content->scope;
 580
 581			foreach ($content->children as $child) {
 582				$this->compileChild($child, $out);
 583			}
 584
 585			unset($this->storeEnv);
 586			break;
 587		case "debug":
 588			list(,$value, $pos) = $child;
 589			$line = $this->parser->getLineNo($pos);
 590			$value = $this->compileValue($this->reduce($value, true));
 591			fwrite(STDERR, "Line $line DEBUG: $value\n");
 592			break;
 593		default:
 594			throw new exception("unknown child type: $child[0]");
 595		}
 596	}
 597
 598	protected function expToString($exp) {
 599		list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
 600		$content = array($left);
 601		if ($whiteLeft) $content[] = " ";
 602		$content[] = $op;
 603		if ($whiteRight) $content[] = " ";
 604		$content[] = $right;
 605		return array("string", "", $content);
 606	}
 607
 608	// should $value cause its operand to eval
 609	protected function shouldEval($value) {
 610		switch ($value[0]) {
 611		case "exp":
 612			if ($value[1] == "/") {
 613				return $this->shouldEval($value[2], $value[3]);
 614			}
 615		case "var":
 616		case "fncall":
 617			return true;
 618		}
 619		return false;
 620	}
 621
 622	protected function reduce($value, $inExp = false) {
 623		list($type) = $value;
 624		switch ($type) {
 625			case "exp":
 626				list(, $op, $left, $right, $inParens) = $value;
 627				$opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op;
 628
 629				$inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
 630
 631				$left = $this->reduce($left, true);
 632				$right = $this->reduce($right, true);
 633
 634				// only do division in special cases
 635				if ($opName == "div" && !$inParens && !$inExp) {
 636					if ($left[0] != "color" && $right[0] != "color") {
 637						return $this->expToString($value);
 638					}
 639				}
 640
 641				$left = $this->coerceForExpression($left);
 642				$right = $this->coerceForExpression($right);
 643
 644				$ltype = $left[0];
 645				$rtype = $right[0];
 646
 647				// this tries:
 648				// 1. op_[op name]_[left type]_[right type]
 649				// 2. op_[left type]_[right type] (passing the op as first arg
 650				// 3. op_[op name]
 651				$fn = "op_${opName}_${ltype}_${rtype}";
 652				if (is_callable(array($this, $fn)) ||
 653					(($fn = "op_${ltype}_${rtype}") &&
 654						is_callable(array($this, $fn)) &&
 655						$passOp = true) ||
 656					(($fn = "op_${opName}") &&
 657						is_callable(array($this, $fn)) &&
 658						$genOp = true))
 659				{
 660					$unitChange = false;
 661					if (!isset($genOp) &&
 662						$left[0] == "number" && $right[0] == "number")
 663					{
 664						if ($opName == "mod" && $right[2] != "") {
 665							throw new Exception(sprintf('Cannot modulo by a number with units: %s%s.', $right[1], $right[2]));
 666						}
 667
 668						$unitChange = true;
 669						$emptyUnit = $left[2] == "" || $right[2] == "";
 670						$targetUnit = "" != $left[2] ? $left[2] : $right[2];
 671
 672						if ($opName != "mul") {
 673							$left[2] = "" != $left[2] ? $left[2] : $targetUnit;
 674							$right[2] = "" != $right[2] ? $right[2] : $targetUnit;
 675						}
 676
 677						if ($opName != "mod") {
 678							$left = $this->normalizeNumber($left);
 679							$right = $this->normalizeNumber($right);
 680						}
 681
 682						if ($opName == "div" && !$emptyUnit && $left[2] == $right[2]) {
 683							$targetUnit = "";
 684						}
 685
 686						if ($opName == "mul") {
 687							$left[2] = "" != $left[2] ? $left[2] : $right[2];
 688							$right[2] = "" != $right[2] ? $right[2] : $left[2];
 689						} elseif ($opName == "div" && $left[2] == $right[2]) {
 690							$left[2] = "";
 691							$right[2] = "";
 692						}
 693					}
 694
 695					$shouldEval = $inParens || $inExp;
 696					if (isset($passOp)) {
 697						$out = $this->$fn($op, $left, $right, $shouldEval);
 698					} else {
 699						$out = $this->$fn($left, $right, $shouldEval);
 700					}
 701
 702					if (!is_null($out)) {
 703						if ($unitChange && $out[0] == "number") {
 704							$out = $this->coerceUnit($out, $targetUnit);
 705						}
 706						return $out;
 707					}
 708				}
 709
 710				return $this->expToString($value);
 711			case "unary":
 712				list(, $op, $exp, $inParens) = $value;
 713				$inExp = $inExp || $this->shouldEval($exp);
 714
 715				$exp = $this->reduce($exp);
 716				if ($exp[0] == "number") {
 717					switch ($op) {
 718					case "+":
 719						return $exp;
 720					case "-":
 721						$exp[1] *= -1;
 722						return $exp;
 723					}
 724				}
 725
 726				if ($op == "not") {
 727					if ($inExp || $inParens) {
 728						if ($exp == self::$false) {
 729							return self::$true;
 730						} else {
 731							return self::$false;
 732						}
 733					} else {
 734						$op = $op . " ";
 735					}
 736				}
 737
 738				return array("string", "", array($op, $exp));
 739			case "var":
 740				list(, $name) = $value;
 741				return $this->reduce($this->get($name));
 742			case "list":
 743				foreach ($value[2] as &$item) {
 744					$item = $this->reduce($item);
 745				}
 746				return $value;
 747			case "string":
 748				foreach ($value[2] as &$item) {
 749					if (is_array($item)) {
 750						$item = $this->reduce($item);
 751					}
 752				}
 753				return $value;
 754			case "interpolate":
 755				$value[1] = $this->reduce($value[1]);
 756				return $value;
 757			case "fncall":
 758				list(,$name, $argValues) = $value;
 759
 760				// user defined function?
 761				$func = $this->get(self::$namespaces["function"] . $name, false);
 762				if ($func) {
 763					$this->pushEnv();
 764
 765					// set the args
 766					if (isset($func->args)) {
 767						$this->applyArguments($func->args, $argValues);
 768					}
 769
 770					// throw away lines and children
 771					$tmp = (object)array(
 772						"lines" => array(),
 773						"children" => array()
 774					);
 775					$ret = $this->compileChildren($func->children, $tmp);
 776					$this->popEnv();
 777
 778					return is_null($ret) ? self::$defaultValue : $ret;
 779				}
 780
 781				// built in function
 782				if ($this->callBuiltin($name, $argValues, $returnValue)) {
 783					return $returnValue;
 784				}
 785
 786				// need to flatten the arguments into a list
 787				$listArgs = array();
 788				foreach ((array)$argValues as $arg) {
 789					if (empty($arg[0])) {
 790						$listArgs[] = $this->reduce($arg[1]);
 791					}
 792				}
 793				return array("function", $name, array("list", ",", $listArgs));
 794			default:
 795				return $value;
 796		}
 797	}
 798
 799	// just does physical lengths for now
 800	protected function normalizeNumber($number) {
 801		list(, $value, $unit) = $number;
 802		if (isset(self::$unitTable["in"][$unit])) {
 803			$conv = self::$unitTable["in"][$unit];
 804			return array("number", $value / $conv, "in");
 805		}
 806		return $number;
 807	}
 808
 809	// $number should be normalized
 810	protected function coerceUnit($number, $unit) {
 811		list(, $value, $baseUnit) = $number;
 812		if (isset(self::$unitTable[$baseUnit][$unit])) {
 813			$value = $value * self::$unitTable[$baseUnit][$unit];
 814		}
 815
 816		return array("number", $value, $unit);
 817	}
 818
 819	protected function op_add_number_number($left, $right) {
 820		return array("number", $left[1] + $right[1], $left[2]);
 821	}
 822
 823	protected function op_mul_number_number($left, $right) {
 824		return array("number", $left[1] * $right[1], $left[2]);
 825	}
 826
 827	protected function op_sub_number_number($left, $right) {
 828		return array("number", $left[1] - $right[1], $left[2]);
 829	}
 830
 831	protected function op_div_number_number($left, $right) {
 832		return array("number", $left[1] / $right[1], $left[2]);
 833	}
 834
 835	protected function op_mod_number_number($left, $right) {
 836		return array("number", $left[1] % $right[1], $left[2]);
 837	}
 838
 839	// adding strings
 840	protected function op_add($left, $right) {
 841		if ($strLeft = $this->coerceString($left)) {
 842			if ($right[0] == "string") {
 843				$right[1] = "";
 844			}
 845			$strLeft[2][] = $right;
 846			return $strLeft;
 847		}
 848
 849		if ($strRight = $this->coerceString($right)) {
 850			if ($left[0] == "string") {
 851				$left[1] = "";
 852			}
 853			array_unshift($strRight[2], $left);
 854			return $strRight;
 855		}
 856	}
 857
 858	protected function op_and($left, $right, $shouldEval) {
 859		if (!$shouldEval) return;
 860		if ($left != self::$false) return $right;
 861		return $left;
 862	}
 863
 864	protected function op_or($left, $right, $shouldEval) {
 865		if (!$shouldEval) return;
 866		if ($left != self::$false) return $left;
 867		return $right;
 868	}
 869
 870	protected function op_color_color($op, $left, $right) {
 871		$out = array('color');
 872		foreach (range(1, 3) as $i) {
 873			$lval = isset($left[$i]) ? $left[$i] : 0;
 874			$rval = isset($right[$i]) ? $right[$i] : 0;
 875			switch ($op) {
 876			case '+':
 877				$out[] = $lval + $rval;
 878				break;
 879			case '-':
 880				$out[] = $lval - $rval;
 881				break;
 882			case '*':
 883				$out[] = $lval * $rval;
 884				break;
 885			case '%':
 886				$out[] = $lval % $rval;
 887				break;
 888			case '/':
 889				if ($rval == 0) {
 890					throw new exception("color: Can't divide by zero");
 891				}
 892				$out[] = $lval / $rval;
 893				break;
 894			default:
 895				throw new exception("color: unknow op $op");
 896			}
 897		}
 898
 899		if (isset($left[4])) $out[4] = $left[4];
 900		elseif (isset($right[4])) $out[4] = $right[4];
 901
 902		return $this->fixColor($out);
 903	}
 904
 905	protected function op_color_number($op, $left, $right) {
 906		$value = $right[1];
 907		return $this->op_color_color($op, $left,
 908			array("color", $value, $value, $value));
 909	}
 910
 911	protected function op_number_color($op, $left, $right) {
 912		$value = $left[1];
 913		return $this->op_color_color($op,
 914			array("color", $value, $value, $value), $right);
 915	}
 916
 917	protected function op_eq($left, $right) {
 918		if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
 919			$lStr[1] = "";
 920			$rStr[1] = "";
 921			return $this->toBool($this->compileValue($lStr) == $this->compileValue($rStr));
 922		}
 923
 924		return $this->toBool($left == $right);
 925	}
 926
 927	protected function op_neq($left, $right) {
 928		return $this->toBool($left != $right);
 929	}
 930
 931	protected function op_gte_number_number($left, $right) {
 932		return $this->toBool($left[1] >= $right[1]);
 933	}
 934
 935	protected function op_gt_number_number($left, $right) {
 936		return $this->toBool($left[1] > $right[1]);
 937	}
 938
 939	protected function op_lte_number_number($left, $right) {
 940		return $this->toBool($left[1] <= $right[1]);
 941	}
 942
 943	protected function op_lt_number_number($left, $right) {
 944		return $this->toBool($left[1] < $right[1]);
 945	}
 946
 947	protected function toBool($thing) {
 948		return $thing ? self::$true : self::$false;
 949	}
 950
 951	protected function compileValue($value) {
 952		$value = $this->reduce($value);
 953
 954		list($type) = $value;
 955		switch ($type) {
 956		case "keyword":
 957			return $value[1];
 958		case "color":
 959			// [1] - red component (either number for a %)
 960			// [2] - green component
 961			// [3] - blue component
 962			// [4] - optional alpha component
 963			list(, $r, $g, $b) = $value;
 964
 965			$r = round($r);
 966			$g = round($g);
 967			$b = round($b);
 968
 969			if (count($value) == 5 && $value[4] != 1) { // rgba
 970				return 'rgba('.$r.', '.$g.', '.$b.', '.$value[4].')';
 971			}
 972
 973			$h = sprintf("#%02x%02x%02x", $r, $g, $b);
 974
 975			// Converting hex color to short notation (e.g. #003399 to #039)
 976			if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
 977				$h = '#' . $h[1] . $h[3] . $h[5];
 978			}
 979
 980			return $h;
 981		case "number":
 982			return round($value[1], self::$numberPrecision) . $value[2];
 983		case "string":
 984			return $value[1] . $this->compileStringContent($value) . $value[1];
 985		case "function":
 986			$args = !empty($value[2]) ? $this->compileValue($value[2]) : "";
 987			return "$value[1]($args)";
 988		case "list":
 989			$value = $this->extractInterpolation($value);
 990			if ($value[0] != "list") return $this->compileValue($value);
 991
 992			list(, $delim, $items) = $value;
 993			foreach ($items as &$item) {
 994				$item = $this->compileValue($item);
 995			}
 996			return implode("$delim ", $items);
 997		case "interpolated": # node created by extractInterpolation
 998			list(, $interpolate, $left, $right) = $value;
 999			list(,, $whiteLeft, $whiteRight) = $interpolate;
1000
1001			$left = count($left[2]) > 0 ?
1002				$this->compileValue($left).$whiteLeft : "";
1003
1004			$right = count($right[2]) > 0 ?
1005				$whiteRight.$this->compileValue($right) : "";
1006
1007			return $left.$this->compileValue($interpolate).$right;
1008
1009		case "interpolate": # raw parse node
1010			list(, $exp) = $value;
1011
1012			// strip quotes if it's a string
1013			$reduced = $this->reduce($exp);
1014			if ($reduced[0] == "string") {
1015				$reduced = array("keyword",
1016					$this->compileStringContent($reduced));
1017			}
1018
1019			return $this->compileValue($reduced);
1020		default:
1021			throw new exception("unknown value type: $type");
1022		}
1023	}
1024
1025	protected function compileStringContent($string) {
1026		$parts = array();
1027		foreach ($string[2] as $part) {
1028			if (is_array($part)) {
1029				$parts[] = $this->compileValue($part);
1030			} else {
1031				$parts[] = $part;
1032			}
1033		}
1034
1035		return implode($parts);
1036	}
1037
1038	// doesn't need to be recursive, compileValue will handle that
1039	protected function extractInterpolation($list) {
1040		$items = $list[2];
1041		foreach ($items as $i => $item) {
1042			if ($item[0] == "interpolate") {
1043				$before = array("list", $list[1], array_slice($items, 0, $i));
1044				$after = array("list", $list[1], array_slice($items, $i + 1));
1045				return array("interpolated", $item, $before, $after);
1046			}
1047		}
1048		return $list;
1049	}
1050
1051	// find the final set of selectors
1052	protected function multiplySelectors($env, $childSelectors = null) {
1053		if (is_null($env)) {
1054			return $childSelectors;
1055		}
1056
1057		// skip env, has no selectors
1058		if (empty($env->selectors)) {
1059			return $this->multiplySelectors($env->parent, $childSelectors);
1060		}
1061
1062		if (is_null($childSelectors)) {
1063			$selectors = $env->selectors;
1064		} else {
1065			$selectors = array();
1066			foreach ($env->selectors as $parent) {
1067				foreach ($childSelectors as $child) {
1068					$selectors[] = $this->joinSelectors($parent, $child);
1069				}
1070			}
1071		}
1072
1073		return $this->multiplySelectors($env->parent, $selectors);
1074	}
1075
1076	// looks for & to replace, or append parent before child
1077	protected function joinSelectors($parent, $child) {
1078		$setSelf = false;
1079		$out = array();
1080		foreach ($child as $part) {
1081			$newPart = array();
1082			foreach ($part as $p) {
1083				if ($p == self::$selfSelector) {
1084					$setSelf = true;
1085					foreach ($parent as $i => $parentPart) {
1086						if ($i > 0) {
1087							$out[] = $newPart;
1088							$newPart = array();
1089						}
1090
1091						foreach ($parentPart as $pp) {
1092							$newPart[] = $pp;
1093						}
1094					}
1095				} else {
1096					$newPart[] = $p;
1097				}
1098			}
1099
1100			$out[] = $newPart;
1101		}
1102
1103		return $setSelf ? $out : array_merge($parent, $child);
1104	}
1105
1106	protected function multiplyMedia($env, $childQueries = null) {
1107		if (is_null($env) ||
1108			!empty($env->block->type) && $env->block->type != "media")
1109		{
1110			return $childQueries;
1111		}
1112
1113		// plain old block, skip
1114		if (empty($env->block->type)) {
1115			return $this->multiplyMedia($env->parent, $childQueries);
1116		}
1117
1118		$parentQueries = $env->block->queryList;
1119		if ($childQueries == null) {
1120			$childQueries = $parentQueries;
1121		} else {
1122			$originalQueries = $childQueries;
1123			$childQueries = array();
1124
1125			foreach ($parentQueries as $parentQuery){
1126				foreach ($originalQueries as $childQuery) {
1127					$childQueries []= array_merge($parentQuery, $childQuery);
1128				}
1129			}
1130		}
1131
1132		return $this->multiplyMedia($env->parent, $childQueries);
1133	}
1134
1135	// convert something to list
1136	protected function coerceList($item, $delim = ",") {
1137		if (!is_null($item) && $item[0] == "list") {
1138			return $item;
1139		}
1140
1141		return array("list", $delim, is_null($item) ? array(): array($item));
1142	}
1143
1144	protected function applyArguments($argDef, $argValues) {
1145		$argValues = (array)$argValues;
1146
1147		$keywordArgs = array();
1148		$remaining = array();
1149
1150		// assign the keyword args
1151		foreach ($argValues as $arg) {
1152			if (!empty($arg[0])) {
1153				$keywordArgs[$arg[0][1]] = $arg[1];
1154			} else {
1155				$remaining[] = $arg[1];
1156			}
1157		}
1158
1159		foreach ($argDef as $i => $arg) {
1160			list($name, $default) = $arg;
1161
1162			if (isset($remaining[$i])) {
1163				$val = $remaining[$i];
1164			} elseif (isset($keywordArgs[$name])) {
1165				$val = $keywordArgs[$name];
1166			} elseif (!empty($default)) {
1167				$val = $default;
1168			} else {
1169				$val = self::$defaultValue;
1170			}
1171
1172			$this->set($name, $this->reduce($val, true), true);
1173		}
1174	}
1175
1176	protected function pushEnv($block=null) {
1177		$env = new stdclass;
1178		$env->parent = $this->env;
1179		$env->store = array();
1180		$env->block = $block;
1181		$env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
1182
1183		$this->env = $env;
1184		return $env;
1185	}
1186
1187	protected function normalizeName($name) {
1188		return str_replace("-", "_", $name);
1189	}
1190
1191	protected function getStoreEnv() {
1192		return isset($this->storeEnv) ? $this->storeEnv : $this->env;
1193	}
1194
1195	protected function set($name, $value, $shadow=false) {
1196		$name = $this->normalizeName($name);
1197		if ($shadow) {
1198			$this->setRaw($name, $value);
1199		} else {
1200			$this->setExisting($name, $value);
1201		}
1202	}
1203
1204	// todo: this is bugged?
1205	protected function setExisting($name, $value, $env = null) {
1206		if (is_null($env)) $env = $this->getStoreEnv();
1207
1208		if (isset($env->store[$name])) {
1209			$env->store[$name] = $value;
1210		} elseif (!is_null($env->parent)) {
1211			$this->setExisting($name, $value, $env->parent);
1212		} else {
1213			$this->env->store[$name] = $value;
1214		}
1215	}
1216
1217	protected function setRaw($name, $value) {
1218		$this->env->store[$name] = $value;
1219	}
1220
1221	protected function get($name, $defaultValue = null, $env = null) {
1222		$name = $this->normalizeName($name);
1223
1224		if (is_null($env)) $env = $this->getStoreEnv();
1225		if (is_null($defaultValue)) $defaultValue = self::$defaultValue;
1226
1227		if (isset($env->store[$name])) {
1228			return $env->store[$name];
1229		} elseif (!is_null($env->parent)) {
1230			return $this->get($name, $defaultValue, $env->parent);
1231		}
1232
1233		return $defaultValue; // found nothing
1234	}
1235
1236	protected function popEnv() {
1237		$env = $this->env;
1238		$this->env = $this->env->parent;
1239		return $env;
1240	}
1241
1242	public function getParsedFiles() {
1243		return $this->parsedFiles;
1244	}
1245
1246	public function addImportPath($path) {
1247		$this->importPaths[] = $path;
1248	}
1249
1250	public function setImportPaths($path) {
1251		$this->importPaths = (array)$path;
1252	}
1253
1254	public function setFormatter($formatterName) {
1255		$this->formatter = $formatterName;
1256	}
1257
1258	public function registerFunction($name, $func) {
1259		$this->userFunctions[$this->normalizeName($name)] = $func;
1260	}
1261
1262	public function unregisterFunction($name) {
1263		unset($this->userFunctions[$this->normalizeName($name)]);
1264	}
1265
1266	protected function importFile($path, $out) {
1267		// see if tree is cached
1268		$realPath = realpath($path);
1269		if (isset($this->importCache[$realPath])) {
1270			$tree = $this->importCache[$realPath];
1271		} else {
1272			$code = file_get_contents($path);
1273			$parser = new scss_parser($path);
1274			$tree = $parser->parse($code);
1275			$this->parsedFiles[] = $path;
1276
1277			$this->importCache[$realPath] = $tree;
1278		}
1279
1280		$pi = pathinfo($path);
1281		array_unshift($this->importPaths, $pi['dirname']);
1282		$this->compileChildren($tree->children, $out);
1283		array_shift($this->importPaths);
1284	}
1285
1286	// results the file path for an import url if it exists
1287	protected function findImport($url) {
1288		$urls = array();
1289
1290		// for "normal" scss imports (ignore vanilla css and external requests)
1291		if (!preg_match('/\.css|^http:\/\/$/', $url)) {
1292			// try both normal and the _partial filename
1293			$urls = array($url, preg_replace('/[^\/]+$/', '_\0', $url));
1294		}
1295
1296		foreach ($this->importPaths as $dir) {
1297			if (is_string($dir)) {
1298				// check urls for normal import paths
1299				foreach ($urls as $full) {
1300					$full = $dir .
1301						(!empty($dir) && substr($dir, -1) != '/' ? '/' : '') .
1302						$full;
1303
1304					if ($this->fileExists($file = $full.'.scss') ||
1305						$this->fileExists($file = $full))
1306					{
1307						return $file;
1308					}
1309				}
1310			} else {
1311				// check custom callback for import path
1312				$file = call_user_func($dir,$url,$this);
1313				if ($file !== null) {
1314					return $file;
1315				}
1316			}
1317		}
1318
1319		return null;
1320	}
1321
1322	protected function fileExists($name) {
1323		return is_file($name);
1324	}
1325
1326	protected function callBuiltin($name, $args, &$returnValue) {
1327		// try a lib function
1328		$name = $this->normalizeName($name);
1329		$libName = "lib_".$name;
1330		$f = array($this, $libName);
1331		$prototype = isset(self::$$libName) ? self::$$libName : null;
1332
1333		if (is_callable($f)) {
1334			$sorted = $this->sortArgs($prototype, $args);
1335			foreach ($sorted as &$val) {
1336				$val = $this->reduce($val, true);
1337			}
1338			$returnValue = call_user_func($f, $sorted, $this);
1339		} else if (isset($this->userFunctions[$name])) {
1340			// see if we can find a user function
1341			$fn = $this->userFunctions[$name];
1342
1343			foreach ($args as &$val) {
1344				$val = $this->reduce($val[1], true);
1345			}
1346
1347			$returnValue = call_user_func($fn, $args, $this);
1348		}
1349
1350		if (isset($returnValue)) {
1351			// coerce a php value into a scss one
1352			if (is_numeric($returnValue)) {
1353				$returnValue = array('number', $returnValue, "");
1354			} elseif (is_bool($returnValue)) {
1355				$returnValue = $returnValue ? self::$true : self::$false;
1356			} elseif (!is_array($returnValue)) {
1357				$returnValue = array('keyword', $returnValue);
1358			}
1359
1360			return true;
1361		}
1362
1363		return false;
1364	}
1365
1366	// sorts any keyword arguments
1367	// TODO: merge with apply arguments
1368	protected function sortArgs($prototype, $args) {
1369		$keyArgs = array();
1370		$posArgs = array();
1371
1372		foreach ($args as $arg) {
1373			list($key, $value) = $arg;
1374			$key = $key[1];
1375			if (empty($key)) {
1376				$posArgs[] = $value;
1377			} else {
1378				$keyArgs[$key] = $value;
1379			}
1380		}
1381
1382		if (is_null($prototype)) return $posArgs;
1383
1384		$finalArgs = array();
1385		foreach ($prototype as $i => $names) {
1386			if (isset($posArgs[$i])) {
1387				$finalArgs[] = $posArgs[$i];
1388				continue;
1389			}
1390
1391			$set = false;
1392			foreach ((array)$names as $name) {
1393				if (isset($keyArgs[$name])) {
1394					$finalArgs[] = $keyArgs[$name];
1395					$set = true;
1396					break;
1397				}
1398			}
1399
1400			if (!$set) {
1401				$finalArgs[] = null;
1402			}
1403		}
1404
1405		return $finalArgs;
1406	}
1407
1408	protected function coerceForExpression($value) {
1409		if ($color = $this->coerceColor($value)) {
1410			return $color;
1411		}
1412
1413		return $value;
1414	}
1415
1416	protected function coerceColor($value) {
1417		switch ($value[0]) {
1418		case "color": return $value;
1419		case "keyword":
1420			$name = $value[1];
1421			if (isset(self::$cssColors[$name])) {
1422				list($r, $g, $b) = explode(',', self::$cssColors[$name]);
1423				return array('color', $r, $g, $b);
1424			}
1425			return null;
1426		}
1427
1428		return null;
1429	}
1430
1431	protected function coerceString($value) {
1432		switch ($value[0]) {
1433		case "string":
1434			return $value;
1435		case "keyword":
1436			return array("string", "", array($value[1]));
1437		}
1438		return null;
1439	}
1440
1441	protected function assertColor($value) {
1442		if ($color = $this->coerceColor($value)) return $color;
1443		throw new exception("expecting color");
1444	}
1445
1446	protected function assertNumber($value) {
1447		if ($value[0] != "number")
1448			throw new exception("expecting number");
1449		return $value[1];
1450	}
1451
1452	protected function coercePercent($value) {
1453		if ($value[0] == "number") {
1454			if ($value[2] == "%") {
1455				return $value[1] / 100;
1456			}
1457			return $value[1];
1458		}
1459		return 0;
1460	}
1461
1462	// make sure a color's components don't go out of bounds
1463	protected function fixColor($c) {
1464		foreach (range(1, 3) as $i) {
1465			if ($c[$i] < 0) $c[$i] = 0;
1466			if ($c[$i] > 255) $c[$i] = 255;
1467		}
1468
1469		return $c;
1470	}
1471
1472	function toHSL($r, $g, $b) {
1473		$r = $r / 255;
1474		$g = $g / 255;
1475		$b = $b / 255;
1476
1477		$min = min($r, $g, $b);
1478		$max = max($r, $g, $b);
1479
1480		$L = ($min + $max) / 2;
1481		if ($min == $max) {
1482			$S = $H = 0;
1483		} else {
1484			if ($L < 0.5)
1485				$S = ($max - $min)/($max + $min);
1486			else
1487				$S = ($max - $min)/(2.0 - $max - $min);
1488
1489			if ($r == $max) $H = ($g - $b)/($max - $min);
1490			elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min);
1491			elseif ($b == $max) $H = 4.0 + ($r - $g)/($max - $min);
1492
1493		}
1494
1495		return array('hsl',
1496			($H < 0 ? $H + 6 : $H)*60,
1497			$S*100,
1498			$L*100,
1499		);
1500	}
1501
1502	function toRGB_helper($comp, $temp1, $temp2) {
1503		if ($comp < 0) $comp += 1.0;
1504		elseif ($comp > 1) $comp -= 1.0;
1505
1506		if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp;
1507		if (2 * $comp < 1) return $temp2;
1508		if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6;
1509
1510		return $temp1;
1511	}
1512
1513	// H from 0 to 360, S and L from 0 to 100
1514	function toRGB($H, $S, $L) {
1515		$H = $H % 360;
1516		if ($H < 0) $H += 360;
1517
1518		$S = min(100, max(0, $S));
1519		$L = min(100, max(0, $L));
1520
1521		$H = $H / 360;
1522		$S = $S / 100;
1523		$L = $L / 100;
1524
1525		if ($S == 0) {
1526			$r = $g = $b = $L;
1527		} else {
1528			$temp2 = $L < 0.5 ?
1529				$L*(1.0 + $S) :
1530				$L + $S - $L * $S;
1531
1532			$temp1 = 2.0 * $L - $temp2;
1533
1534			$r = $this->toRGB_helper($H + 1/3, $temp1, $temp2);
1535			$g = $this->toRGB_helper($H, $temp1, $temp2);
1536			$b = $this->toRGB_helper($H - 1/3, $temp1, $temp2);
1537		}
1538
1539		$out = array('color', $r*255, $g*255, $b*255);
1540		return $out;
1541	}
1542
1543	// Built in functions
1544
1545	protected static $lib_if = array("condition", "if-true", "if-false");
1546	protected function lib_if($args) {
1547		list($cond,$t, $f) = $args;
1548		if ($cond == self::$false) return $f;
1549		return $t;
1550	}
1551
1552	protected static $lib_rgb = array("red", "green", "blue");
1553	protected function lib_rgb($args) {
1554		list($r,$g,$b) = $args;
1555		return array("color", $r[1], $g[1], $b[1]);
1556	}
1557
1558	protected static $lib_rgba = array(
1559		array("red", "color"),
1560		"green", "blue", "alpha");
1561	protected function lib_rgba($args) {
1562		if ($color = $this->coerceColor($args[0])) {
1563			$num = is_null($args[1]) ? $args[3] : $args[1];
1564			$alpha = $this->assertNumber($num);
1565			$color[4] = $alpha;
1566			return $color;
1567		}
1568
1569		list($r,$g,$b, $a) = $args;
1570		return array("color", $r[1], $g[1], $b[1], $a[1]);
1571	}
1572
1573	// helper function for adjust_color, change_color, and scale_color
1574	protected function alter_color($args, $fn) {
1575		$color = $this->assertColor($args[0]);
1576
1577		foreach (array(1,2,3,7) as $i) {
1578			if (!is_null($args[$i])) {
1579				$val = $this->assertNumber($args[$i]);
1580				$ii = $i == 7 ? 4 : $i; // alpha
1581				$color[$ii] =
1582					$this->$fn(isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
1583			}
1584		}
1585
1586		if (!is_null($args[4]) || !is_null($args[5]) || !is_null($args[6])) {
1587			$hsl = $this->toHSL($color[1], $color[2], $color[3]);
1588			foreach (array(4,5,6) as $i) {
1589				if (!is_null($args[$i])) {
1590					$val = $this->assertNumber($args[$i]);
1591					$hsl[$i - 3] = $this->$fn($hsl[$i - 3], $val, $i);
1592				}
1593			}
1594
1595			$rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
1596			if (isset($color[4])) $rgb[4] = $color[4];
1597			$color = $rgb;
1598		}
1599
1600		return $color;
1601	}
1602
1603	protected static $lib_adjust_color = array(
1604		"color", "red", "green", "blue",
1605		"hue", "saturation", "lightness", "alpha"
1606	);
1607	protected function adjust_color_helper($base, $alter, $i) {
1608		return $base += $alter;
1609	}
1610	protected function lib_adjust_color($args) {
1611		return $this->alter_color($args, "adjust_color_helper");
1612	}
1613
1614	protected static $lib_change_color = array(
1615		"color", "red", "green", "blue",
1616		"hue", "saturation", "lightness", "alpha"
1617	);
1618	protected function change_color_helper($base, $alter, $i) {
1619		return $alter;
1620	}
1621	protected function lib_change_color($args) {
1622		return $this->alter_color($args, "change_color_helper");
1623	}
1624
1625	protected static $lib_scale_color = array(
1626		"color", "red", "green", "blue",
1627		"hue", "saturation", "lightness", "alpha"
1628	);
1629	protected function scale_color_helper($base, $scale, $i) {
1630		// 1,2,3 - rgb
1631		// 4, 5, 6 - hsl
1632		// 7 - a
1633		switch ($i) {
1634		case 1:
1635		case 2:
1636		case 3:
1637			$max = 255; break;
1638		case 4:
1639			$max = 360; break;
1640		case 7:
1641			$max = 1; break;
1642		default:
1643			$max = 100;
1644		}
1645
1646		$scale = $scale / 100;
1647		if ($scale < 0) {
1648			return $base * $scale + $base;
1649		} else {
1650			return ($max - $base) * $scale + $base;
1651		}
1652	}
1653	protected function lib_scale_color($args) {
1654		return $this->alter_color($args, "scale_color_helper");
1655	}
1656
1657	protected static $lib_ie_hex_str = array("color");
1658	protected function lib_ie_hex_str($args) {
1659		$color = $this->coerceColor($args[0]);
1660		$color[4] = isset($color[4]) ? round(255*$color[4]) : 255;
1661
1662		return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
1663	}
1664
1665	protected static $lib_red = array("color");
1666	protected function lib_red($args) {
1667		list($color) = $args;
1668		return $color[1];
1669	}
1670
1671	protected static $lib_green = array("color");
1672	protected function lib_green($args) {
1673		list($color) = $args;
1674		return $color[2];
1675	}
1676
1677	protected static $lib_blue = array("color");
1678	protected function lib_blue($args) {
1679		list($color) = $args;
1680		return $color[3];
1681	}
1682
1683	protected static $lib_alpha = array("color");
1684	protected function lib_alpha($args) {
1685		if ($color = $this->coerceColor($args[0])) {
1686			return isset($color[4]) ? $color[4] : 1;
1687		}
1688
1689		// this might be the IE function, so return value unchanged
1690		return array("function", "alpha", array("list", ",", $args));
1691	}
1692
1693	protected static $lib_opacity = array("color");
1694	protected function lib_opacity($args) {
1695		return $this->lib_alpha($args);
1696	}
1697
1698	// mix two colors
1699	protected static $lib_mix = array("color-1", "color-2", "weight");
1700	protected function lib_mix($args) {
1701		list($first, $second, $weight) = $args;
1702		$first = $this->assertColor($first);
1703		$second = $this->assertColor($second);
1704
1705		if (is_null($weight)) {
1706			$weight = 0.5;
1707		} else {
1708			$weight = $this->coercePercent($weight);
1709		}
1710
1711		$first_a = isset($first[4]) ? $first[4] : 1;
1712		$second_a = isset($second[4]) ? $second[4] : 1;
1713
1714		$w = $weight * 2 - 1;
1715		$a = $first_a - $second_a;
1716
1717		$w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
1718		$w2 = 1.0 - $w1;
1719
1720		$new = array('color',
1721			$w1 * $first[1] + $w2 * $second[1],
1722			$w1 * $first[2] + $w2 * $second[2],
1723			$w1 * $first[3] + $w2 * $second[3],
1724		);
1725
1726		if ($first_a != 1.0 || $second_a != 1.0) {
1727			$new[] = $first_a * $weight + $second_a * ($weight - 1);
1728		}
1729
1730		return $this->fixColor($new);
1731	}
1732
1733	protected static $lib_hsl = array("hue", "saturation", "lightness");
1734	protected function lib_hsl($args) {
1735		list($h, $s, $l) = $args;
1736		return $this->toRGB($h[1], $s[1], $l[1]);
1737	}
1738
1739	protected static $lib_hsla = array("hue", "saturation",
1740		"lightness", "alpha");
1741	protected function lib_hsla($args) {
1742		list($h, $s, $l, $a) = $args;
1743		$color = $this->toRGB($h[1], $s[1], $l[1]);
1744		$color[4] = $a[1];
1745		return $color;
1746	}
1747
1748	protected static $lib_hue = array("color");
1749	protected function lib_hue($args) {
1750		$color = $this->assertColor($args[0]);
1751		$hsl = $this->toHSL($color[1], $color[2], $color[3]);
1752		return array("number", $hsl[1], "deg");
1753	}
1754
1755	protected static $lib_saturation = array("color");
1756	protected function lib_saturation($args) {
1757		$color = $this->assertColor($args[0]);
1758		$hsl = $this->toHSL($color[1], $color[2], $color[3]);
1759		return array("number", $hsl[2], "%");
1760	}
1761
1762	protected static $lib_lightness = array("color");
1763	protected function lib_lightness($args) {
1764		$color = $this->assertColor($args[0]);
1765		$hsl = $this->toHSL($color[1], $color[2], $color[3]);
1766		return array("number", $hsl[3], "%");
1767	}
1768
1769
1770	protected function adjustHsl($color, $idx, $amount) {
1771		$hsl = $this->toHSL($color[1], $color[2], $color[3]);
1772		$hsl[$idx] += $amount;
1773		$out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
1774		if (isset($color[4])) $out[4] = $color[4];
1775		return $out;
1776	}
1777
1778	protected static $lib_adjust_hue = array("color", "degrees");
1779	protected function lib_adjust_hue($args) {
1780		$color = $this->assertColor($args[0]);
1781		$degrees = $this->assertNumber($args[1]);
1782		return $this->adjustHsl($color, 1, $degrees);
1783	}
1784
1785	protected static $lib_lighten = array("color", "amount");
1786	protected function lib_lighten($args) {
1787		$color = $this->assertColor($args[0]);
1788		$amount = 100*$this->coercePercent($args[1]);
1789		return $this->adjustHsl($color, 3, $amount);
1790	}
1791
1792	protected static $lib_darken = array("color", "amount");
1793	protected function lib_darken($args) {
1794		$color = $this->assertColor($args[0]);
1795		$amount = 100*$this->coercePercent($args[1]);
1796		return $this->adjustHsl($color, 3, -$amount);
1797	}
1798
1799	protected static $lib_saturate = array("color", "amount");
1800	protected function lib_saturate($args) {
1801		$color = $this->assertColor($args[0]);
1802		$amount = 100*$this->coercePercent($args[1]);
1803		return $this->adjustHsl($color, 2, $amount);
1804	}
1805
1806	protected static $lib_desaturate = array("color", "amount");
1807	protected function lib_desaturate($args) {
1808		$color = $this->assertColor($args[0]);
1809		$amount = 100*$this->coercePercent($args[1]);
1810		return $this->adjustHsl($color, 2, -$amount);
1811	}
1812
1813	protected static $lib_grayscale = array("color");
1814	protected function lib_grayscale($args) {
1815		return $this->adjustHsl($this->assertColor($args[0]), 2, -100);
1816	}
1817
1818	protected static $lib_complement = array("color");
1819	protected function lib_complement($args) {
1820		return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
1821	}
1822
1823	protected static $lib_invert = array("color");
1824	protected function lib_invert($args) {
1825		$color = $this->assertColor($args[0]);
1826		$color[1] = 255 - $color[1];
1827		$color[2] = 255 - $color[2];
1828		$color[3] = 255 - $color[3];
1829		return $color;
1830	}
1831
1832
1833	// increases opacity by amount
1834	protected static $lib_opacify = array("color", "amount");
1835	protected function lib_opacify($args) {
1836		$color = $this->assertColor($args[0]);
1837		$amount = $this->coercePercent($args[1]);
1838
1839		$color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
1840		$color[4] = min(1, max(0, $color[4]));
1841		return $color;
1842	}
1843
1844	protected static $lib_fade_in = array("color", "amount");
1845	protected function lib_fade_in($args) {
1846		return $this->lib_opacify($args);
1847	}
1848
1849	// decreases opacity by amount
1850	protected static $lib_transparentize = array("color", "amount");
1851	protected function lib_transparentize($args) {
1852		$color = $this->assertColor($args[0]);
1853		$amount = $this->coercePercent($args[1]);
1854
1855		$color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
1856		$color[4] = min(1, max(0, $color[4]));
1857		return $color;
1858	}
1859
1860	protected static $lib_fade_out = array("color", "amount");
1861	protected function lib_fade_out($args) {
1862		return $this->lib_transparentize($args);
1863	}
1864
1865	protected static $lib_unquote = array("string");
1866	protected function lib_unquote($args) {
1867		$str = $args[0];
1868		if ($str[0] == "string") $str[1] = "";
1869		return $str;
1870	}
1871
1872	protected static $lib_quote = array("string");
1873	protected function lib_quote($args) {
1874		$value = $args[0];
1875		if ($value[0] == "string" && !empty($value[1]))
1876			return $value;
1877		return array("string", '"', array($value));
1878	}
1879
1880	protected static $lib_percentage = array("value");
1881	protected function lib_percentage($args) {
1882		return array("number",
1883			$this->coercePercent($args[0]) * 100,
1884			"%");
1885	}
1886
1887	protected static $lib_round = array("value");
1888	protected function lib_round($args) {
1889		$num = $args[0];
1890		$num[1] = round($num[1]);
1891		return $num;
1892	}
1893
1894	protected static $lib_floor = array("value");
1895	protected function lib_floor($args) {
1896		$num = $args[0];
1897		$num[1] = floor($num[1]);
1898		return $num;
1899	}
1900
1901	protected static $lib_ceil = array("value");
1902	protected function lib_ceil($args) {
1903		$num = $args[0];
1904		$num[1] = ceil($num[1]);
1905		return $num;
1906	}
1907
1908	protected static $lib_abs = array("value");
1909	protected function lib_abs($args) {
1910		$num = $args[0];
1911		$num[1] = abs($num[1]);
1912		return $num;
1913	}
1914
1915	protected function lib_min($args) {
1916		$numbers = $this->getNormalizedNumbers($args);
1917		$min = null;
1918		foreach ($numbers as $key => $number) {
1919			if (null === $min || $number <= $min[1]) {
1920				$min = array($key, $number);
1921			}
1922		}
1923
1924		return $args[$min[0]];
1925	}
1926
1927	protected function lib_max($args) {
1928		$numbers = $this->getNormalizedNumbers($args);
1929		$max = null;
1930		foreach ($numbers as $key => $number) {
1931	

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