- <?php
- /*
- * Lokorin.com
- * Copyright 2004-2006
- * Licensed under the GNU LGPL. See COPYING for full terms.
- */
- /**
- * Describes a library responsible for processing and caching CSS files. The
- * library allows variables to be used within the CSS files.
- * @author Andreas Launila
- * @version $Revision: 1.2 $
- * @package com.lokorin.lokorin.lib
- * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
- */
-
- /**
- * For access to common constants and functions.
- */
- require_once('common.php');
-
- /**
- * Unlinks all cached CSS files.
- * @return integer The number of files that were unlinked.
- * @throws IOException If the css cache folder couldn't be opened.
- */
- function clearCssCache() {
- //Unlink all files in the cache directory with a .css extension.
- if(!($dir = @opendir(DOC_ROOT.FOLDER_CSS_CACHE))) {
- throw new IOException("The CSS cache folder could not be opened.");
- }
-
- $counter = 0;
- while(($file = readdir($dir)) !== false) {
- if(($file != '..') && ($file != '.') && (substr($file, -4) == ".css")) {
- unlink(DOC_ROOT.FOLDER_CACHE.$file);
- $counter++;
- }
- }
- closedir($dir);
- return $counter;
- }
-
- /**
- * A class that handles everything that has to do with CSS, all CSS files that
- * are to be included are recorded by this class. The appropriate actions are
- * then taken to make sure that each of the files are then handled correctly
- * when the page should be displayed. Implements the singleton pattern.
- * @author Andreas Launila
- * @package com.lokorin.lokorin.lib
- */
- class CssHandler implements HeadAddable {
- /**
- * The one and only instance of the class.
- * @var CssHandler
- */
- static private $instance;
- /**
- * The names of all CSS files that should be included when displaying the
- * page.
- */
- private $cssNames;
- /**
- * The name of the XML resource file that contains all CSS values.
- */
- const CSS_VALUE_FILE_NAME = 'cssValues.xml';
- /**
- * The CSS values as a tree structure. The array may contain arrays, in
- * which case the key is the name of the node. The array may also contain
- * textual values, which are leaves.
- * @var array
- */
- private $cssValues;
-
- /**
- * Gets the one and only instance of the class.
- * @return CssTemplateConverter The one and only instance.
- */
- static public function getInstance() {
- if(!isset(CssHandler::$instance)) {
- CssHandler::$instance = new CssHandler();
- }
- return CssHandler::$instance;
- }
-
- /**
- * Constructor for CssConverter. Adds itself as additional head information
- * to the page.
- */
- private function __construct() {
- $this->cssNames = array();
- Page::getInstance()->addToHead($this);
- }
-
- /**
- * Gets the XHTML link that represents the specified CSS name.
- * @param $cssName The name of the CSS file to which the link should be
- * created.
- * @access public
- */
- private function getCssLink($cssName) {
- $fileName = $this->stripCssExtension($cssName).'.css';
- $cssFile = FOLDER_CSS_CACHE.$fileName;
- if(!file_exists(DOC_ROOT.$cssFile)) {
- //A cached version of the file does not exist, create it.
- $text = file_get_contents(DOC_ROOT.FOLDER_CSS.$fileName);
- if($text === false) {
- //Could not read the file.
- throw new IOException('The css file ('.$cssFile.') could not ' .
- 'be read.');
- }
- file_put_contents(DOC_ROOT.$cssFile, $this->convertTemplate($text));
- }
- return '<link href="'.PAGE_ROOT.$cssFile.'" type="text/css" rel="stylesheet"/>';
- }
-
- /**
- * Strips any .css extensions that occur last in a specified text.
- * @param string $text The text from which .css extensions should be stripped.
- * @return string Text without a .css extension.
- */
- private function stripCssExtension($text) {
- //The method is crude and removes all occurences of '.css', alter it if it
- //causes problems.
- return str_replace('.css', '', $text);
- }
-
- /**
- * Converts a string containing a CSS template to a string representing
- * a real CSS file.
- * @param string $template A string with CSS but with added tokens that
- * have special meaning, making it a CSS template.
- * @return string The corresponding CSS (without any special tokens).
- * @throws IllegalArgumentException If the template contains unexpected
- * variables.
- */
- private function convertTemplate($template) {
- if(!isset($this->cssValues)) {
- //Make sure that the css values are loaded.
- $this->cssValues = $this->loadCssValueTree();
- }
-
- //Replace CSS value tokens with their real values.
- foreach($this->cssValues as $key => $values) {
- $template = $this->replaceCssValues($template, $values, array($key));
- }
-
- //Check if there are still any unreplaced values left.
- $matches = array();
- preg_match("/\\$[^\s;]+/", $template, $matches);
- if(count($matches) > 0) {
- throw new IllegalArgumentException("Unexpected variable (" .
- $matches[0] . ") in the template.");
- }
-
- return $template;
- }
-
- /**
- * Loads all CSS values into a tree structure. The resulting tree structure
- * reflects the XML resource file, but does not contain the root node.
- * @return array An array containing a tree structure. The array may contain
- * arrays, in which case the key is the name of the node. The array may
- * also contain textual values, which are leaves.
- * @throws IOException If the CSS value files can't be accessed.
- */
- private function loadCssValueTree() {
- //Read the contents of the file.
- $text = file_get_contents(
- DOC_ROOT.FOLDER_RESOURCES.CssHandler::CSS_VALUE_FILE_NAME);
- if($text === false) {
- throw new IOException("The CSS value file could not be read.");
- }
-
- //Parse it.
- $xml = simplexml_load_string($text);
- //Parse the root node.
- return $this->parseCssValueNode($xml);
- }
-
- /**
- * Recursivly parses a css value node and turns it into an array structure.
- * @param SimpleXmlElement $node The node that should be parsed.
- * @return array An array tree structure representing the node. Each array
- * in the structure is a subnode and each value that is not an array
- * represents a leaf.
- */
- private function parseCssValueNode(SimpleXmlElement $node) {
- $values = array();
- foreach($node as $name => $child) {
- //Parse all nodes.
- $values[$name] = $this->parseCssValueNode($child);
- }
-
- if(count($values) == 0) {
- //No nodes found, it's a leaf.
- return (string)$node;
- }
- return $values;
- }
-
- /**
- * Replaces all css values defined by the specified array in the specified
- * text. The values are represented in the css text as
- * $namespace:subcat:value, the subcat may or may not be used, any number
- * of subctegories can be used after the namespace but before the value.
- * @param string $text The text in which values should be replaced.
- * @param array $values The values that shpould be replaced, the array may
- * contain subarrays, which are also processed.
- * @param array $steps The steps that have been taken so far. The elements
- * should be sorted in the order that they have been traversed. I.e. the
- * namespace should be first, then the first subcat and so on.
- * @return string The text but with the css values replaced.
- */
- private function replaceCssValues($text, $values, $steps) {
- if(!is_array($values)) {
- return $text;
- }
- foreach($values as $name => $value) {
- $loopSteps = $steps;
- $loopSteps[] = $name;
- if(is_array($value)) {
- //Handle the array recursivly.
- $text = $this->replaceCssValues($text, $value,
- $loopSteps + array($name));
- } else {
- $token = $this->getToken($loopSteps);
- $value = $this->getValue($loopSteps);
- //Replace the value.
- $text = str_replace($token." ", $value." ", $text);
- //We allow a trailing semicolon too.
- $text = str_replace($token.";", $value.";", $text);
- }
- }
- return $text;
- }
-
- /**
- * Gets the CSS value that correspond to the specified steps.
- * @param array $steps The steps that have been taken to get to the value.
- * The elements should be sorted in the order that they have been
- * traversed. I.e. the namespace should be first, then the first subcat
- * and so on.
- * @return string The corresponding CSS value.
- * @throws NoSuchElementException If the specified steps do not correspond
- * to an existing CSS value.
- * @throws IllegalArgumentException If the steps didn't end at a leaf.
- */
- private function getValue($steps) {
- if(!isset($this->cssValues)) {
- //Make sure that the css values are loaded.
- $this->cssValues = $this->loadCssValueTree();
- }
-
- $node = $this->cssValues;
- foreach($steps as $step) {
- if(!isset($node[$step])) {
- //Value not found, throw an exception.
- throw new NoSuchElementException("No value could be found " .
- "for the parameter (" . implode(", ", $steps). ")");
- }
- $node = $node[$step];
- }
- if(is_array($node)) {
- throw new IllegalArgumentException("The steps (" .
- implode(", ", $steps) . ") did not end at a leaf.");
- }
-
- return $node;
- }
-
- /**
- * Gets the token that represents the steps take. It's the token that should
- * be replaced in any text by the corresponding value.
- * @param array $steps The steps that have been taken to get to the value.
- * The elements should be sorted in the order that they have been
- * traversed. I.e. the namespace should be first, then the first subcat
- * and so on.
- * @return string The corresponding token.
- */
- private function getToken($steps) {
- $token = "\$";
- foreach($steps as $step) {
- if(strlen($token) != 1) {
- $token .= ":";
- }
- $token .= $step;
- }
- return $token;
- }
-
- /**
- * Ensures that a css file with the specified name is included in the page
- * output.
- * @param string $cssName The name of the css file without the .css extension,
- * i.e. 'form' if one wants to ensure that the file in the css dir named
- * 'form.css' is included.
- */
- public function includeCss($cssName) {
- try {
- if(strpos($cssName, '/') !== false) {
- throw new InvalidArguementException('Invalid library name ' .
- '('.$cssName.').');
- }
- if(!file_exists(DOC_ROOT.FOLDER_CSS.$cssName.'.css')) {
- throw new FileNotFoundException('CSS file ('.$cssName.'.css) ' .
- 'does not exist.');
- }
-
- //Add it if it is not already included.
- if(!in_array($cssName, $this->cssNames)) {
- $this->cssNames[] = $cssName;
- }
- } catch(Exception $e) {
- logException($e);
- }
- }
-
- /**
- * Gets all the CSS links that should be added.
- */
- public function getHeadAddition() {
- $links = array();
- foreach($this->cssNames as $cssName) {
- try {
- $links[] = $this->getCssLink($cssName);
- } catch(Exception $e) {
- logException($e);
- }
- }
- return implode("\n", $links);
- }
- }
- ?>