<?php
/**
* NestedSelectors
*
* @author Anthony Short
* @dependencies None
**/
class NestedSelectors
{
/**
* Array of selectors to skip and keep them nested.
* It just checks if the string is present, so it can
* just be part of a string, like the @media rule is below.
*
* @var array
*/
protected static $skip = array
(
'@media',
);
/**
* Stores the CSS comments so they don't break processing
*
* @var array
*/
private static $comments = array();
/**
* The main processing function called by Scaffold. MUST return $css!
*
* @author Anthony Short
* @return $css string
*/
public static function post_process()
{
# These will break the xml, so we'll transform them for now
Scaffold::$css->convert_entities('encode');
$xml = self::to_xml(Scaffold::$css);
if( !$xml ) {
return null;
}
$css = "";
foreach($xml->children() as $key => $value)
{
$attributes = (array)$value->attributes();
$attributes = $attributes['@attributes'];
/*
Comments are given the same <property> node as actual properties
so that they are output next to their respective property in a list
rather than being all put together at the end of the list.
So we need to search for these properties as well as the normal rules.
*/
if($key == 'property')
{
if($attributes['name'] == 'comment')
{
$css .= self::parse_comment($attributes['value']);
}
}
/*
Imports should be at the root level, so we'll assume they are.
If they're nested inside a rule, they'll just be ignored.
*/
elseif($key == 'import')
{
$imports[] = array
(
'url' => $attributes['url'],
'media' => $attributes['media']
);
}
/*
Otherwise it's just a rule
*/
else
{
$css .= self::parse_rule($value);
}
}
if(isset($imports))
{
foreach(array_reverse($imports) as $import)
{
$css = "@import '{$import['url']}' " . $import['media'] . ";" . $css;
}
}
Scaffold::$css->string = str_replace('#NEWLINE#', "\n", $css);
Scaffold::$css->convert_entities('decode');
}
/**
* Parses the comment nodes
*
* @param $comment
* @return string
*/
private static function parse_comment($comment)
{
return '/*'. self::$comments[$comment] .'*/';
}
/**
* Parse the css selector rule
*
* @author Anthony Short
* @param $rule
* @return return type
*/
public static function parse_rule($rule, $parent = '')
{
$css_string = "";
$property_list = "";
$parent = trim($parent);
$skip = false;
# Get the selector and store it away
foreach($rule->attributes() as $type => $value)
{
$child = (string)$value;
# if its NOT a root selector and has parents
if($parent != "")
{
$parent = explode(",", $parent);
foreach($parent as $parent_key => $parent_value)
{
$parent[$parent_key] = self::parse_selector(trim($parent_value), $child);
}
$parent = implode(",", $parent);
}
elseif( strstr($child,'@media') )
{
$skip = true;
$parent = $child;
}
# Otherwise it's a root selector
else
{
$parent = $child;
}
}
foreach($rule->property as $p)
{
$property = (array)$p->attributes();
$property = $property['@attributes'];
if($property['name'] == 'comment')
{
$property_list .= self::parse_comment($property['value']);
}
else
{
$property_list .= $property['name'].":".$property['value'].";";
}
}
# Create the css string
if($property_list != "" && $skip !== true)
{
$css_string .= $parent . "{" . $property_list . "}";
}
foreach($rule->rule as $inner_rule)
{
# If the selector is in our skip array in the
# member variable, we'll leave the selector as nested.
foreach(self::$skip as $selector)
{
if(strstr($parent, $selector))
{
$skip = true;
continue;
}
}
# We don't want the selectors inside @media to have @media before them
if($skip)
{
$css_string .= self::parse_rule($inner_rule, '');
}
else
{
$css_string .= self::parse_rule($inner_rule, $parent);
}
}
# Build our @media string full of these properties if we need to
if($skip)
{
$css_string = $parent . "{" . $css_string . "}";
}
return $css_string;
}
/**
* Parses the parent and child to find the next parent
* to pass on to the function
*
* @author Anthony Short
* @param $parent
* @param $child
* @param $atmedia Is this an at media group?
* @return string
*/
public static function parse_selector($parent, $child)
{
# If there are listed parents eg. #id, #id2, #id3
if(strstr($child, ","))
{
$parent = self::split_children($child, $parent);
}
# If the child references the parent selector
elseif (strstr($child, "#SCAFFOLD-PARENT#"))
{
$parent = str_replace("#SCAFFOLD-PARENT#", $parent, $child);
}
# Otherwise, do it normally
else
{
$parent = "$parent $child";
}
return $parent;
}
/**
* Splits selectors with , and adds the parent to each
*
* @author Anthony Short
* @param $children
* @param $parent
* @return string
*/
public static function split_children($children, $parent)
{
$children = explode(",", $children);
foreach($children as $key => $child)
{
# If the child references the parent selector
if (strstr($child, "#SCAFFOLD-PARENT#"))
{
$children[$key] = str_replace("#SCAFFOLD-PARENT#", $parent, $child);
}
else
{
$children[$key] = "$parent $child";
}
}
return implode(",",$children);
}
/**
* Transforms CSS into XML
*
* @return string $css
*/
public static function to_xml($css)
{
# Convert comments
self::$comments = array();
$xml = preg_replace_callback('/\/\*(.*?)\*\//sx', function ($comment)
{
// Encode new lines
$comment = preg_replace('/\n|\r/', '#NEWLINE#',$comment[1]);
// Save it
self::$comments[] = $comment;
return "<property name=\"comment\" value=\"" . (count(self::$comments) - 1) . "\" />";
} ,$css);
# Convert imports
$xml = preg_replace(
'/
@import\\s+
(?:url\\(\\s*)? # maybe url(
[\'"]? # maybe quote
(.*?) # 1 = URI
[\'"]? # maybe end quote
(?:\\s*\\))? # maybe )
([a-zA-Z,\\s]*)? # 2 = media list
; # end token
/x',
"<import url=\"$1\" media=\"$2\" />",
$xml
);
# Add semi-colons to the ends of property lists which don't have them
$xml = preg_replace('/((\:|\+)[^;])*?\}/', "$1;}", $xml);
# Transform properties
$xml = preg_replace_callback(
'/([-_A-Za-z*]+)\s*:\s*([^;}{]+)(?:;)/i',
function ($data)
{
return "<property name=\"".trim($data[1])."\" value=\"".trim($data[2])."\" />\n";
},
$xml
);
# Transform selectors
$xml = preg_replace_callback(
'/(\s*)([_@#.0-9A-Za-z\/\+~*\|\(\)\[\]^\"\'=\$:,\s-]*?)\{/m',
function ($data)
{
return $data[1]."\n<rule selector=\"".preg_replace('/\s+/', ' ', trim($data[2]))."\">\n";
},
$xml
);
# Close rules
$xml = preg_replace('/\;?\s*\}/', "\n</rule>", $xml);
# Indent everything one tab
$xml = preg_replace('/\n/', "\r\t", $xml);
# Tie it up with a bow
$xml = '<?xml version="1.0" ?'.">\r<css>\r\t$xml\r</css>\r";
libxml_use_internal_errors(true);
$data = simplexml_load_string($xml);
if( !$data ) {
Scaffold::log('Nested Selectors: ' . print_r(libxml_get_errors(), true), 0);
}
libxml_use_internal_errors(false);
return $data;
}
/**
* Turns CSS comments into an xml format
*
* @param $comment
* @return return type
*/
protected function encode_comment($comment)
{
// Encode new lines
$comment = preg_replace('/\n|\r/', '#NEWLINE#',$comment[1]);
// Save it
self::$comments[] = $comment;
return "<property name=\"comment\" value=\"" . (count(self::$comments) - 1) . "\" />";
}
/**
* Transform Property
*
* @param $data regex matches
* @return string
*/
protected function _propertyTransform($data)
{
return "<property name=\"".trim($data[1])."\" value=\"".trim($data[2])."\" />\n";
}
/**
* Transform Selector
*
* @param $data regex matches
* @return string
*/
protected function _selectorTransform($data)
{
return $data[1]."\n<rule selector=\"".preg_replace('/\s+/', ' ', trim($data[2]))."\">\n";
}
}