Location: PHPKode > scripts > XCS Parser > xcs-parser/lib/XcsParser_class.php
<?php
/**
 * XcsParser is a XCS/CSS parsing and compression class for PHP5.
 * XCS stands for "eXtended Cascading Stylesheet".
 * "Extended", because XCS introduces several new concepts
 * and shorthands to CSS standard:
 *     1) Global variables (e.g. '!foregroundColor = #333;');
 *     2) Simple arythmetic operations on variables, and/or
 *        integer constants ("simple" means only 2 operands allowed).
 *        (e.g. '!width = !columnWidth * !numberOfColumns;')
 *        (e.g. '!width = !columnWidth * 4;')
 *     3) Complex expression handling, if allowed
 *        (e.g. '!date = expr(date("Y:m:d"));')
 *     4) Inline file inclusion, if allowed. This is different
 *        from normal CSS file inclusion, as it is performed
 *        in place (i.e. server-side), instead of client-side.
 *        (e.g. '@require(variable_defines.css);')
 *     5) Creating new CSS definitions by extending the
 *        existing rules
 *        (e.g. '.smalltext{font-size:0.8em} .smallredtext extends(.smalltext){color:red}')
 *     6) Line comments syntax with '// ... '
 *        (e.g. 'width: 120px; // this is a comment')
 * Of course, XCS syntax is optional, and the class can be used
 * for basic CSS prettyfying/compression alone. Depending on
 * compression flag set, the class will eiher:
 *     1) Leave your CSS as-is.
 *     2) Remove duplicated properties, alphabetize them
 *        and, optionally, alphabetize rules as well.
 *     3) Like 2), but will also strip any duplicated
 *        newlines.
 *     4) Like 3), but will also strip any CSS comments and
 *        zap all whitespaces to a single ' '.
 *
 * The class requires PHP5, and will not work with PHP4.
 *
 * All overridable `private` members are declared `protected`, so
 * one could easily extend the class to meet her particular needs
 * (e.g. to pre-populate options and flags in order to avoid
 * in-script option setting). By changing internal regexen, one
 * could also tweak XCS syntax to her liking. For more on this,
 * see included examples.
 *
 * Included example scripts:
 * @example ../examples/basic.php Basic usage example.
 * @example ../examples/static.php Static loader usage example.
 * @example ../examples/inherit.php Class inheritance usage example.
 * @example ../examples/noexpr.php Forbidden `expr()` syntax example.
 * @example ../examples/norequire.php Forbidden `@require()` syntax example.
 * @example ../examples/compression.php Various compression/prettyfying examples.
 * @example ../examples/syntax.php XCS syntax changing advanced examples.
 *
 * @author Ve Bailovity, <hide@address.com>
 * @license http://bsd? BSD-style
 * @version 1.0
 */
class XcsParser {

	/* ================= */
	/* === Constants === */
	/* ================= */

	const COMPRESS_NONE = 0;
	const COMPRESS_PRETTY_READABLE = 1;
	const COMPRESS_HUMAN_READABLE = 2;
	const COMPRESS_ALL = 3;

	/* ========================= */
	/* === Options and flags === */
	/* ========================= */

	/**
	 * Compression level is one of the
	 * XcsParser::COMPRESS_* constants above.
	 */
	protected $compressionLevel = XcsParser::COMPRESS_HUMAN_READABLE;

	/**
	 * Allow stripping CSS comments outside CSS rules?
	 * It affects alphabetizig the CSS rules with compression
	 * level set to XcsParser::COMPRESS_PRETTY_READABLE
	 * and above.
	 */
	protected $commentsAreImportant = false;

	/**
	 * Allow and process XCS `expr(...)` statements?
	 * @Warning: implies liberal `eval`ing if set to (bool)True.
	 */
	protected $allowExpressions = true;

	/**
	 * Allow and process XCS `@require()` statements?
	 * @Warning: will include external files if set to (bool)True.
	 */
	protected $allowExternalFiles = true;

	/* ======================== */
	/* === Internal regexen === */
	/* ======================== */

	/**
	 * XCS variable name prefix.
	 */
	protected $varNamePrefix = '!';

	/**
	 * XCS variable name regex.
	 * Change this as well, if you change $varNamePrefix.
	 */
	protected $varNameRx = '![A-Za-z0-9_-]+';

	/**
	 * XCS variable value regex.
	 */
	protected $varValueRx = '[!#@:()~.%$0-9A-Za-z ]+';

	/**
	 * XCS line comment start regex.
	 */
	protected $lineCommentStartRx = '\/\/';

	/**
	 * XCS `@require()` syntax regex.
	 */
	protected $requireRx = '\@require\s*\((.*?)\);';

	/**
	 * XCS `expr()` syntax regex.
	 */
	protected $expressionRx = 'expr\s*\(([^;]+)\)';

	/**
	 * XCS `extends(ruledef)` syntax regex.
	 */
	protected $extendsRx = 'extends\s*\((.*?)\)\s*\{';

	/* ======================== */
	/* === Internal storage === */
	/* ======================== */

	/**
	 * Internal XCS/CSS storage. String.
	 */
	private $css = null;

	/**
	 * Internal storage for defined XCS global variables.
	 */
	private $cssVars = array();

	/**
	 * Internal array of files already included with
	 * XCS `@require()` statement.
	 */
	private $includedFiles = array();

	/* ============================ */
	/* ====== Public methods ====== */
	/* ============================ */

	/* ================ */
	/* === Creators === */
	/* ================ */

	/**
	 * Static loader.
	 * Loads the contents of a supplied filename and
	 * creates a parser.
	 * @throws Exception on no file found on specified path.
	 * @param string $filename A XCS/CSS file path.
	 * @param int $compressionLevel Optional compression level.
	 * @return XcsParser object
	 */
	public static function load ($filename) {
		if (!file_exists($filename)) throw new Exception("No such file: -{$filename}-");
		return new XcsParser(
			file_get_contents($filename)
		);
	}

	/**
	 * Constructor.
	 * @param string $css A XCS/CSS string to parse/compress.
	 */
	public function __construct ($css = false) {
		$this->setCss($css);
	}

	/* =============== */
	/* === Setters === */
	/* =============== */

	/**
	 * Sets the XCS/CSS string.
	 * @param string $css
	 * @return object $this
	 */
	final public function setCss ($css) {
		$this->css = $css;
		return $this;
	}

	/**
	 * Sets a global XCS variable.
	 * @param string $name Variable name
	 * @param mixed $value Variable value
	 * @return object $this
	 */
	final public function setVar ($name, $value) {
		if ($this->varNamePrefix == substr($name, 0, strlen($this->varNamePrefix))) $name = substr($name, strlen($this->varNamePrefix));
		$this->cssVars[$name] = $value;
		return $this;
	}

	/**
	 * Sets XCS parser compression level.
	 * @param int $cl Compression level flag (a class constant)
	 * @return object $this
	 */
	final public function setCompressionLevel ($cl) {
		if (is_int($cl)) $this->compressionLevel = $cl;
		return $this;
	}

	/**
	 * Sets the flag for stripping comments outside rules.
	 * @param bool $c Are comments important?
	 * @return object $this
	 */
	final public function setCommentsAreImportant ($c) {
		if ($c) $this->commentsAreImportant = true;
		else $this->commentsAreImportant = false;
		return $this;
	}

	/**
	 * Sets internal flag for (dis)allowing the XCS
	 * `expr(...)` statements.
	 * @param bool $a Allow?
	 * @return object $this
	 */
	final public function setAllowExpressions ($a) {
		if ($a) $this->allowExpressions = true;
		else $this->allowExpressions = false;
		return $this;
	}

	/**
	 * Sets internal flag for (dis)allowing the XCS
	 * `@require()` statements.
	 * @param bool $a Allow?
	 * @return object $this
	 */
	final public function setAllowExternalFiles ($a) {
		if ($a) $this->allowExternalFiles = true;
		else $this->allowExternalFiles = false;
		return $this;
	}

	/* =============== */
	/* === Getters === */
	/* =============== */

	/**
	 * Gets a XCS global variable value.
	 * @param string $name Variable name
	 * @return mixed Variable value
	 */
	final public function getVar ($name) {
		if ($this->varNamePrefix == substr($name, 0, strlen($this->varNamePrefix))) $name = substr($name, strlen($this->varNamePrefix));
		return @$this->cssVars[$name];
	}

	/**
	 * Gets all defined XCS global variables.
	 * @return array
	 */
	final public function getAllVars () {
		return $this->cssVars;
	}

	/**
	 * Gets a parsed CSS string.
	 * @return string CSS string
	 */
	final public function getCss () {
		return $this->css;
	}

	/* ================== */
	/* === Processing === */
	/* ================== */

	/**
	 * Parses internal XCS string into CSS.
	 * @return object $this
	 */
	final public function parse () {
		if (!$this->css) return false;
		if ($this->allowExternalFiles) $this->includeFiles();
		$this->parseVarDefinitions();
		while (1) {
			if (!$this->replaceVarDefOperations()) break;
		}
		if ($this->allowExpressions) $this->doExpressions();
		/* Parse definitions again, after operator */
		/* and expressions substitution */
		$this->parseVarDefinitions();
		while (1) {
			if (!$this->replaceVarDefinitions()) break;
		}
		$this->doExtendedRules();
		$this->doLineComments();
		return $this;
	}

	/**
	 * Compresses/prettyfies parsed CSS.
	 * @return object $this
	 */
	final public function compress () {
		if (!$this->css) return false;
		if ($this->compressionLevel == self::COMPRESS_NONE) return $this;
		if ($this->compressionLevel >= self::COMPRESS_PRETTY_READABLE) {
			if (!$this->commentsAreImportant) $this->alphabetizeSelectors();
			$this->alphabetizeProperties();
		}
		if ($this->compressionLevel >= self::COMPRESS_HUMAN_READABLE) {
			$this->css = preg_replace('/(\r?\n)+/', "\n", $this->css);
		}
		if ($this->compressionLevel >= self::COMPRESS_ALL) {
			$this->css = preg_replace('/\/\*(.*?)\*\//s', '', $this->css); // remove comments
			$this->css = preg_replace('/\s\s*/', ' ', trim($this->css)); // collapse whitespace
			$this->css = preg_replace('/([A-Za-z])\s?\{\s?/', '\1{', $this->css); //collapse space around brackets
			$this->css = preg_replace('/([;:])\s/', '\1', $this->css); // collapse spaces around punctuation
		}
		return $this;
	}

	/* =========================== */
	/* ===== Private methods ===== */
	/* =========================== */

	/**
	 * Handles `@require` XCS statements.
	 * @throws Exception on no file found
	 * @return bool
	 */
	final private function includeFiles () {
		$result = $lines = $files = array();
		preg_match_all('/' . $this->requireRx . '/sm', $this->css, $result);
		$lines = $result[0];
		$files = $result[1];
		foreach ($files as $key=>$file) {
			if (in_array($file, $this->includedFiles)) continue;
			$file = trim($file);
			if (!file_exists($file)) throw new Exception("No such file: -{$filename}-");
			$css = file_get_contents($file);
			$lines[$key] = $this->escapeRx($lines[$key], 1);
			$this->css = preg_replace('/' . $lines[$key] . '/', $css, $this->css);
			$this->includedFiles[] = $file;
		}
		return true;
	}

	/**
	 * Finds and extracts simple XCS variable definitions.
	 * Extracted definitions are stored internally and can
	 * be accessed with xcsParser::getVar(name).
	 * @return bool
	 */
	final private function parseVarDefinitions () {
		$vars = array();
		$rx = '/(' . $this->varNameRx . ')\s*=\s*(' . $this->varValueRx . ')\;/';
		preg_match_all($rx, $this->css, $vars);
		array_shift($vars);
		foreach ($vars[0] as $key=>$name) {
			if ($this->varNamePrefix == substr($name, 0, strlen($this->varNamePrefix))) $name = substr($name, strlen($this->varNamePrefix));
			if (!isset($this->cssVars[$name])) $this->cssVars[$name] = $vars[1][$key];
		}
		/* Destroy var defs */
		$this->css = preg_replace($rx, '', $this->css);
		return true;
	}

	/**
	 * Finds, solves and extracts simple XCS expressions.
	 * Simple XCS expressions involve only 2 operands:
	 * 	1) either 2 XCS variables, or
	 * 	2) one XCS variable, and one integer
	 * return bool True on change
	 */
	final private function replaceVarDefOperations () {
		$varOps = array();
		$oldCss = $this->css;
		$rx = '/(' . $this->varNameRx . '|[0-9]+)\s*([-+*\/])\s*(' . $this->varNameRx . '|[0-9]+)/';
		preg_match_all($rx, $this->css, $varOps);
		foreach ($varOps[0] as $key=>$currentOp) {
			$name1 = $varOps[1][$key];
			$value1 = ($this->getVar($name1)) ? $this->getVar($name1) : $name1;
			$op = $varOps[2][$key];
			$name2 = $varOps[3][$key];
			$value2 = ($this->getVar($name2)) ? $this->getVar($name2) : $name2;
			$result = $this->executeVarOperation ($value1, $op, $value2);
			$currentOp = $this->escapeRx($currentOp);
			$this->css = preg_replace('/' . $currentOp . '/', $result, $this->css);
		}
		/* Did we substitute anything in this pass? */
		if ($oldCss == $this->css) return false;
		return true;
	}

	/**
	 * Iterates through XC Stylesheet and swaps variable
	 * names with their values from internal storage.
	 * Unknown values are left untouched.
	 * @return bool True on change
	 */
	final private function replaceVarDefinitions () {
		$oldCss = $this->css;
		foreach ($this->cssVars as $name=>$value) {
			$this->css = preg_replace('/' . $this->escapeRx($this->varNamePrefix . $name, 1) . '/', $value, $this->css);
		}
		/* Did we substitute anything in this pass? */
		if ($oldCss == $this->css) return false;
		return true;
	}

	/**
	 * Executes simple XCS expressions.
	 * Tries to respect the units at hand.
	 * @return mixed $result
	 */
	final private function executeVarOperation ($v1, $op, $v2) {
		$result = '';
		if (preg_match('/#[0-9ABCDEFabcdef]+/', $v1)) {
			/* We have color codes */
			$val1 = intval(substr($v1, 1), 16);
			$val2 = intval(substr($v2, 1), 16);
			$result = '#' . dechex(eval('return ' . $val1 . $op . $val2 . ';'));
		} else if (preg_match('/[0-9.]+(em|ex|px|pt)/', $v1)) {
			/* We have measurements */
			$val1 = floatval($v1);
			$val2 = floatval($v2);
			$unit = trim(preg_replace('/[0-9.]+/', '', $v1));
			$result = eval('return ' . $val1 . $op . $val2 . ';') . $unit;
		} else if (preg_match('/#[0-9ABCDEFabcdef]+/', $v2)) {
			/* We have a constant and a color code */
			$val1 = intval($v1);
			$val2 = intval(substr($v2, 1), 16);
			$result = '#' . dechex(eval('return ' . $val1 . $op . $val2 . ';'));
		} else if (preg_match('/[0-9.]+(em|ex|px|pt)/', $v2)) {
			/* We have a constant and a measurement */
			$val1 = floatval($v1);
			$val2 = floatval($v2);
			$unit = trim(preg_replace('/[0-9.]+/', '', $v2));
			$result = eval('return ' . $val1 . $op . $val2 . ';') . $unit;
		} else {
			$result = @eval('return ' . $v1 . $op . $v2 .';');
		}
		return $result;
	}

	/**
	 * Executes complex XCS expressions.
	 * Since this involves relatively liberal use ov `eval`,
	 * this step can be forbidden by setting the
	 * XcsParser::allowExpressions property to
	 * (bool)False.
	 * @return bool
	 */
	final private function doExpressions () {
		$result = $exprs = $evals = array();
		preg_match_all ('/' . $this->expressionRx . '/sm', $this->css, $result);
		$exprs = $result[0];
		$evals = $result[1];
		foreach ($evals as $key=>$eval) {
			$res = eval('return ' . $eval . ';');
			$exprs[$key] = $this->escapeRx($exprs[$key], 1);
			$this->css = preg_replace(
				'/' . $exprs[$key] . '/',
				$res, $this->css
			);
		}
		return true;
	}

	/**
	 * Process XCS `extends(ruledef)` statements.
	 * @return bool
	 */
	final private function doExtendedRules () {
		$matches = $lines = $extends = array();
		preg_match_all('/' . $this->extendsRx . '/sm', $this->css, $matches);
		$lines = $matches[0];
		$extends = $matches[1];
		foreach ($extends as $key=>$extend) {
			$extend = trim($extend);
			$rule = array();
			if (preg_match('/^' . $extend . '\s*\{(.*?)\}/sm', $this->css, $rule)) {
				$lines[$key] = $this->escapeRx($lines[$key], 1);
				$this->css = preg_replace('/' . $lines[$key] . '/', "{\n\t".trim($rule[1]), $this->css);
			}
		}
		return true;
	}

	/**
	 * Converts XCS line comments (// ...) into
	 * CSS-acceptable form.
	 * @return bool
	 */
	final private function doLineComments () {
		$this->css = preg_replace(
			'/' . $this->lineCommentStartRx . '(.*?)\r?\n/',
			"/* \\1 */\n",
			$this->css
		);
		return true;
	}

	/**
	 * Alphabetizes CSS properties.
	 * Also strips duplicated properties, keeping only
	 * the LAST one (it's called CASCADING after all, no?).
	 * @return bool
	 */
	final private function alphabetizeProperties () {
		$matches = $lines = $propBlocks = array();
		preg_match_all('/\{(.*?)\}/sm', $this->css, $matches);
		$lines = $matches[0];
		$propBlocks = $matches[1];
		foreach ($propBlocks as $key=>$propBlock) {
			$props = explode("\n", trim($propBlock));
			$props = array_map('trim', $props);
			/* Destroy doubled properties */
			$propdefs = array();
			for ($i = count($props); $i>=0; $i--) {
				$def = $val = false;
				@list($def,$val) = @explode(':', $props[$i]);
				if (!$def || !$val) continue;
				if (in_array($def, $propdefs)) unset($props[$i]);
				else $propdefs[] = $def;
			}
			/* Back to alphabetizing properties */
			sort($props);
			$props = join("\n\t", $props);
			$lines[$key] = $this->escapeRx($lines[$key]);
			$this->css = preg_replace('/' . $lines[$key] . '/', "{\n\t$props\n}\n", $this->css);
		}
		return true;
	}

	/**
	 * Tries to alphabetize CSS rules.
	 * @Warning: kills comments outside rules!
	 * @return bool
	 */
	final private function alphabetizeSelectors () {
		$rules = array();
		$matches = preg_split('/^(.*\s?\{)\s*$/m', $this->css, -1, PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);
		for ($i=0; $i<count($matches); $i++) {
			if (preg_match('/\{/', $matches[$i])) {
				$rules[$i] = $matches[$i] . $matches[$i+1];
				$i++;
			} else {
				$rest[$i] = $matches[$i];
			}
		}
		sort($rules);
		$this->css = join("\n", $rules);
		return true;
	}

	/**
	 * Regex escaping helper method.
	 * @param string $str String to escape
	 * @param bool $quote To call preg_quote as well?
	 * @return string Escaped string
	 */
	final private static function escapeRx ($str, $quote=false) {
		if ($quote) $str = preg_quote($str);
		$str = preg_replace('/\//', '\/', $str);
		$str = preg_replace('/\*/', '\*', $str);
		if (!$quote) $str = preg_replace('/\$/', '\\\$', $str);
		return $str;
	}
}
?>
Return current item: XCS Parser