dear-anon/wp-content/plugins/child-theme-configurator/includes/classes/CSS.php

2205 lines
97 KiB
PHP

<?php
// Exit if accessed directly
if ( !defined( 'ABSPATH' ) ) exit;
/*
Class: ChildThemeConfiguratorCSS
Plugin URI: http://www.childthemeconfigurator.com/
Description: Handles all CSS input, output, parsing, normalization and storage
Version: 2.5.6
Author: Lilaea Media
Author URI: http://www.lilaeamedia.com/
Text Domain: chld_thm_cfg
Domain Path: /lang
License: GPLv2
Copyright (C) 2014-2018 Lilaea Media
*/
class ChildThemeConfiguratorCSS {
// data dictionaries
var $dict_query; // @media queries and 'base'
var $dict_sel; // selectors
var $dict_qs; // query/selector lookup
var $dict_rule; // css rules
var $dict_val; // css values
var $dict_seq; // child load order (priority)
var $dict_token;
// hierarchies
var $val_ndx; // selector => rule => value hierarchy
// key counters
var $qskey; // counter for dict_qs
var $querykey; // counter for dict_query
var $selkey; // counter for dict_sel
var $rulekey; // counter for dict_rule
var $valkey; // counter for dict_val
var $tokenkey; // counter for dict_token
// from parent/child form
var $child; // child theme slug
var $parnt; // parent theme slug
var $configtype; // legacy plugin slug
var $handling; // child stylesheet handling option
var $enqueue; // whether or not to load parent theme
var $ignoreparnt; // no not parse or enqueue parent
var $qpriority;
var $mpriority;
var $hasstyles;
var $parntloaded;
var $childloaded;
var $parnt_deps; //
var $child_deps; //
var $forcedep;
var $swappath;
var $addl_css;
var $cssunreg;
var $csswphead;
var $cssnotheme;
var $reorder;
// header settings
var $child_name; // child theme name
var $child_author; // child theme author
var $child_authoruri; // child theme author website
var $child_themeuri; // child theme website
var $child_descr; // child theme description
var $child_tags; // child theme tags
var $child_version; // stylesheet version
// miscellaneous properties
var $fsize; // used to check if styles changed since last update
var $converted; // @imports coverted to <link>?
var $version; // version of last saved data
var $templates; // cache of parent template files
var $imports; // @import rules
var $recent; // history of edited styles
var $max_sel;
var $memory;
var $styles; // temporary update cache
var $temparray;
var $instances = array();
var $vendorrule = array(
'box\-sizing',
'font\-smoothing',
'border(\-(top|right|bottom|left))*\-radius',
'box\-shadow',
'transition',
'transition\-property',
'transition\-duration',
'transition\-timing\-function',
'transition\-delay',
'hyphens',
'transform',
'columns',
'column\-gap',
'column\-count',
);
var $configvars = array(
'addl_css',
'forcedep',
'swappath',
'cssunreg',
'csswphead',
'cssnotheme',
'reorder',
'parnt_deps',
'child_deps',
'hasstyles',
'parntloaded',
'childloaded',
'ignoreparnt',
'qpriority',
'mpriority',
'enqueue',
'handling',
'templates',
'max_sel',
'imports',
'child_version',
'child_author',
'child_name',
'child_themeuri',
'child_authoruri',
'child_descr',
'child_tags',
'parnt',
'child',
'configtype', // legacy support
'valkey',
'rulekey',
'qskey',
'selkey',
'querykey',
'tokenkey',
'recent',
'converted',
'fsize',
'version',
);
var $dicts = array(
'dict_qs' => 0,
'dict_sel' => 0,
'dict_query' => 0,
'dict_rule' => 0,
'dict_val' => 0,
'dict_seq' => 0,
'dict_token' => 0,
'val_ndx' => 0,
);
var $packer; // packer object
function __construct() {
$this->mem_chk();
// scalars
$this->querykey = 0;
$this->selkey = 0;
$this->qskey = 0;
$this->rulekey = 0;
$this->valkey = 0;
$this->max_sel = 0;
$this->child_name = '';
$this->child_author = 'Child Theme Configurator';
$this->child_themeuri = '';
$this->child_authoruri = '';
$this->child_descr = '';
$this->child_tags = '';
$this->child_version = '1.0';
$this->configtype = 'theme'; // legacy support
$this->child = '';
$this->parnt = '';
$this->ignoreparnt = 0;
$this->qpriority = 10;
$this->mpriority = 10;
$this->version = '2.5.6';
// do not set enqueue, not being set is used to flag old versions
// multi-dim arrays
$this->templates = array();
$this->imports = array( 'child' => array(), 'parnt' => array() );
$this->recent = array();
$this->packer = new ChildThemeConfiguratorPacker();
}
// helper function to globalize ctc object
function ctc() {
return ChildThemeConfigurator::ctc();
}
function mem_chk() {
$currmemory = $this->memory;
if ( function_exists( 'memory_get_peak_usage' ) ) {
$usage = memory_get_peak_usage();
} else {
$usage = memory_get_usage();
}
$this->memory = $usage;
$usage -= $currmemory;
return number_format( $this->memory / ( 1024 * 1024 ), 2 ) . ' MB diff ' . number_format( $usage / 1024, 2 ) . ' kB';
}
// loads current ctc config data
function load_config( $key = NULL ) {
//list(, $callerarr) = debug_backtrace(false);
//$caller = ( isset( $callerarr[ 'class' ] ) ? $callerarr[ 'class' ] . '::' : '' ) . $callerarr[ 'function' ];
if ( $key && isset( $this->dicts[ $key ] ) ):
$option = CHLD_THM_CFG_OPTIONS . apply_filters( 'chld_thm_cfg_option', '' );
if ( !$this->dicts[ $key ] ): // dict not loaded yet
//$this->ctc()->debug( 'memory before load: ' . $this->mem_chk(), __FUNCTION__, __CLASS__ );
//$this->ctc()->debug( $option . '_' . $key . ' -- called from ' . $caller, __FUNCTION__, __CLASS__ );
//
if ( !$this->ctc()->is_new && ( $config = get_site_option( $option . '_' . $key ) ) ):
$this->{ $key } = $config; // if not new child theme and option exists, load it
else:
$this->{ $key } = array(); // otherwise load empty array
endif;
$this->dicts[ $key ] = 1; // flag as loaded
//$this->ctc()->debug( 'memory after load: ' . $this->mem_chk(), __FUNCTION__, __CLASS__ );
else:
//$this->ctc()->debug( $key . ' already loaded -- called from ' . $caller, __FUNCTION__, __CLASS__ );
endif;
endif;
// initial setup
if ( empty( $key ) && !isset( $this->enqueue ) ):
$option = CHLD_THM_CFG_OPTIONS . apply_filters( 'chld_thm_cfg_option', '' );
//$this->ctc()->debug( 'loading option: ' . $option . '_' . ( $key ? $key : 'configvars' )
// . ' -- called from ' . $caller, __FUNCTION__, __CLASS__ );
if ( ( $configarray = get_site_option( $option . '_configvars' ) ) && count( $configarray ) ):
foreach ( $this->configvars as $configkey ):
if ( isset( $configarray[ $configkey ] ) )
$this->{$configkey} = $configarray[ $configkey ];
endforeach;
// convert dictionaries from < 2.1.0
if ( empty( $configarray[ 'version' ] ) || version_compare( $configarray[ 'version' ], '2.1.0', '<' ) ):
$this->convert_dict_arrays();
else:
$this->ctc()->debug( 'dict format up to date', __FUNCTION__, __CLASS__ );
endif;
else:
return FALSE;
endif;
endif;
}
// writes ctc config data to options api
function save_config( $override = NULL ) {
// set latest stylesheet size
$this->get_stylesheet_path();
global $wpdb;
if ( isset( $override ) ) $option = $override;
else $option = apply_filters( 'chld_thm_cfg_option', '' );
$option = CHLD_THM_CFG_OPTIONS . $option;
$configarray = array();
foreach ( $this->configvars as $configkey )
$configarray[ $configkey ] = empty( $this->{$configkey} ) ? NULL : $this->{ $configkey };
//$this->ctc()->debug( 'saving option: ' . $option . '_configvars', __FUNCTION__, __CLASS__ );
if ( is_multisite() ):
update_site_option( $option . '_configvars', $configarray );
else:
// do not autoload ( passing false above only works if value changes
update_option( $option . '_configvars', $configarray, FALSE );
endif;
foreach ( $this->dicts as $configkey => $loaded ):
if ( $loaded ): // only save if dict is loaded
//$this->ctc()->debug( 'saving option: ' . $option . '_' . $configkey, __FUNCTION__, __CLASS__ );
if ( is_multisite() ):
update_site_option( $option . '_' . $configkey, $this->{$configkey} );
else:
// do not autoload ( passing false for update_site_option only works if value changes )
update_option( $option . '_' . $configkey, $this->{$configkey}, FALSE );
endif;
endif;
endforeach;
}
/**
* determine effective stylesheet path and measure size
*/
function get_stylesheet_path() {
$stylesheet = apply_filters( 'chld_thm_cfg_target', $this->get_child_target( $this->ctc()->get_child_stylesheet() ), $this );
$this->fsize = filesize( $stylesheet );
$this->ctc()->debug( 'updated file size: ' . $this->fsize, __FUNCTION__, __CLASS__ );
return $stylesheet;
}
/**
* get_prop
* Getter interface (data sliced different ways depending on objname )
*/
function get_prop( $property, $params = NULL ) {
switch ( $property ):
case 'fsize':
return empty( $this->fsize ) ? FALSE : $this->fsize;
case 'converted':
return !empty( $this->converted );
case 'max_sel':
return empty( $this->max_sel ) ? FALSE : $this->max_sel;
case 'imports':
return $this->obj_to_utf8( !empty( $this->imports[ 'child' ] ) && is_array( $this->imports[ 'child' ] ) ?
( current( $this->imports[ 'child' ] ) == 1 ?
array_keys( $this->imports[ 'child' ] ) :
array_keys( array_flip( $this->imports[ 'child' ] ) ) ) :
array() );
case 'queries':
return $this->obj_to_utf8( $this->denorm_dict_qs() );
case 'selectors':
return empty( $params[ 'key' ] ) ?
array() : $this->obj_to_utf8( $this->denorm_dict_qs( $params[ 'key' ] ) );
case 'rule_val':
return empty( $params[ 'key' ] ) ? array() : $this->denorm_rule_val( $params[ 'key' ] );
case 'val_qry':
if ( isset( $params[ 'rule' ] ) ):
return empty( $params[ 'key' ] ) ?
array() : $this->denorm_val_query( $params[ 'key' ], $params[ 'rule' ] );
endif;
case 'qsid':
return empty( $params[ 'key' ] ) ?
array() : $this->obj_to_utf8( $this->denorm_sel_val( $params[ 'key' ] ) );
case 'rules':
$this->load_config( 'dict_rule' );
asort( $this->dict_rule );
//ksort( $this->dict_rule );
//return $this->obj_to_utf8( $this->dict_rule );
/** lookup ** -- need to flip array??? */
return $this->obj_to_utf8( array_flip( $this->dict_rule ) );
case 'child':
return $this->child;
case 'parnt':
return $this->parnt;
case 'configtype': // legacy plugin extension support
return $this->configtype;
case 'enqueue':
return empty( $this->enqueue ) ? FALSE : $this->enqueue;
case 'addl_css':
return empty( $this->addl_css ) ? array() : $this->addl_css;
case 'parnt_imp':
return empty( $this->parnt_imp ) ? array() : $this->parnt_imp;
case 'forcedep': // v2.1.3
return empty( $this->forcedep ) ? array() : array_keys( $this->forcedep );
case 'swappath': // v2.3.1
return empty( $this->swappath ) ? array() : (array) $this->swappath;
case 'parnt_deps':
return empty( $this->parnt_deps ) ? array() : $this->quotify_dependencies( 'parnt_deps' );
case 'child_deps':
return empty( $this->child_deps ) ? array() : $this->quotify_dependencies( 'child_deps' );
case 'templates':
return empty( $this->templates ) ? FALSE : $this->templates;
case 'ignoreparnt':
return empty( $this->ignoreparnt ) ? 0 : 1;
case 'qpriority':
return empty( $this->qpriority ) ? 10 : $this->qpriority;
case 'mpriority':
return empty( $this->mpriority ) ? 10 : $this->mpriority;
case 'parntloaded':
return empty( $this->parntloaded ) ? FALSE : $this->parntloaded;
case 'childloaded':
return empty( $this->childloaded ) ? FALSE : $this->childloaded;
case 'hasstyles':
return empty( $this->hasstyles ) ? 0 : 1;
case 'cssunreg':
return empty( $this->cssunreg ) ? 0 : 1;
case 'csswphead':
return empty( $this->csswphead ) ? 0 : 1;
case 'cssnotheme':
return empty( $this->cssnotheme ) ? 0 : 1;
case 'reorder':
return empty( $this->reorder ) ? 0 : 1;
case 'handling':
return empty( $this->handling ) ? 'primary' : $this->handling;
case 'child_name':
return stripslashes( $this->child_name );
case 'author':
return stripslashes( $this->child_author );
case 'themeuri':
return isset( $this->child_themeuri ) ? $this->child_themeuri : FALSE;
case 'authoruri':
return isset( $this->child_authoruri ) ? $this->child_authoruri : FALSE;
case 'descr':
return isset( $this->child_descr ) ? stripslashes( $this->child_descr ) : FALSE;
case 'tags':
return isset( $this->child_tags ) ? stripslashes( $this->child_tags ) : FALSE;
case 'version':
return $this->child_version;
case 'preview':
$this->styles = '';
if ( empty( $params[ 'key' ] ) || 'child' == $params[ 'key' ] ):
$this->read_stylesheet( 'child', $this->ctc()->get_child_stylesheet() );
else:
if ( isset( $this->addl_css ) ):
foreach ( $this->addl_css as $file ):
$this->styles .= '/*** BEGIN ' . $file . ' ***/' . LF;
$this->read_stylesheet( 'parnt', $file );
$this->styles .= '/*** END ' . $file . ' ***/' . LF;
endforeach;
endif;
if ( $this->get_prop( 'hasstyles' ) && !$this->get_prop( 'ignoreparnt' ) ):
$this->styles .= '/*** BEGIN Parent style.css ***/' . LF;
$this->read_stylesheet( 'parnt', 'style.css' );
$this->styles .= '/*** END Parent style.css ***/' . LF;
endif;
if ( 'separate' == $this->get_prop( 'handling' ) ):
$this->styles .= '/*** BEGIN Child style.css ***/' . LF;
$this->read_stylesheet( 'child', 'style.css' );
$this->styles .= '/*** END Child style.css ***/' . LF;
endif;
endif;
$this->normalize_css();
return $this->styles;
break;
default:
return $this->obj_to_utf8( apply_filters( 'chld_thm_get_prop', NULL, $property, $params ) );
endswitch;
return FALSE;
}
/**
* set_prop
* Setter interface (scalar values only)
*/
function set_prop( $property, $value ) {
if ( is_null( $this->{ $property } ) || is_scalar( $this->{ $property } ) )
$this->{ $property } = $value;
else return FALSE;
}
// formats css string for accurate parsing
function normalize_css() {
if ( preg_match( "/(\}[\w\#\.]|; *\})/", $this->styles ) ): // prettify compressed CSS
$this->styles = preg_replace( "/\*\/\s*/s", "*/\n", $this->styles ); // end comment
$this->styles = preg_replace( "/\{\s*/s", " {\n ", $this->styles ); // open brace
$this->styles = preg_replace( "/;\s*/s", ";\n ", $this->styles ); // semicolon
$this->styles = preg_replace( "/\s*\}\s*/s", "\n}\n", $this->styles ); // close brace
endif;
}
function quotify_dependencies( $prop ) {
$arr = array();
foreach ( array_diff( $this->{ $prop }, $this->get_prop( 'forcedep' ) ) as $el )
$arr[] = "'" . str_replace("'", "\'", $el ) . "'";
return $arr;
}
// creates header comments for stylesheet
function get_css_header() {
return array(
'Theme Name' => $this->get_prop( 'child_name' ),
'Theme URI' => ( ( $attr = $this->get_prop( 'themeuri' ) ) ? $attr : '' ),
'Template' => $this->get_prop( 'parnt' ),
'Author' => $this->get_prop( 'author' ),
'Author URI' => ( ( $attr = $this->get_prop( 'authoruri' ) ) ? $attr : '' ),
'Description' => ( ( $attr = $this->get_prop( 'descr' ) ) ? $attr : '' ),
'Tags' => ( ( $attr = $this->get_prop( 'tags' ) ) ? $attr : '' ),
'Version' => $this->get_prop( 'version' ) . '.' . time(),
'Updated' => current_time( 'mysql' ),
);
}
function get_css_header_comment( $handling = 'primary' ) {
if ( 'separate' == $handling ):
$contents = "/*" . LF
. 'CTC Separate Stylesheet' . LF
. 'Updated: ' . current_time( 'mysql' ) . LF
. '*/' . LF;
else:
$contents = "/*" . LF;
foreach ( $this->get_css_header() as $param => $value ):
if ( $value ):
$contents .= $param . ': ' . $value . LF;
endif;
endforeach;
$contents .= LF . "*/" . LF . $this->get_css_imports();
endif;
return $contents;
}
function get_css_imports() {
$newheader = '';
if ( 'import' == $this->get_prop( 'enqueue' ) ):
$this->ctc()->debug( 'using import ', __FUNCTION__, __CLASS__ );
if ( ! $this->get_prop( 'ignoreparnt' ) )
$newheader .= "@import url('../" . $this->get_prop( 'parnt' ) . "/style.css');" . LF;
endif;
return $newheader;
}
// formats file path for child theme file
function get_child_target( $file = '', $theme = NULL ) {
return trailingslashit( get_theme_root() ) . trailingslashit( $theme ? $theme : $this->get_prop( 'child' ) ) . $file;
}
// formats file path for parent theme file
function get_parent_source( $file = 'style.css', $theme = NULL ) {
return trailingslashit( get_theme_root() ) . trailingslashit( $theme ? $theme : $this->get_prop( 'parnt' ) ) . $file;
}
/**
* get_dict_id
* lookup function retrieves normalized id from string input
* automatically adds to dictionary if it does not exist
* incrementing key value for dictionary
*/
function get_dict_id( $dict, $value ) {
if ( FALSE === ( $id = $this->lookup_dict_value( $dict, $value ) ) ):
// add value to dictionary
$id = ++$this->{ $dict . 'key' };
$this->set_dict_value( $dict, $value, $id );
endif;
return $id;
}
function lookup_dict_value( $dict, $value ){
//$this->ctc()->debug( 'dict: ' . $dict . ' value: %' . $value . '%', __FUNCTION__, __CLASS__ );
$property = 'dict_' . $dict;
if ( $id = array_search( (string) $value, $this->{ $property } ) )
return $id;
return FALSE;
}
function get_dict_value( $dict, $id ) {
$property = 'dict_' . $dict;
return ( isset( $this->{ $property }[ $id ] ) )
? $this->{ $property }[ $id ]
: FALSE;
}
function set_dict_value( $dict, $value, $id ) {
$property = 'dict_' . $dict;
$this->{ $property }[ $id ] = ( string ) $value;
}
/**
* get_qsid
* query/selector id is the combination of two dictionary values
* also throttles parsing if memory limit is reached
*/
function get_qsid( $query, $sel ) {
$qs = $this->get_dict_id( 'query', $query ) . ':' . $this->get_dict_id( 'sel', $sel );
return $this->get_dict_id( 'qs', $qs );
}
function unpack_val_ndx( $qsid ){
if ( isset( $this->val_ndx[ $qsid ] ) ):
try {
$this->packer->reset( $this->packer->decode( $this->val_ndx[ $qsid ] ) );
return $this->packer->unpack();
} catch ( Exception $e ){
$this->ctc()->debug( 'Unpack failed -- ' . $e->getMessage(), __FUNCTION__, __CLASS__ );
return FALSE;
}
endif;
return FALSE;
}
function pack_val_ndx( $qsid, $valarr ){
try {
$this->val_ndx[ $qsid ] = $this->packer->encode( $this->packer->pack( $valarr ) );
} catch ( Exception $e ){
$this->ctc()->debug( 'Pack failed -- ' . $e->getMessage(), __FUNCTION__, __CLASS__ );
}
}
/**
* update_arrays
* accepts CSS properties as raw strings and normilizes into
* CTC object arrays, creating update cache in the process.
* ( Update cache is returned to UI via AJAX to refresh page )
* This has been refactored in v1.7.5 to accommodate multiple values per property.
* @param $template p or c
* @param $query media query
* @param $sel selector
* @param $rule property (rule)
* @param $value individual value ( property has array of values )
* @param $important important flag for value
* @param $rulevalid unique id of value for property
* @param $reset clear current values to prevent multiple values from being generated from Raw CSS post input data
* @return $qsid query/selector id for this entry
*/
function update_arrays(
$template,
$query,
$sel,
$rule = NULL,
$value = NULL,
$important = 0,
$rulevalid = NULL,
$reset = FALSE
) {
// if ( $this->max_sel ) return; // Future use
if ( FALSE === strpos( $query, '@' ) )
$query = 'base';
// normalize selector styling
$sel = implode( ', ', preg_split( '#\s*,\s*#s', trim( $sel ) ) );
$qsid = $this->get_qsid( $query, $this->tokenize( $sel ) );
// set data and value
if ( $rule ):
// get ids and quit if max is reached ( get_qsid handles )
$ruleid = $this->get_dict_id( 'rule', $rule );
$valid = $this->get_dict_id( 'val', $value );
/**
* v2.1.0
* pack/unpack val_ndx
*/
// create empty array IF value array does not exist
if ( FALSE === ( $valarr = $this->unpack_val_ndx( $qsid ) ) )
$valarr = array(
$ruleid => array(
$template => array(),
),
);
// create empty array IF rule array does not exist
if ( !isset( $valarr[ $ruleid ] ) )
$valarr[ $ruleid ] = array(
$template => array(),
);
// create empty rule array if template is child and reset is TRUE
// or IF template array does not exist
if ( ( $reset && 'child' == $template ) || !isset( $valarr[ $ruleid ][ $template ] ) )
$valarr[ $ruleid ][ $template ] = array();
// rulevalid passed
//$this->ctc()->debug( 'rule: ' . $rule . ' ' . $ruleid . ' value: ' . ( '' == $value? 'NULL' : '%' . $value . '%' ) . ' ' . ( FALSE == $valid ? 'FALSE' : $valid ) . ' valarr: ' . print_r( $valarr, TRUE ), __FUNCTION__, __CLASS__ );
if ( isset( $rulevalid ) ):
$this->unset_rule_value( $valarr[ $ruleid ][ $template ], $rulevalid );
// value empty?
if ( '' === $value ):
// value exist?
elseif ( $id = $this->rule_value_exists( $valarr[ $ruleid ][ $template ], $valid ) ):
$this->unset_rule_value( $valarr[ $ruleid ][ $template ], $id );
$this->update_rule_value( $valarr[ $ruleid ][ $template ], $rulevalid, $valid, $important );
// update new value
else:
$this->update_rule_value( $valarr[ $ruleid ][ $template ], $rulevalid, $valid, $important );
endif;
// rulevalid not passed
else:
// value exist?
if ( $id = $this->rule_value_exists( $valarr[ $ruleid ][ $template ], $valid ) ):
$this->unset_rule_value( $valarr[ $ruleid ][ $template ], $id );
$this->update_rule_value( $valarr[ $ruleid ][ $template ], $id, $valid, $important );
// get new id and update new value
else:
$id = $this->get_rule_value_id( $valarr[ $ruleid ][ $template ] );
$this->update_rule_value( $valarr[ $ruleid ][ $template ], $id, $valid, $important );
endif;
endif;
// moved call to prune_if_empty to parse_post_data v2.2.5
$this->pack_val_ndx( $qsid, $valarr );
// return query selector id
return $qsid;
endif;
}
/**
* rule_value_exists
* Determine if a value already exists for a property
* and return its id
*/
function rule_value_exists( &$arr, $valid ) {
foreach ( $arr as $valarr ):
if ( isset( $valarr[ 0 ] ) && isset( $valarr[ 2 ] ) && $valid == $valarr[ 0 ] ):
return $valarr[ 2 ];
endif;
endforeach;
return FALSE;
}
/**
* get_rule_value_id
* Generate a new rulevalid by iterating existing ids
* and returning the next in sequence
*/
function get_rule_value_id( &$arr ) {
$newid = 1;
foreach ( $arr as $valarr )
if ( isset( $valarr[ 2 ] ) && $valarr[ 2 ] >= $newid ) $newid = $valarr[ 2 ] + 1;
return $newid;
}
/**
* update_rule_value
* Generate a new value subarray
*/
function update_rule_value( &$arr, $id, $valid, $important ) {
$arr[] = array(
$valid,
$important,
$id,
);
}
/**
* unset_rule_value
* Delete (splice) old value subarray from values
*/
function unset_rule_value( &$arr, $id ) {
$index = 0;
foreach ( $arr as $valarr ):
if ( $id == $valarr[ 2 ] ):
array_splice( $arr, $index, 1 );
break;
endif;
++$index;
endforeach;
}
/**
* prune_if_empty
* Automatically cleans up hierarchies when no values exist
* in either parent or child for a given selector.
*/
function prune_if_empty( $qsid ) {
$empty = $this->get_dict_id( 'val', '' );
if ( FALSE == ( $valarr = $this->unpack_val_ndx( $qsid ) ) ) return FALSE;
foreach ( $valarr as $ruleid => $arr ):
foreach ( array( 'c', 'p' ) as $template ):
if ( isset( $arr[ $template ] ) ):
// v1.7.5: don't prune until converted to multi value format
if ( !is_array( $arr[ $template ] ) ) return FALSE;
// otherwise check each value, if not empty return false
foreach ( $arr[ $template ] as $valarr )
if ( $empty != $valarr[ 0 ] ) return FALSE;
endif;
endforeach;
endforeach;
// no values, prune from sel index, val index and qs dict data ( keep other dictionary records )
unset( $this->val_ndx[ $qsid ] );
unset( $this->dict_qs[ $qsid ] );
unset( $this->dict_seq[ $qsid ] );
return TRUE;
}
/**
* recurse_directory
* searches filesystem for valid files based on parameters and returns array of filepaths.
* Core WP recurse function is not used because we require logic specific to CTC.
*/
function recurse_directory( $rootdir, $ext = 'css', $all = FALSE ) {
// make sure we are only recursing theme and plugin files
if ( !$this->is_file_ok( $rootdir, 'search' ) )
return array();
$files = array();
$dirs = array( $rootdir );
$loops = 0;
if ( 'img' == $ext )
$ext = '(' . implode( '|', array_keys( $this->ctc()->imgmimes ) ) . ')';
while( count( $dirs ) && $loops < CHLD_THM_CFG_MAX_RECURSE_LOOPS ): // failsafe valve
$loops++;
$dir = array_shift( $dirs );
if ( $handle = opendir( $dir ) ):
while ( FALSE !== ( $file = readdir( $handle ) ) ):
if ( preg_match( "/^\./", $file ) ) continue;
$filepath = trailingslashit( $dir ) . $file;
if ( is_dir( $filepath ) ):
array_unshift( $dirs, $filepath );
if ( $all ):
$files[] = $filepath;
endif;
elseif ( is_file( $filepath ) && ( $all || preg_match( "/\.".$ext."$/i", $filepath ) ) ):
$files[] = $filepath;
endif;
endwhile;
closedir( $handle );
endif;
endwhile;
return $files;
}
/**
* parse_post_data
* Parse user form input into separate properties and pass to update_arrays
* FIXME - this function has grown too monolithic - refactor and componentize
*/
function parse_post_data() {
$this->load_config( 'dict_query' );
$this->load_config( 'dict_sel' );
$this->load_config( 'dict_token' );
$this->load_config( 'dict_rule' );
$this->load_config( 'dict_val' );
$this->load_config( 'val_ndx' );
$this->load_config( 'dict_seq' );
$this->load_config( 'dict_qs' );
$this->cache_updates = TRUE;
// process RAW CSS input
if ( isset( $_POST[ 'ctc_new_selectors' ] ) ):
$this->styles = $this->parse_css_input( LF . $_POST[ 'ctc_new_selectors' ] );
$this->parse_css( 'child',
isset( $_POST[ 'ctc_sel_ovrd_query' ] ) ? trim( $_POST[ 'ctc_sel_ovrd_query' ] ) : NULL,
FALSE,
'',
TRUE
);
// process WEB FONTS & CSS inputs
elseif ( isset( $_POST[ 'ctc_child_imports' ] ) ):
$this->imports[ 'child' ] = array();
$this->styles = $this->parse_css_input( $_POST[ 'ctc_child_imports' ] );
$this->parse_css( 'child' );
// process CONFIGURE inputs
elseif ( isset( $_POST[ 'ctc_configtype' ] ) ):
ob_start();
do_action( 'chld_thm_cfg_get_stylesheets' );
$this->ctc()->updates[] = array(
'obj' => 'stylesheets',
'key' => '',
'data' => ob_get_contents(),
);
ob_end_clean();
ob_start();
do_action( 'chld_thm_cfg_get_backups' );
$this->ctc()->updates[] = array(
'obj' => 'backups',
'key' => '',
'data' => ob_get_contents(),
);
ob_end_clean();
return;
// process SAVE inputs
else:
// New query added v2.3.0
$newquery = isset( $_POST[ 'ctc_rewrite_query' ] ) ?
$this->sanitize( $this->parse_css_input( $_POST[ 'ctc_rewrite_query' ] ) ) : NULL;
$newselector = isset( $_POST[ 'ctc_rewrite_selector' ] ) ?
$this->sanitize( $this->parse_css_input( $_POST[ 'ctc_rewrite_selector' ] ) ) : NULL;
$newqsid = NULL;
// set the custom sequence value
foreach ( preg_grep( '#^ctc_ovrd_child_seq_#', array_keys( $_POST ) ) as $post_key ):
if ( preg_match( '#^ctc_ovrd_child_seq_(\d+)$#', $post_key, $matches ) ):
$qsid = $matches[ 1 ];
$seq = intval( $_POST[ $post_key ] );
$this->ctc()->debug( 'set seq( ' . $qsid . ' ): custom: ' . $seq, __FUNCTION__, __CLASS__ );
if ( $seq != $qsid ):
$this->set_dict_value( 'seq', $seq, $qsid );
else:
unset( $this->dict_seq[ $seq ] );
endif;
endif;
endforeach;
// iterate each property input
$parts = array();
foreach ( preg_grep( '#^ctc_(ovrd|\d+)_child#', array_keys( $_POST ) ) as $post_key ):
// parse input key into individual components if it matches specific format, skip otherwise
if ( preg_match( '#^ctc_(ovrd|\d+)_child_([\w\-]+?)_(\d+?)_(\d+?)(_(.+))?$#', $post_key, $matches ) ):
$valid = $matches[ 1 ]; // this is used for inputs from property value tab
$rule = $matches[ 2 ]; // property name
if ( NULL == $rule || FALSE === $this->lookup_dict_value( 'rule', $rule ) )
continue;
$qsid = $matches[ 3 ]; // query/selector id
$rulevalid = $matches[ 4 ]; // id to identify multiple values of same property
// normalize input value
$value = $this->normalize_color( $this->sanitize( $this->parse_css_input( $_POST[ $post_key ] ) ) );
// set important flag
$important = $this->is_important( $value ); // strip and set if !important passed in input
// set important if checkbox input is set
if ( !empty( $_POST[ 'ctc_' . $valid . '_child_' . $rule . '_i_' . $qsid . '_' . $rulevalid ] ) ) $important = 1;
// get current values from query/selector id if it exists, skip this property otherwise
$selarr = $this->denorm_query_sel( $qsid );
if ( empty( $selarr ) ) continue;
// if there is a "rule-part" (e.g., border or gradient properties ), store in parts array and process separately.
if ( !empty( $matches[ 6 ] ) ):
$parts[ $qsid ][ $rule ][ 'values' ][ $rulevalid ][ $matches[ 6 ] ] = $value;
$parts[ $qsid ][ $rule ][ 'values' ][ $rulevalid ][ 'important' ] = $important;
$parts[ $qsid ][ $rule ][ 'query' ] = $selarr[ 'query' ];
$parts[ $qsid ][ $rule ][ 'selector' ] = $selarr[ 'selector' ];
// otherwise process this property
else:
$newqsid = $this->update_property(
$newquery,
$newselector,
$selarr[ 'query' ],
$selarr[ 'selector' ],
$rule,
$value,
$important,
$rulevalid
);
endif;
endif;
endforeach;
/**
* Inputs for border and background-image are broken into multiple "rule parts"
* With the addition of multiple property values in v1.7.5, the parts loop
* has been modified to segment the parts into rulevalids under a new 'values' array.
* The important flag has also been moved into the parts array.
*/
foreach ( $parts as $qsid => $rules ):
foreach ( $rules as $rule => $rule_arr ):
// new 'values' array to segment parts into rulevalids
foreach ( $rule_arr[ 'values' ] as $rulevalid => $rule_part ):
if ( 'background' == $rule ):
$value = $rule_part[ 'background_url' ];
elseif ( 'background-image' == $rule ):
if ( empty( $rule_part[ 'background_url' ] ) ):
// custom multi-stop or radial gradients can be passed verbatim in the origin field
// but will not be parsed for vender-prefix support.
if ( !empty( $rule_part[ 'background_origin' ] )
&& preg_match( '{gradient}', $rule_part[ 'background_origin' ] ) ):
$value = $rule_part[ 'background_origin' ];
elseif ( empty( $rule_part[ 'background_color2' ] ) ):
$value = '';
else:
if ( empty( $rule_part[ 'background_origin' ] ) )
$rule_part[ 'background_origin' ] = 'top';
if ( empty( $rule_part[ 'background_color1' ] ) )
$rule_part[ 'background_color1' ] = $rule_part[ 'background_color2' ];
$value = implode( ':', array(
$rule_part[ 'background_origin' ],
$rule_part[ 'background_color1' ], '0%',
$rule_part[ 'background_color2' ], '100%'
) );
endif;
else:
$value = $rule_part[ 'background_url' ];
endif;
elseif ( preg_match( '#^border(\-(top|right|bottom|left))?$#', $rule ) ):
if ( empty( $rule_part[ 'border_width' ] ) && !empty( $rule_part[ 'border_color' ] ) )
$rule_part[ 'border_width' ] = '1px';
if ( empty( $rule_part[ 'border_style' ] ) && !empty( $rule_part[ 'border_color' ] ) )
$rule_part[ 'border_style' ] = 'solid';
$value = implode( ' ', array(
$rule_part[ 'border_width' ],
$rule_part[ 'border_style' ],
$rule_part[ 'border_color' ]
) );
else:
$value = '';
endif;
$newqsid = $this->update_property(
$newquery,
$newselector,
$rule_arr[ 'query' ],
$rule_arr[ 'selector' ],
$rule,
$value,
$rule_part[ 'important' ],
$rulevalid
);
endforeach;
endforeach;
endforeach;
// remove if all values have been cleared - moved from update_arrays v2.2.5
$this->prune_if_empty( $qsid );
if ( $newqsid != $qsid )
$qsid = $newqsid;
// return updated qsid to browser to update form
if ( $this->ctc()->cache_updates )
$this->ctc()->updates[] = array(
'obj' => 'qsid',
'key' => $qsid,
'data' => $this->obj_to_utf8( $this->denorm_sel_val( $qsid ) ),
);
do_action( 'chld_thm_cfg_update_qsid', $qsid );
endif;
// update enqueue function if imports have not been converted or new imports passed
if ( isset( $_POST[ 'ctc_child_imports' ] ) || !$this->get_prop( 'converted' ) )
add_action( 'chld_thm_cfg_addl_files', array( $this->ctc(), 'enqueue_parent_css' ), 15, 2 );
}
/**
* standarized property update function
* added v2.3.0
*/
function update_property(
$newquery,
$newselector,
$query,
$selector,
$rule,
$value,
$important,
$rulevalid
){
// If this is a renamed selector, add new selector to data
// otherwise update existing selector
$newqsid = $this->update_arrays(
'c',
$newquery ? $newquery : $query,
$newselector ? $newselector : $selector,
$rule,
trim( $value ),
$important,
$rulevalid
);
// if query or selector have been renamed,
// clear the original selector's value:
if ( $newquery || $newselector ):
$qsid = $this->update_arrays(
'c',
$query,
$selector,
$rule,
'',
0,
$rulevalid
);
// add new sequence entry
$seq = $this->get_dict_value( 'seq', $qsid );
$this->set_dict_value( 'seq', $newqsid, $seq );
endif;
return $newqsid;
}
/**
* parse_css_input
* Normalize raw user CSS input so that the parser can read it.
*/
function parse_css_input( $styles ) {
return $this->repl_octal( stripslashes( $this->esc_octal( $styles ) ) );
}
// strips non printables and potential commands
function sanitize( $styles ) {
return sanitize_text_field( preg_replace( '/[^[:print:]]|[\{\}].*/', '', $styles ) );
}
// escapes octal values in input to allow for specific ascii strings in content rule
function esc_octal( $styles ){
return preg_replace( "#(['\"])\\\\([0-9a-f]{4})(['\"])#i", "$1##bs##$2$3", $styles );
}
// unescapes octal values for writing specific ascii strings in content rule
function repl_octal( $styles ) {
return str_replace( "##bs##", "\\", $styles );
}
/**
* parse_css_file
* reads stylesheet to get WordPress meta data and passes rest to parse_css
*/
function parse_css_file( $template, $file = 'style.css', $cfgtemplate = FALSE ) {
if ( '' == $file ) $file = 'style.css';
/**
* Future use: have we run out of memory?
*
if ( $this->max_sel ):
//$this->ctc()->debug( 'Insufficient memory to parse file.', __FUNCTION__, __CLASS__ );
return FALSE;
endif;
*/
// turn off caching when parsing files to reduce memory usage
$this->ctc()->cache_updates = FALSE;
$this->styles = ''; // reset styles
$this->read_stylesheet( $template, $file );
// get theme name
$regex = '#Theme Name:\s*(.+?)\n#i';
preg_match( $regex, $this->styles, $matches );
$child_name = $this->get_prop( 'child_name' );
if ( !empty( $matches[ 1 ] ) && 'child' == $template && empty( $child_name ) ) $this->set_prop( 'child_name', $matches[ 1 ] );
$this->parse_css(
$cfgtemplate ? $cfgtemplate : $template,
NULL,
TRUE,
$this->ctc()->normalize_path( dirname( $file ) )
);
}
// loads raw css file into local memory
function read_stylesheet( $template = 'child', $file = 'style.css' ) {
// these conditions support revert/restore option in 1.6.0+
if ( 'all' == $file ) return;
elseif ( '' == $file ) $file = 'style.css';
// end revert/restore conditions
$source = $this->get_prop( $template );
if ( empty( $source ) || !is_scalar( $source ) ) return FALSE;
$themedir = trailingslashit( get_theme_root() ) . $source;
$stylesheet = apply_filters( 'chld_thm_cfg_' . $template, trailingslashit( $themedir )
. $file , ( $this->ctc()->is_legacy() ? $this : $file ) ); // support for plugins extension < 2.0
// read stylesheet
if ( $stylesheet_verified = $this->is_file_ok( $stylesheet, 'read' ) ):
/**
* Future use: make sure we have space to parse
*
if ( filesize( $stylesheet_verified ) * 3 > $this->ctc()->get_free_memory() ):
$this->max_sel = 1;
//$this->ctc()->debug( 'Insufficient memory to read file', __FUNCTION__, __CLASS__ );
return;
endif;
*/
$this->styles .= @file_get_contents( $stylesheet_verified ) . "\n";
//echo 'count after get contents: ' . strlen( $this->styles ) . LF;
else:
//echo 'not ok!' . LF;
endif;
}
/**
* parse_css
* Accepts raw CSS as text and parses into individual properties.
* FIXME - this function has grown too monolithic - refactor and componentize
* FIXME - migrate to event parser? handle comments?
*/
function parse_css( $template, $basequery = NULL, $parse_imports = TRUE, $relpath = '', $reset = FALSE ) {
//$this->load_config( 'sel_ndx' );
$this->load_config( 'val_ndx' );
$this->load_config( 'dict_query' );
$this->load_config( 'dict_sel' );
$this->load_config( 'dict_token' );
$this->load_config( 'dict_qs' );
$this->load_config( 'dict_val' );
$this->load_config( 'dict_rule' );
$this->load_config( 'dict_seq' );
if ( FALSE === strpos( $basequery, '@' ) ):
$basequery = 'base';
endif;
$ruleset = array();
// ignore commented code
$this->styles = preg_replace( '#\/\*.*?\*\/#s', '', $this->styles );
// space braces to ensure correct matching
$this->styles = preg_replace( '#([\{\}])\s*#', "$1\n", $this->styles );
// get all imports
if ( $parse_imports ):
$regex = '#(\@import\s+url\(.+?\));#';
preg_match_all( $regex, $this->styles, $matches );
foreach ( preg_grep( '#' . $this->get_prop( 'parnt' ) . '\/style\.css#', $matches[ 1 ], PREG_GREP_INVERT ) as $import ):
$import = preg_replace( "#^.*?url\(([^\)]+?)\).*#", "$1", $import );
$import = preg_replace( "#[\'\"]#", '', $import );
$import = '@import url(' . trim( $import ) . ')';
$this->imports[ $template ][ $import ] = 1;
endforeach;
if ( $this->ctc()->cache_updates ):
$this->ctc()->updates[] = array(
'obj' => 'imports',
'data' => array_keys( $this->imports[ $template ] ),
);
endif;
endif;
// break into @ segments
foreach ( array(
'#(\@media[^\{]+?)\{(\s*?)\}#', // get any placehoder (empty) media queries
'#(\@media[^\{]+?)\{(.*?\})?\s*?\}#s', // get all other media queries
) as $regex ): // (((?!\@media).) backreference too memory intensive - rolled back in v 1.4.8.1
preg_match_all( $regex, $this->styles, $matches );
foreach ( $matches[ 1 ] as $segment ):
$segment = $this->normalize_query( $segment );
$ruleset[ $segment ] = array_shift( $matches[ 2 ] )
. ( isset( $ruleset[ $segment ] ) ?
$ruleset[ $segment ] : '' );
endforeach;
// stripping rulesets leaves base styles
$this->styles = preg_replace( $regex, '', $this->styles );
endforeach;
$ruleset[ $basequery ] = $this->styles;
$qsid = NULL;
foreach ( $ruleset as $query => $segment ):
// make sure there is a newline before the first selector
$segment = LF . $segment;
// make sure there is semicolon before closing brace
$segment = preg_replace( '#(\})#', ";$1", $segment );
// parses selectors and corresponding rules
$regex = '#\n\s*([\[\.\#\:\w][\w\-\s\(\)\[\]\'\^\*\.\#\+\~:,"=>]+?)\s*\{(.*?)\}#s'; //[^\{] may be to expensive
preg_match_all( $regex, $segment, $matches );
foreach( $matches[ 1 ] as $sel ):
$stuff = array_shift( $matches[ 2 ] );
$this->update_arrays(
'child' == $template ? 'c' : 'p',
$query,
$sel
);
// handle base64 data
$stuff = preg_replace( '#data:([^;]+?);([^\)]+?)\)#s', "data:$1%%semi%%$2)", $stuff );
// rule semaphore makes sure rules are only reset the first time they appear
$resetrule = array();
foreach ( explode( ';', $stuff ) as $ruleval ):
if ( FALSE === strpos( $ruleval, ':' ) ) continue;
list( $rule, $value ) = explode( ':', $ruleval, 2 );
// trim, replace spaces with dashes, make lowercase
$rule = preg_replace( '/\s+/', '-', trim( strtolower( $rule ) ) );
$rule = preg_replace_callback( "/[^\w\-]/", array( $this, 'to_ascii' ), $rule );
// handle base64 data
$value = trim( str_replace( '%%semi%%', ';', $value ) );
$rules = $values = array();
// save important flag
$important = $this->is_important( $value );
// normalize color
$value = $this->normalize_color( $value );
// normalize font
if ( 'font' == $rule ):
$this->normalize_font( $value, $rules, $values );
// normalize background
elseif( 'background' == $rule ):
$this->normalize_background( $value, $rules, $values );
// normalize margin/padding
elseif ( 'margin' == $rule || 'padding' == $rule ):
$this->normalize_margin_padding( $rule, $value, $rules, $values );
else:
$rules[] = $rule;
$values[] = $value;
endif;
foreach ( $rules as $rule ):
$value = trim( array_shift( $values ) );
// normalize zero values
$value = preg_replace( '#\b0(px|r?em)#', '0', $value );
// normalize gradients
if ( FALSE !== strpos( $value, 'gradient' ) ):
if ( FALSE !== strpos( $rule, 'filter' ) ):
// treat as background-image, we'll add filter rule later
$rule = 'background-image';
continue;
endif;
if ( FALSE !== strpos( $value, 'webkit-gradient' ) ) continue; // bail on legacy webkit, we'll add it later
$value = $this->encode_gradient( $value );
endif;
// normalize common vendor prefixes
$rule = preg_replace( '#(\-(o|ms|moz|webkit)\-)?(' . implode( '|', $this->vendorrule ) . ')#', "$3", $rule );
if ( 'parnt' == $template && 'background-image' == $rule && strstr( $value, 'url(' ) )
$value = $this->convert_rel_url( $value, $relpath );
/**
* The reset flag forces the values for a given property (rule) to be rewritten completely
* when using the raw CSS input or when reading from a stylesheet.
* This permits complete blocks of style data to be entered verbatim, replacing existing styles.
* When entering individual values from the Query/Selector inputs, multiple fallback values for existing
* properties can be added in the order they are entered (e.g., margin: 1rem; margin: 1em;)
*/
if ( !$reset ) $resetrule[ $rule ] = TRUE;
$qsid = $this->update_arrays(
'child' == $template ? 'c' : 'p',
$query,
$sel,
$rule,
$value,
$important,
NULL, // no rulevalid is passed when parsing from css (vs post input data)
empty( $resetrule[ $rule ] ) // if rule semaphore is TRUE, reset will be FALSE
);
$resetrule[ $rule ] = TRUE; // set rule semaphore so if same rule occurs again, it is not reset
endforeach;
endforeach;
endforeach;
endforeach;
// if this is a raw css update pass the last selector back to the browser to update the form
if ( $this->ctc()->cache_updates && $qsid ):
$this->ctc()->updates[] = array(
'obj' => 'qsid',
'key' => $qsid,
'data' => $this->obj_to_utf8( $this->denorm_sel_val( $qsid ) ),
);
do_action( 'chld_thm_cfg_update_qsid', $qsid );
endif;
}
// converts relative path to absolute path for preview
function convert_rel_url( $value, $relpath, $url = TRUE ) {
if ( preg_match( '/data:/', $value ) ) return $value;
$path = preg_replace( '%url\([\'" ]*(.+?)[\'" ]*\)%', "$1", $value );
if ( preg_match( '%(https?:)?//%', $path ) ) return $value;
$pathparts = explode( '/', $path );
$fileparts = explode( '/', $relpath );
$newparts = array();
while ( $pathpart = array_shift( $pathparts ) ):
if ( '..' == $pathpart )
array_pop( $fileparts );
else array_push( $newparts, sanitize_text_field( $pathpart ) );
endwhile;
$newvalue = ( $url ? 'url(' : '' )
. ( $fileparts ? trailingslashit( implode( '/', $fileparts ) ) : '' )
. implode( '/', $newparts ) . ( $url ? ')' : '' );
$this->ctc()->debug( 'converted ' . $value . ' to ' . $newvalue . ' with ' . $relpath, __FUNCTION__, __CLASS__ );
return $newvalue;
}
/**
* write_css
* converts normalized CSS object data into stylesheet.
* Preserves selector sequence and !important flags of parent stylesheet.
* @media query blocks are sorted using internal heuristics (see sort_queries)
* New selectors are appended to the end of each media query block.
* FIXME - this function has grown too monolithic - refactor and componentize
*/
function write_css() {
$output = '';
foreach ( $this->sort_queries() as $query => $sort_order ):
$has_selector = 0;
$sel_output = '';
$selectors = $this->denorm_dict_qs( $query, FALSE );
uasort( $selectors, array( $this, 'cmp_seq' ) );
if ( 'base' != $query ) $sel_output .= $query . ' {' . LF;
foreach ( $selectors as $selid => $qsid ):
if ( $valarr = $this->unpack_val_ndx( $qsid ) ):
$sel = $this->detokenize( $this->get_dict_value( 'sel', $selid ) );
$shorthand = array();
$rule_output = array();
foreach ( $valarr as $ruleid => $temparr ):
// normalize values for backward compatability
if ( isset( $temparr[ 'c' ] ) &&
( !isset( $temparr[ 'p' ] ) || $temparr[ 'p' ] != $temparr[ 'c' ] ) ):
foreach ( $temparr[ 'c' ] as $rulevalarr ):
$this->add_vendor_rules(
$rule_output,
$shorthand,
$this->get_dict_value( 'rule', $ruleid ),
$this->get_dict_value( 'val', $rulevalarr[ 0 ] ),
$rulevalarr[ 1 ],
$rulevalarr[ 2 ]
);
endforeach;
/**
* for testing
else:
foreach ( $temparr[ 'parnt' ] as $rulevalarr ):
$this->add_vendor_rules(
$rule_output,
$shorthand,
$rulearr[ $ruleid ],
$valarr[ $rulevalarr[ 0 ] ],
$rulevalarr[ 1 ],
$rulevalarr[ 2 ]
);
endforeach;
*/
endif;
endforeach;
/** FIXME ** need better way to sort rules and multiple values ***/
$this->encode_shorthand( $shorthand, $rule_output );
if ( count( $rule_output ) ):
// show load order -- removed in v.1.7.6 by popular demand
//$sel_output .= isset( $this->dict_seq[ $qsid ] )?'/*' . $this->dict_seq[ $qsid ] . '*/' . LF:'';
$sel_output .= $sel . ' {' . LF . $this->stringify_rule_output( $rule_output ) . '}' . LF;
$has_selector = 1;
endif;
endif;
endforeach;
if ( 'base' != $query ) $sel_output .= '}' . LF;
if ( $has_selector ) $output .= $sel_output;
endforeach;
$output = $this->get_css_header_comment( $this->get_prop( 'handling' ) ) . LF . $output;
$stylesheet = $this->get_stylesheet_path();
$this->ctc()->debug( 'writing stylesheet: ' . $stylesheet, __FUNCTION__, __CLASS__ );
//echo //print_r(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), true) . LF;
if ( $stylesheet_verified = $this->is_file_ok( $stylesheet, 'write' ) ):
global $wp_filesystem; // this was initialized earlier;
$mode = 'direct' == $this->ctc()->fs_method ? FALSE : 0666;
// write new stylesheet:
// try direct write first, then wp_filesystem write
// stylesheet must already exist and be writable by web server
if ( $this->ctc()->is_ajax ):
if ( is_writable( $stylesheet_verified ) ):
$this->ctc()->debug( 'Attempting Ajax write...', __FUNCTION__, __CLASS__ );
if ( FALSE === @file_put_contents( $stylesheet_verified, $output ) ):
$this->ctc()->debug( 'Ajax write failed.', __FUNCTION__, __CLASS__ );
return FALSE;
endif;
else:
$this->ctc()->debug( 'File not writable.', __FUNCTION__, __CLASS__ );
return FALSE;
endif;
elseif ( FALSE === $wp_filesystem->put_contents( $this->ctc()->fspath( $stylesheet_verified ), $output, $mode ) ):
$this->ctc()->debug( 'Filesystem write failed.', __FUNCTION__, __CLASS__ );
return FALSE;
endif;
$this->ctc()->debug( 'No write failure reported.', __FUNCTION__, __CLASS__ );
return TRUE;
endif;
$this->ctc()->debug( 'File NOT ok.', __FUNCTION__, __CLASS__ );
return FALSE;
}
function stringify_rule_output( &$rule_output ) {
$output = '';
asort( $rule_output );
foreach ( $rule_output as $rule => $sortstr )
$output .= ' ' . $rule . ";\n";
return $output;
}
function sortstr( $rule, $rulevalid ) {
return substr( "0000" . $this->get_dict_id( 'rule', $rule ), -4) . substr( "00" . $rulevalid, -2 );
}
/**
* encode_shorthand
* converts CTC long syntax into CSS shorthand
* v1.7.5 refactored for multiple values per property
* v2.1.0 to prevent incorrect rendering, do not use shorthand if multiple values exist for any side property.
*/
function encode_shorthand( $shorthand, &$rule_output ) {
foreach ( $shorthand as $property => $sides ):
if ( isset( $sides[ 'top' ] ) && 1 == count( $sides[ 'top' ] ) ):
foreach ( $sides[ 'top' ] as $tval => $tarr ):
if ( isset( $sides[ 'right' ] ) && 1 == count( $sides[ 'right' ] ) ):
$currseq = $tarr[ 1 ];
foreach ( $sides[ 'right' ] as $rval => $rarr ):
// value must exist from side and priority must match all sides
if ( isset( $sides[ 'bottom' ] ) && 1 == count( $sides[ 'bottom' ] ) && $tarr[ 0 ] == $rarr[ 0 ] ):
if ( $rarr[ 1 ] > $currseq ) $currseq = $rarr[ 1 ];
foreach ( $sides[ 'bottom' ] as $bval => $barr ):
if ( isset( $sides[ 'left' ] ) && 1 == count( $sides[ 'left' ] ) && $tarr[ 0 ] == $barr[ 0 ] ):
// use highest sort sequence of all sides
if ( $barr[ 1 ] > $currseq ) $currseq = $barr[ 1 ];
foreach ( $sides[ 'left' ] as $lval => $larr ):
if ( $tarr[ 0 ] != $larr[ 0 ] ) continue;
if ( $larr[ 1 ] > $currseq ) $currseq = $larr[ 1 ];
$combo = array(
$tval,
$rval,
$bval,
$lval,
);
// echo 'combo before: ' . print_r( $combo, TRUE ) . LF;
// remove from shorthand array
unset( $shorthand[ $property ][ 'top' ][ $tval ] );
unset( $shorthand[ $property ][ 'right' ][ $rval ] );
unset( $shorthand[ $property ][ 'bottom' ][ $bval ] );
unset( $shorthand[ $property ][ 'left' ][ $lval ] );
// combine into shorthand syntax
if ( $lval === $rval ):
//echo 'left same as right, popping left' . LF;
array_pop( $combo );
if ( $bval === $tval ):
//echo 'bottom same as top, popping bottom' . LF;
array_pop( $combo );
if ( $rval === $tval ): // && $bval === $tval ):
//echo 'right same as top, popping right' . LF;
array_pop( $combo );
endif;
endif;
endif;
//echo 'combo after: ' . print_r( $combo, TRUE ) . LF;
// set rule
$rule_output[ $property . ': ' . implode( ' ', $combo ) . ( $tarr[ 0 ] ? ' !important' : '' ) ] = $this->sortstr( $property, $currseq );
// reset sort sequence
$currseq = 0;
endforeach;
endif;
endforeach;
endif;
endforeach;
endif;
endforeach;
endif;
endforeach;
// add remaining rules
foreach ( $shorthand as $property => $sides ):
foreach ( $sides as $side => $values ):
$rule = $property . '-' . $side;
foreach ( $values as $val => $valarr ):
// set rule
$rule_output[ $rule . ': ' . $val . ( $valarr[ 0 ] ? ' !important' : '' ) ] = $this->sortstr( $rule, $valarr[ 1 ] );
endforeach;
endforeach;
endforeach;
}
/**
* add_vendor_rules
* Applies vendor prefixes to rules/values and separates out shorthand properties .
* These are based on commonly used practices and not all vendor prefixes are supported.
* TODO: verify this logic against vendor and W3C documentation
*/
function add_vendor_rules( &$rule_output, &$shorthand, $rule, $value, $important, $rulevalid ) {
if ( '' === trim( $value ) ) return;
if ( 'filter' == $rule && ( FALSE !== strpos( $value, 'progid:DXImageTransform.Microsoft.Gradient' ) ) ) return;
$importantstr = $important ? ' !important' : '';
if ( preg_match( "/^(margin|padding)\-(top|right|bottom|left)$/", $rule, $matches ) ):
$shorthand[ $matches[ 1 ] ][ $matches[ 2 ] ][ $value ] = array(
$important,
$rulevalid,
);
return;
elseif ( preg_match( '/^(' . implode( '|', $this->vendorrule ) . ')$/', $rule ) ):
foreach( array( 'moz', 'webkit', 'o' ) as $prefix ):
$rule_output[ '-' . $prefix . '-' . $rule . ': ' . $value . $importantstr ] = $this->sortstr( $rule, $rulevalid++ );
endforeach;
$rule_output[ $rule . ': ' . $value . $importantstr ] = $this->sortstr( $rule, $rulevalid );
elseif ( 'background-image' == $rule ):
// gradient?
if ( $gradient = $this->decode_gradient( $value ) ):
// standard gradient
foreach( array( 'moz', 'webkit', 'o', 'ms' ) as $prefix ):
// build key before dereferencing array - v.2.3.0.3
$propkey = 'background-image: -' . $prefix . '-' . 'linear-gradient(' . $gradient[ 'origin' ] . ', '
. $gradient[ 'color1' ] . ', ' . $gradient[ 'color2' ] . ')' . $importantstr;
$rule_output[ $propkey ] = $this->sortstr( $rule, $rulevalid++ );
endforeach;
// W3C standard gradient
// rotate origin 90 degrees
if ( preg_match( '/(\d+)deg/', $gradient[ 'origin' ], $matches ) ):
$org = ( 90 - $matches[ 1 ] ) . 'deg';
else:
foreach ( preg_split( "/\s+/", $gradient[ 'origin' ] ) as $dir ):
$dir = strtolower( $dir );
$dirs[] = ( 'top' == $dir ? 'bottom' :
( 'bottom' == $dir ? 'top' :
( 'left' == $dir ? 'right' :
( 'right' == $dir ? 'left' : $dir ) ) ) );
endforeach;
$org = 'to ' . implode( ' ', $dirs );
endif;
// build key before dereferencing array - v.2.3.0.3
$propkey = 'background-image: linear-gradient(' . $org . ', '
. $gradient[ 'color1' ] . ', ' . $gradient[ 'color2' ] . ')' . $importantstr;
$rule_output[ $propkey ] = $this->sortstr( $rule, $rulevalid );
// legacy webkit gradient - we'll add if there is demand
// '-webkit-gradient(linear,' .$origin . ', ' . $color1 . ', '. $color2 . ')';
/**
* MS filter gradient - DEPRECATED in v1.7.5
* $type = ( in_array( $gradient[ 'origin' ], array( 'left', 'right', '0deg', '180deg' ) ) ? 1 : 0 );
* $color1 = preg_replace( "/^#/", '#00', $gradient[ 'color1' ] );
* $rule_output[ 'filter: progid:DXImageTransform.Microsoft.Gradient(GradientType=' . $type . ', StartColorStr="'
* . strtoupper( $color1 ) . '", EndColorStr="' . strtoupper( $gradient[ 'color2' ] ) . '")'
* . $importantstr ] = $this->sortstr( $rule, $rulevalid );
*/
else:
// url or other value
$rule_output[ $rule . ': ' . $value . $importantstr ] = $this->sortstr( $rule, $rulevalid );
endif;
else:
$rule = preg_replace_callback( "/\d+/", array( $this, 'from_ascii' ), $rule );
$rule_output[ $rule . ': ' . $value . $importantstr ] = $this->sortstr( $rule, $rulevalid );
endif;
}
/**
* normalize_background
* parses background shorthand value and returns
* normalized rule/value pairs for each property
*/
function normalize_background( $value, &$rules, &$values ) {
if ( FALSE !== strpos( $value, 'gradient' ) ):
// only supporting linear syntax
if ( preg_match( '#(linear\-|Microsoft\.)#', $value ) ):
$values[] = $value;
$rules[] = 'background-image';
else:
// don't try to normalize non-linear gradients
$values[] = $value;
$rules[] = 'background';
endif;
else:
$regexes = array(
'image' => 'url *\\([^)]+?\\)|none',
'attachment' => 'scroll|fixed|local',
'clip' => '(padding|border|content)\\-box',
'repeat' => '(no\\-)?repeat(\\-(x|y))?|round|space',
'size' => 'cover|contain|auto',
'position' => 'top|bottom|left|right|center|\b0 +0\b|(\b0 +)?[\\-\\d.]+(px|%)( +0\b)?',
'color' => '\\#[a-fA-F0-9]{3,6}|(hsl|rgb)a? *\\([^)]+?\\)|[a-z]+'
);
//echo '<pre><code>' . "\n";
//echo '<strong>' . $value . '</strong>' . "\n";
foreach ( $regexes as $property => $regex ):
$this->temparray = array();
//echo $property . ': ' . $regex . "\n";
$value = preg_replace_callback( "/(" . $regex . ")/", array( $this, 'background_callback' ), $value );
if ( count( $this->temparray ) ):
$rules[] = 'background-' . $property;
$values[] = implode( ' ', $this->temparray );
//echo '<strong>result: ' . implode( ' ', $this->temparray ) . "</strong>\n";
endif;
endforeach;
//echo '</code></pre>' . "\n";
endif;
}
function background_callback( $matches ) {
$this->temparray[] = $matches[ 1 ];
}
/**
* normalize_font
* parses font shorthand value and returns
* normalized rule/value pairs for each property
*/
function normalize_font( $value, &$rules, &$values ) {
$regex = '#^((\d+|bold|normal) )?((italic|normal) )?(([\d\.]+(px|r?em|%))[\/ ])?(([\d\.]+(px|r?em|%)?) )?(.+)$#is';
preg_match( $regex, $value, $parts );
if ( !empty( $parts[ 2 ] ) ):
$rules[] = 'font-weight';
$values[] = $parts[ 2 ];
endif;
if ( !empty( $parts[ 4 ] ) ):
$rules[] = 'font-style';
$values[] = $parts[ 4 ];
endif;
if ( !empty( $parts[ 6 ] ) ):
$rules[] = 'font-size';
$values[] = $parts[ 6 ];
endif;
if ( !empty( $parts[ 9 ] ) ):
$rules[] = 'line-height';
$values[] = $parts[ 9 ];
endif;
if ( !empty( $parts[ 11 ] ) ):
$rules[] = 'font-family';
$values[] = $parts[ 11 ];
endif;
}
/**
* normalize_margin_padding
* parses margin or padding shorthand value and returns
* normalized rule/value pairs for each property
*/
function normalize_margin_padding( $rule, $value, &$rules, &$values ) {
$parts = preg_split( "/ +/", trim( $value ) );
if ( !isset( $parts[ 1 ] ) ) $parts[ 1 ] = $parts[ 0 ];
if ( !isset( $parts[ 2 ] ) ) $parts[ 2 ] = $parts[ 0 ];
if ( !isset( $parts[ 3 ] ) ) $parts[ 3 ] = $parts[ 1 ];
$rules[ 0 ] = $rule . '-top';
$values[ 0 ] = $parts[ 0 ];
$rules[ 1 ] = $rule . '-right';
$values[ 1 ] = $parts[ 1 ];
$rules[ 2 ] = $rule . '-bottom';
$values[ 2 ] = $parts[ 2 ];
$rules[ 3 ] = $rule . '-left';
$values[ 3 ] = $parts[ 3 ];
}
/**
* encode_gradient
* Normalize linear gradients from a bazillion formats into standard CTC syntax.
* This has been refactored in v1.7.5 to accommodate new spectrum color picker color "names."
* Currently only supports two-color linear gradients with no inner stops.
* TODO: legacy webkit? more gradients?
*/
function encode_gradient( $value ) {
// don't try this at home, kids
$regex = '/gradient[^\)]*?\( #exp descr
( #[1]
( #[2]
(to\x20)? #[3] reverse
(top|bottom|left|right)? #[4] direction1
(\x20 #[5]
(top|bottom|left|right))? #[6] direction2
|\d+deg),)? # or angle
(color-stop\()? #[7] optional
([^\w\#\)]*[\'"]? #[8]
(\#\w{3,8} #[9] color (hex)
|rgba?\([\d.,\x20]+?\) # red green blue (alpha)
|hsla?\([\d%.,\x20]+?\) # hue sat. lum. (alpha)
|[a-z]+) # color (name)
(\x20+[\d.]+%?)?) #[10] stop position
(\),\x20*)? #[11] optional close
(color-stop\()? #[12] optional
([^\w\#\)]*[\'"]? #[13]
(\#\w{3,8} #[14] color (hex)
|rgba?\([\d.,\x20]+?\) # red green blue (alpha)
|hsla?\([\d%.,\x20]+?\) # hue sat. lum. (alpha)
|[a-z]+) # color (name)
(\x20+[\d.]+%?)?) #[15] stop position
(\))? #[16] optional close
([^\w\)]*gradienttype=[\'"]? #[17] IE
(\d) #[18] IE
[\'"]?)? # IE
[^\w\)]*\)/ix';
$param = $parts = array();
preg_match( $regex, $value, $parts );
//$this->ctc()->debug( 'gradient value: ' . $value . ' parts: ' . print_r( $parts, TRUE ), __FUNCTION__, __CLASS__ );
if ( empty( $parts[ 18 ] ) ):
if ( empty( $parts[ 2 ] ) ):
$param[ 0 ] = 'top';
elseif ( 'to ' == $parts[ 3 ] ):
$param[ 0 ] = ( 'top' == $parts[ 4 ] ? 'bottom' :
( 'left' == $parts[ 4 ] ? 'right' :
( 'right' == $parts[ 4 ] ? 'left' :
'top' ) ) ) ;
else:
$param[ 0 ] = trim( $parts[ 2 ] );
endif;
if ( empty( $parts[ 10 ] ) ):
$param[ 2 ] = '0%';
else:
$param[ 2 ] = trim( $parts[ 10 ] );
endif;
if ( empty( $parts[ 15 ] ) ):
$param[ 4 ] = '100%';
else:
$param[ 4 ] = trim( $parts[ 15 ] );
endif;
elseif( '0' == $parts[ 18 ] ):
$param[ 0 ] = 'top';
$param[ 2 ] = '0%';
$param[ 4 ] = '100%';
elseif ( '1' == $parts[ 18 ] ):
$param[ 0 ] = 'left';
$param[ 2 ] = '0%';
$param[ 4 ] = '100%';
endif;
if ( isset( $parts[ 9 ] ) && isset( $parts[ 14 ] ) ):
$param[ 1 ] = $parts[ 9 ];
$param[ 3 ] = $parts[ 14 ];
ksort( $param );
return implode( ':', $param );
else:
return $value;
endif;
}
/**
* decode_border
* De-normalize CTC border syntax into individual properties.
*/
function decode_border( $value ) {
$parts = preg_split( '#\s+#', $value, 3 );
if ( 1 == count( $parts ) ):
$parts[ 0 ] = $value;
$parts[ 1 ] = $parts[ 2 ] = '';
endif;
return array(
'width' => empty( $parts[ 0 ] ) ? '' : $parts[ 0 ],
'style' => empty( $parts[ 1 ] ) ? '' : $parts[ 1 ],
'color' => empty( $parts[ 2 ] ) ? '' : $parts[ 2 ],
);
}
/**
* decode_gradient
* Decode CTC gradient syntax into individual properties.
*/
function decode_gradient( $value ) {
$parts = explode( ':', $value, 5 );
if ( !preg_match( '#(url|none)#i', $value ) && 5 == count( $parts ) ):
return array(
'origin' => empty( $parts[ 0 ] ) ? '' : $parts[ 0 ],
'color1' => empty( $parts[ 1 ] ) ? '' : $parts[ 1 ],
'stop1' => empty( $parts[ 2 ] ) ? '' : $parts[ 2 ],
'color2' => empty( $parts[ 3 ] ) ? '' : $parts[ 3 ],
'stop2' => empty( $parts[ 4 ] ) ? '' : $parts[ 4 ],
);
endif;
return FALSE;
}
/**
* denorm_rule_val
* Return array of unique values corresponding to specific rule
* FIXME: only return child if no original value exists
*/
function denorm_rule_val( $ruleid ) {
$this->load_config( 'dict_val' );
$this->load_config( 'val_ndx' );
$rule_sel_arr = array();
foreach ( $this->val_ndx as $qsid => $p ):
if ( $valarr = $this->unpack_val_ndx( $qsid ) ):
if ( !isset( $valarr[ $ruleid ] ) ) continue;
foreach ( array( 'p', 'c' ) as $template ):
if ( isset( $valarr[ $ruleid ][ $template ] ) ):
foreach ( $valarr[ $ruleid ][ $template ] as $rulevalarr ):
$rule_sel_arr[ $rulevalarr[ 0 ] ] = $this->get_dict_value( 'val', $rulevalarr[ 0 ] );
endforeach;
endif;
endforeach;
endif;
endforeach;
return $rule_sel_arr;
}
/**
* denorm_val_query
* Return array of queries, selectors, rules, and values corresponding to
* specific rule/value combo grouped by query, selector
* FIXME: only return new values corresponding to specific rulevalid of matching original value
*/
function denorm_val_query( $valid, $rule ) {
$this->load_config( 'dict_rule' );
$this->load_config( 'val_ndx' );
$value_query_arr = array();
if ( $thisruleid = $this->get_dict_id( 'rule', $rule ) ):
foreach ( $this->val_ndx as $qsid => $p ):
if ( $valarr = $this->unpack_val_ndx( $qsid ) ):
foreach ( $valarr as $ruleid => $values ):
if ( $ruleid != $thisruleid ) continue;
foreach ( array( 'p', 'c' ) as $template ):
if ( isset( $values[ $template ] ) ):
foreach ( $values[ $template ] as $rulevalarr ):
if ( $rulevalarr[ 0 ] != $valid ) continue;
$selarr = $this->denorm_query_sel( $qsid );
$value_query_arr[ $rule ][ $selarr[ 'query' ] ][ $qsid ] = $this->denorm_sel_val( $qsid );
endforeach;
endif;
endforeach;
endforeach;
endif;
endforeach;
endif;
return $value_query_arr;
}
/**
* denorm_query_sel
* Return id, query and selector values of a specific qsid (query-selector ID)
*/
function denorm_query_sel( $qsid ) {
$this->load_config( 'dict_query' );
$this->load_config( 'dict_sel' );
$this->load_config( 'dict_seq' );
$this->load_config( 'dict_qs' );
$this->load_config( 'dict_token' );
if ( FALSE === ( $qs = $this->get_dict_value( 'qs', $qsid ) ) ):
$this->ctc()->debug( $qsid . ' does not exist', __FUNCTION__, __CLASS__ );
return array();
endif;
list( $q, $s ) = explode( ':', $qs );
if ( $seq = $this->get_dict_value( 'seq', $qsid ) ):
$this->ctc()->debug( 'get seq: custom: ' . $seq, __FUNCTION__, __CLASS__ );
else:
$seq = $qsid;
$this->ctc()->debug( 'get seq: using qsid: ' . $qsid, __FUNCTION__, __CLASS__ );
endif;
$qselarr = array(
'id' => $qsid,
'query' => $this->get_dict_value( 'query', $q ),
'selector' => $this->detokenize( $this->get_dict_value( 'sel', $s ) ),
'seq' => $seq,
);
return $qselarr;
}
/**
* denorm_sel_val
* Return array of rules, and values matching specific qsid (query-selector ID)
* grouped by query, selector
*/
function denorm_sel_val( $qsid ) {
$this->load_config( 'dict_val' );
$this->load_config( 'dict_rule' );
$this->load_config( 'val_ndx' );
$selarr = $this->denorm_query_sel( $qsid );
if ( $valarr = $this->unpack_val_ndx( $qsid ) ):
//$this->ctc()->debug( 'valarr: ' . print_r( $valarr, TRUE ), __FUNCTION__, __CLASS__ );
foreach ( $valarr as $ruleid => $values ):
//$this->ctc()->debug( 'ruleid: ' . $ruleid, __FUNCTION__, __CLASS__ );
foreach ( array( 'p', 'c' ) as $template ):
$t = 'c' == $template ? 'child' : 'parnt';
//$this->ctc()->debug( 'template: ' . $t, __FUNCTION__, __CLASS__ );
if ( isset( $values[ $template ] ) ):
foreach ( $values[ $template ] as $rulevalarr ):
$selarr[ 'value' ][ $this->get_dict_value( 'rule', $ruleid ) ][ $t ][] = array(
$this->get_dict_value( 'val', $rulevalarr[ 0 ] ),
$rulevalarr[ 1 ],
isset( $rulevalarr[ 2 ] ) ? $rulevalarr[ 2 ] : 1,
);
endforeach;
endif;
endforeach;
endforeach;
endif;
//$this->ctc()->debug( print_r( $selarr, TRUE ), __FUNCTION__, __CLASS__ );
return $selarr;
}
/**
/**
* v1.7.5
* convert and/or normalize rule/value index
* to support multiple values per property ( rule )
* allows backward compatility with < v1.7.5
*/
function convert_ruleval_array( &$arr ) {
//$this->ctc()->debug( 'original array: ' . print_r( $arr, TRUE ), __FUNCTION__, __CLASS__ );
foreach ( array( 'parnt', 'child' ) as $template ):
// skip if empty array
if ( !isset( $arr[ $template ] ) ) continue;
$t = 'child' == $template ? 'c' : 'p';
// check if using original data structure ( value is scalar )
if ( ! is_array( $arr[ $template ] ) ):
/**
* create new array to replace old scalar value
* value structure is
* [0] => value
* [1] => important
* [2] => priority
*/
$arr[ $t ] = array( array( $arr[ $template ], $arr[ 'i_' . $template ], 0, 1 ) );
else:
$arr[ $t ] = $arr[ $template ];
endif;
unset( $arr[ $template ] );
endforeach;
//$this->ctc()->debug( 'first pass: ' . print_r( $arr, TRUE ), __FUNCTION__, __CLASS__ );
foreach ( array( 'p', 'c' ) as $template ):
if ( !isset( $arr[ $template ] ) ) continue;
$newarr = array();
// iterate each value and enforce array structure
foreach ( $arr[ $template ] as $rulevalid => $rulevalarr ):
// skip if empty array
if ( empty ( $rulevalarr ) ) continue;
//
if ( ! is_array( $rulevalarr ) ):
// important flag moves to individual value in array
$important = isset( $arr[ 'i_' . $template ] ) ? $arr[ 'i_' . $template ] : 0;
unset( $arr[ 'i_' . $template ] );
$val = (int) $rulevalarr;
$rulevalarr = array( $val, $important, $rulevalid );
elseif ( !isset( $rulevalarr[ 2 ] ) ):
$rulevalarr[ 2 ] = $rulevalid;
endif;
$newarr[] = $rulevalarr;
endforeach;
$arr[ $template ] = $newarr;
endforeach;
//$this->ctc()->debug( 'second pass: ' . print_r( $arr, TRUE ), __FUNCTION__, __CLASS__ );
}
/**
* Convert all internal data dictionaries to latest format.
*/
function convert_dict_arrays(){
$this->ctc()->debug( 'converting dictionaries from old format', __FUNCTION__, __CLASS__ );
foreach ( $this->dicts as $dict => $loaded ):
$this->load_config( $dict );
switch ( $dict ):
case 'dict_seq':
case 'dict_token':
break;
case 'sel_ndx':
$this->{ $dict } = array();
break;
case 'val_ndx':
foreach ( $this->val_ndx as $qsid => $rulearr ):
foreach ( $rulearr as $ruleid => $valarr )
$this->convert_ruleval_array( $this->val_ndx[ $qsid ][ $ruleid ] );
$this->pack_val_ndx( $qsid, $this->val_ndx[ $qsid ] );
endforeach;
break;
case 'dict_qs':
$qsarr = array();
foreach ( $this->dict_qs as $qsid => $arr ):
$qs = $arr[ 'q' ] . ':' . $arr[ 's' ];
$qsarr[ $qsid ] = $qs;
endforeach;
$this->dict_qs = $qsarr;
break;
default:
$this->{ $dict } = array_flip( $this->{ $dict } );
foreach ( $this->{ $dict } as $key => $val ):
if ( 'dict_sel' == $dict )
$this->dict_sel[ $key ] = $this->tokenize( (string) $val );
else
$this->{ $dict }[ $key ] = ( string ) $val;
endforeach;
endswitch;
//echo '<pre><code><small><strong>' . $dict . '</strong>' . print_r( $this->{ $dict }, TRUE) . '</small></code></pre>' . LF;
endforeach;
$this->save_config();
}
/**
* denorm_dict_qs
* Return denormalized array containing query and selector heirarchy
*/
function denorm_dict_qs( $query = NULL, $norm = TRUE ) {
$this->load_config( 'dict_query' );
$this->load_config( 'dict_sel' );
$this->load_config( 'dict_token' );
$this->load_config( 'dict_qs' );
$retarray = array();
if ( $query ):
$q = $this->get_dict_id( 'query', $query );
$selarr = preg_grep( '/^' . $q . ':/', $this->dict_qs );
foreach ( $selarr as $qsid => $qs ):
list( $q, $s ) = explode( ':', $qs );
if ( $norm )
$retarray[ $qsid ] = $this->detokenize( $this->get_dict_value( 'sel', $s ) );
else
$retarray[ $s ] = $qsid;
endforeach;
else:
return array_values( $this->dict_query );
endif;
if ( $norm )
return $this->sort_selectors( $retarray );
return $retarray;
}
/**
* is_important
* Strip important flag from value reference and return boolean
* Updating two values at once
*/
function is_important( &$value ) {
$important = 0;
$value = trim( str_ireplace( '!important', '', $value, $important ) );
return $important;
}
/**
* sort_queries
* De-normalize query data and return array sorted as follows:
* base
* @media max-width queries in descending order
* other @media queries in no particular order
* @media min-width queries in ascending order
*/
function sort_queries() {
$this->load_config( 'dict_query' );
$queries = array();
foreach ( $this->dict_query as $queryid => $query ):
/** lookup **/
if ( 'base' == $query ):
$queries[ 'base' ] = -999999;
continue;
endif;
if ( preg_match( "/((min|max)(\-device)?\-width)\s*:\s*(\d+)/", $query, $matches ) ):
$queries[ $query ] = 'min-width' == $matches[ 1 ] ? $matches[ 4 ] : -$matches[ 4 ];
else:
$queries[ $query ] = $queryid - 10000;
endif;
endforeach;
asort( $queries );
return $queries;
}
function sort_selectors( $selarr ) {
$selarr = ( array ) $selarr;
uasort( $selarr, array( $this, 'cmp_sel' ) );
return array_flip( $selarr );
}
function cmp_sel( $a, $b ) {
$cmpa = preg_replace( "/\W/", '', $a );
$cmpb = preg_replace( "/\W/", '', $b );
if ( $cmpa == $cmpb ) return 0;
return ( $cmpa < $cmpb ) ? -1 : 1;
}
// sort selectors based on dict_seq if exists, otherwise qsid
function cmp_seq( $a, $b ) {
if ( FALSE === ( $cmpa = $this->get_dict_value( 'seq', $a ) ) )
$cmpa = $a;
if ( FALSE === ( $cmpb = $this->get_dict_value( 'seq', $b ) ) )
$cmpb = $b;
if ( $cmpa == $cmpb ) return 0;
return ( $cmpa < $cmpb ) ? -1 : 1;
}
/**
* obj_to_utf8
* sets object data to UTF8
* flattens to array
* and stringifies NULLs
*/
function obj_to_utf8( $data ) {
if ( is_object( $data ) )
$data = get_object_vars( $data );
if ( is_array( $data ) )
return array_map( array( &$this, __FUNCTION__ ), $data );
else
return is_null( $data ) ? '' : utf8_encode( $data );
}
// convert ascii character into decimal value
function to_ascii( $matches ) {
return ord( $matches[ 0 ] );
}
// convert decimal value into ascii character
function from_ascii( $matches ) {
return chr( $matches[ 0 ] );
}
/**
* is_file_ok
* verify file exists and is in valid location
* must be in theme or plugin folders
*/
function is_file_ok( $stylesheet, $permission = 'read' ) {
// remove any ../ manipulations
$stylesheet = $this->ctc()->normalize_path( preg_replace( "%\.\./%", '/', $stylesheet ) );
//$this->ctc()->debug( 'checking file: ' . $stylesheet, __FUNCTION__, __CLASS__ );
if ( 'read' == $permission && !is_file( $stylesheet ) ):
$this->ctc()->debug( 'read ' . $stylesheet . ' no file!', __FUNCTION__, __CLASS__ );
return FALSE;
elseif ( 'write' == $permission && !is_dir( dirname( $stylesheet ) ) ):
$this->ctc()->debug( 'write ' . $stylesheet . ' no dir!', __FUNCTION__, __CLASS__ );
return FALSE;
elseif ( 'search' == $permission && !is_dir( $stylesheet ) ):
$this->ctc()->debug( 'search ' . $stylesheet . ' no dir!', __FUNCTION__, __CLASS__ );
return FALSE;
endif;
// check if in themes dir;
$regex = '%^' . preg_quote( $this->ctc()->normalize_path( get_theme_root() ) ) . '%';
//$this->ctc()->debug( 'theme regex: ' . $regex, __FUNCTION__, __CLASS__ );
if ( preg_match( $regex, $stylesheet ) ):
//$this->ctc()->debug( $stylesheet . ' ok!', __FUNCTION__, __CLASS__ );
return $stylesheet;
endif;
// check if in plugins dir
$regex = '%^' . preg_quote( $this->ctc()->normalize_path( WP_PLUGIN_DIR ) ) . '%';
//$this->ctc()->debug( 'plugin regex: ' . $regex, __FUNCTION__, __CLASS__ );
if ( preg_match( $regex, $stylesheet ) ):
//$this->ctc()->debug( $stylesheet . ' ok!', __FUNCTION__, __CLASS__ );
return $stylesheet;
endif;
$this->ctc()->debug( $stylesheet . ' is not in wp folders!', __FUNCTION__, __CLASS__ );
return FALSE;
}
/**
* normalize_color
* Sets hex string to lowercase and shortens to 3 char format if possible
*/
function normalize_color( $value ) {
$value = preg_replace_callback( "/#([0-9A-F]{3}([0-9A-F]{3})?)/i", array( $this, 'tolower' ), $value );
$value = preg_replace( "/#([0-9A-F])\\1([0-9A-F])\\2([0-9A-F])\\3/i", "#$1$2$3", $value );
return $value;
}
function normalize_query( $value ) {
// space after :
$value = str_replace( ':', ': ', trim( $value ) );
// remove multiple whitespace
$value = preg_replace( "/\s+/s", ' ', $value );
// remove space after (
$value = str_replace( '( ', '(', $value );
// remove space before )
$value = str_replace( ' )', ')', $value );
return $value;
}
// callback for normalize_color regex
function tolower( $matches ) {
return '#' . strtolower( $matches[ 1 ] );
}
function tokenize( $value ){
return $value;
// swap out commas and/or consecutive alphas with leading non-alpha if present
$value = preg_replace_callback( "/(, |[_\W]?[^\W_]+)/", array( $this, 'get_token' ), $value );
// trim off leading/trailing delimiter
$value = preg_replace( "/^%%|%%$/", '', $value );
// split into packable array
$array = array_map( array( $this, 'to_int' ), preg_split( "/(%%)+/", $value ) );
//echo '<pre><code><small>';
//var_dump( $array );
//echo '</small></code></pre>';
try {
return $this->packer->encode( $this->packer->pack( $array ) );
} catch ( Exception $e ) {
$this->ctc()->debug( 'Pack failed -- ' . $e->getMessage(), __FUNCTION__, __CLASS__ );
}
}
function to_int( $val ){
return intval( $val ) == $val ? (int) $val : $val;
}
function detokenize( $packed ){
return $packed;
// unpack array
try {
$this->packer->reset( $this->packer->decode( $packed ) );
$array = $this->packer->unpack();
} catch ( Exception $e ) {
$this->ctc()->debug( 'Unpack failed -- ' . $e->getMessage(), __FUNCTION__, __CLASS__ );
return FALSE;
}
$unpacked = array();
// iterate array and replace tokens
do {
$token = array_shift( $array );
if ( 'integer' == gettype( $token ) )
$unpacked[] = $this->get_dict_value( 'token', $token );
else
$unpacked[] = $token;
} while( $array );
// assemble array
return implode( '', $unpacked );
}
function get_token( $matches ){
$token = $matches[ 1 ];
$id = $this->get_dict_id( 'token', $token );
$this->instances[ $id ] = isset( $this->instances[ $id ] )
? $this->instances[ $id ] + 1
: 1;
return '%%' . $id . '%%';
}
}