├── .gitmodules ├── CONTRIBUTORS.md ├── LICENSE.md ├── README.md └── peroxide.engine /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lessphp"] 2 | path = lessphp 3 | url = git@github.com:codeincarnate/lessphp.git 4 | [submodule "docs"] 5 | path = docs 6 | url = git@github.com:codeincarnate/peroxide.wiki.git 7 | [submodule "phamlp"] 8 | path = phamlp 9 | url = git@github.com:codeincarnate/phamlp.git 10 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | Contributors 2 | ============= 3 | 4 | Peroxide is the work of a growing group of people. 5 | Everyone below has contributed code, documentaiton, 6 | help or support to Peroxide. 7 | 8 | 9 | * Kyle Cunningham - Project Founder 10 | * Bryn Bellomy - Code and Bug Reporting 11 | * David Mignot - Initial D7 porting work 12 | 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This theme engine is made available under the terms of the GPLv2. 2 | For the full text of this license see http://www.gnu.org/licenses/gpl-2.0.html -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Peroxide 2 | ============= 3 | 4 | Peroxide is a Drupal theme engine allowing templates to be created using Haml 5 | and stylesheets to be written using Sass and Scss. 6 | 7 | Templates and styles for large sites can become quite long and complex. The goal of this 8 | project is to make more advanced tools available to developers so that the code for themes 9 | is shorter, easier to understand and more maintainable. 10 | 11 | Peroxide automatically compiles and caches templates as PHP. Sass and Scss files are also 12 | automatically compiled, cached and their resultant output css is automatically added to a page. 13 | 14 | Alongside of peroxide, a kickstarter theme is being developed to make getting started 15 | easier. It can be found at 16 | 17 | Installation 18 | ------------ 19 | 20 | To install peroxide you can use the following commands starting from the root 21 | of your Drupal installaiton: 22 | 23 | cd sites/all/themes (or whatever your theme directory is) 24 | mkdir engines 25 | cd engines 26 | git clone --recursive git://github.com/codeincarnate/peroxide.git 27 | 28 | _Note_: For peroxide to work properly your Drupal site's files directory must be writable by your webserver. 29 | 30 | Usage 31 | ------------ 32 | 33 | To use peroxide as the engine for your theme add this line to your 34 | themes info file. 35 | 36 | engine = peroxide 37 | 38 | Basic Theme 39 | ------------ 40 | 41 | A basic theme using the peroxide theme engine can be found at 42 | 43 | Future 44 | ------------ 45 | 46 | Drupal forms are ugly! Fortunately there are great creative commons licensed 47 | graphics out there to use. Ace (from Sproutcore) and Aristo (from Cappuccino) 48 | both look great. If someone would like to plug either of these in that would be awesome. 49 | See this [article](http://www.antipode.ca/2009/themes-sproutcore-vs-cappuccino/) for more 50 | information. 51 | 52 | License 53 | ------------ 54 | 55 | Peroxide is made available under the terms of the GPLv2. For the full 56 | text of this license see 57 | 58 | 59 | Credits 60 | ------------ 61 | 62 | Peroxide uses the phamlp library to parse HAML and Sass. This can be found at 63 | 64 | 65 | phamlp includes compass which can be found at 66 | -------------------------------------------------------------------------------- /peroxide.engine: -------------------------------------------------------------------------------- 1 | filename) . '/template.php'; 8 | if (file_exists($file)) { 9 | include_once "./$file"; 10 | } 11 | 12 | // Set Haml parser options 13 | if (!empty($theme->info['peroxide']['options']['haml'])) { 14 | _peroxide_set_haml_options($theme, $theme->info['peroxide']['options']['haml']); 15 | } 16 | else { 17 | _peroxide_set_haml_options($theme); 18 | } 19 | 20 | // Set Sass parser options 21 | if (!empty($theme->info['peroxide']['options']['sass'])) { 22 | _peroxide_set_sass_options($theme, $theme->info['peroxide']['options']['sass']); 23 | } 24 | else { 25 | _peroxide_set_sass_options($theme); 26 | } 27 | 28 | // Initialize parsers and render Sass 29 | _peroxide_init(); 30 | 31 | // If the theme implements hook_css_alter(), assume it calls peroxide_css_alter() 32 | if(!function_exists("{$theme->name}_css_alter")) { 33 | _peroxide_scan_sass($theme); 34 | } 35 | 36 | _peroxide_scan_less($theme); 37 | } 38 | 39 | /** 40 | * The extension for our templates 41 | */ 42 | function peroxide_extension() { 43 | return ".haml"; 44 | } 45 | 46 | /** 47 | * We're handling HAML template files 48 | */ 49 | function peroxide_theme($existing, $type, $theme, $path) { 50 | $templates = drupal_find_theme_functions($existing, array('haml', $theme)); 51 | $templates += drupal_find_theme_templates($existing, '.haml', $path); 52 | return $templates; 53 | } 54 | 55 | /** 56 | * Render a HAML template 57 | */ 58 | function peroxide_render_template($template, $vars) { 59 | // Attempt to make a logical cache structure, if that doesn't work then throw it out the window 60 | // Assuming directory is always set this should handle multiple themes well 61 | if (isset($vars['directory'])) { 62 | $cached_haml_path = file_default_scheme() . '://peroxide/haml_c/' . basename($vars['directory']); 63 | } 64 | else { 65 | $cached_haml_path = file_default_scheme() . '://peroxide/haml_c/'; 66 | } 67 | 68 | // Make sure that the directory we're placing css files in exists, if it doesn't exist attempt to create it 69 | _peroxide_check_directory($cached_haml_path); 70 | 71 | // Retrieve options for the Haml parser 72 | $options = _peroxide_get_haml_options(); 73 | 74 | // Extract Variables 75 | extract($vars, EXTR_SKIP); 76 | 77 | // Render the template 78 | ob_start(); 79 | $parser = new HamlParser($options); 80 | include $parser->parse($template, $cached_haml_path, 0755, '.haml', '.tpl.php'); 81 | $contents = ob_get_contents(); 82 | ob_end_clean(); 83 | 84 | // Return contents 85 | return $contents; 86 | } 87 | 88 | /** 89 | * Implements hook_css_alter. Call it from your theme's hook_css_alter. 90 | * 91 | * Processes and .sass/.scss files in the stylesheets[] array. 92 | */ 93 | function peroxide_css_alter(&$css) { 94 | $themes = list_themes(); 95 | $theme_info = $themes[$GLOBALS['theme']]; 96 | 97 | foreach($css as $fn => $info) { 98 | $pathinfo = pathinfo($fn); 99 | if($pathinfo['extension'] == 'scss' || $pathinfo['extension'] == 'sass') { 100 | $new_fn = _peroxide_process_sass($theme_info, $fn); 101 | unset($css[$fn]); 102 | if($new_fn) { 103 | $info['data'] = $new_fn; 104 | $info['preprocess'] = FALSE; 105 | $css[$new_fn] = $info; 106 | } 107 | } 108 | } 109 | } 110 | 111 | /** 112 | * Process a file from the stylesheets[] array. Called from peroxide_css_alter(). 113 | * The new file will be in the same directory, with the extension .sass-cache.css 114 | * 115 | * @param $theme Theme info object 116 | * @param $sass Filename of the sass/scss file (from stylesheets[]) 117 | * @returns The filename of the processed file 118 | */ 119 | function _peroxide_process_sass($theme, $sass) { 120 | $info = pathinfo($sass); 121 | 122 | // We put the new file in the same place because it's easier than reimplementing Drupal's url() rewriting 123 | $css_file = "{$info['dirname']}/{$info['filename']}.sass-cache.css"; 124 | 125 | if(filemtime($css_file) < filemtime($file)) { 126 | return $css_file; 127 | } 128 | 129 | // Try and build the file with the Ruby version 130 | @exec( 131 | 'sass '.($info['extension'] == 'scss' ? '--scss ' : ''). 132 | escapeshellarg($sass).' '.escapeshellarg($css_file), 133 | $op, $ret 134 | ); 135 | if($ret == 0) { 136 | return $css_file; 137 | } 138 | 139 | try { 140 | // Setup the Sass Parser 141 | $options = _peroxide_get_sass_options($theme); 142 | $parser = new SassParser($options); 143 | 144 | $css = $parser->parse($sass)->render(); 145 | } 146 | catch (SassException $e) { 147 | drupal_set_message($e->getMessage(), 'error'); 148 | drupal_set_message($e->getTraceAsString(), 'error'); 149 | } 150 | 151 | file_put_contents($css_file, $css); 152 | return $css_file; 153 | } 154 | 155 | 156 | /** 157 | * Initialize the Haml and Sass Parsers 158 | * 159 | */ 160 | function _peroxide_init() { 161 | $path = drupal_get_path('theme_engine', 'peroxide'); 162 | include_once $path . 'phamlp/haml/HamlParser.php'; 163 | 164 | // Test for Ruby version first 165 | @exec("sass -v", $op, $ret); 166 | if($ret != 0) { 167 | include_once $path . 'phamlp/sass/SassParser.php'; 168 | } 169 | 170 | include_once $path . 'lessphp/lessc.inc.php'; 171 | } 172 | 173 | /** 174 | * Scan for sass files, produce css files and then add them to the page 175 | */ 176 | function _peroxide_scan_sass($theme) { 177 | // Setup initial file paths 178 | $path = drupal_get_path('theme', $theme->name); 179 | $cached_css_path = file_default_scheme() . '://peroxide/css/' . $theme->name; 180 | 181 | // Make sure that the directory we're placing css files in exists, if it doesn't exist attempt to create it 182 | _peroxide_check_directory($cached_css_path); 183 | 184 | // Setup the Sass Parser 185 | $options = _peroxide_get_sass_options($theme); 186 | $parser = new SassParser($options); 187 | 188 | // Read information about sass files from the theme's info file 189 | if (!empty($theme->info['sass'])) { 190 | foreach ($theme->info['sass'] as $media => $sassy) { 191 | foreach ($sassy as $sass) { 192 | $sass_path = $path . '/' . $sass; 193 | $info = pathinfo($sass_path); 194 | $css_file = $cached_css_path . '/' . $info['filename'] . '.css'; 195 | 196 | try { 197 | $css = $parser->parse($sass_path)->render(); 198 | } 199 | catch (SassException $e) { 200 | drupal_set_message($e->getMessage(), 'error'); 201 | drupal_set_message($e->getTraceAsString(), 'error'); 202 | } 203 | 204 | // rewrite asset paths if the theme's settings specify to do so 205 | if ($theme->info['peroxide']['options']['sass']['rewrite asset paths'] == 'true' 206 | || (is_array($theme->info['peroxide']['options']['sass']['rewrite asset paths']) 207 | && in_array($sass, $theme->info['peroxide']['options']['sass']['rewrite asset paths']))) { 208 | 209 | $matches = array(); 210 | if (preg_match_all('/url\([\'"](?P.+[\'"])\)/', $css, $matches)) { 211 | foreach ($matches['url'] as $url) { 212 | $url = str_replace(array("'", '"'), '', $url); // remove single and double quotes which sometimes remain 213 | 214 | // for now, just pop off the last element since there's never a time you'd reference a directory and not a file inside a css url() 215 | $url = explode('/', $url); 216 | $filename = array_pop($url); 217 | $url = implode('/', $url); 218 | 219 | // calculate the new relative path 220 | $sass_dir = dirname($sass_path); // if sass[all][] = src/mysass.sass is specified, $sass_dir is "src" 221 | $new_url = peroxide_calculate_path_difference($sass_dir . '/' . $url, $cached_css_path) . "/$filename"; 222 | 223 | // do the actual replacement. we either have to do str_replace twice or preg_replace once. 224 | $css = str_replace("url('$url/$filename')", "url('$new_url')", $css); 225 | $css = str_replace("url(\"$url/$filename\")", "url(\"$new_url\")", $css); 226 | } 227 | } 228 | } 229 | 230 | // cache CSS in a file 231 | file_put_contents($css_file, $css); 232 | 233 | if ($media != 'compile-only') { 234 | drupal_add_css($css_file, 'theme', $media); 235 | } 236 | } 237 | } 238 | } 239 | } 240 | 241 | function _peroxide_scan_less($theme) { 242 | // Setup initial file paths 243 | $path = drupal_get_path('theme', $theme->name); 244 | $cached_css_path = file_default_scheme() . '://peroxide/css/' . $theme->name; 245 | 246 | // Read information about sass files from the theme's info file 247 | if (!empty($theme->info['less'])) { 248 | foreach ($theme->info['less'] as $media => $lessc) { 249 | foreach ($lessc as $less) { 250 | $less_path = $path . '/' . $less; 251 | $info = pathinfo($less_path); 252 | $css_file = $cached_css_path . '/' . $info['filename'] . ".css"; 253 | 254 | // Compile the less file 255 | try { 256 | lessc::ccompile($less_path, $css_file); 257 | } 258 | catch (Exception $e) { 259 | drupal_set_message($e->getMessage(), 'error'); 260 | } 261 | 262 | // Add to the page 263 | drupal_add_css($css_file, 'theme', $media); 264 | } 265 | } 266 | } 267 | } 268 | 269 | /** 270 | * Check to see if a directory exists, if it doesn't 271 | * attempt to recursively create the necessary directories. 272 | */ 273 | function _peroxide_check_directory($path) { 274 | $dir_exists = file_prepare_directory($path); 275 | if (!$dir_exists) { 276 | $result = mkdir($path, 0755, TRUE); 277 | if (!$result) { 278 | drupal_set_message('You must have your Drupal files directory correctly configured to use peroxide.', 'error'); 279 | return; 280 | } 281 | } 282 | else { 283 | $result = TRUE; 284 | } 285 | 286 | return $result; 287 | } 288 | 289 | 290 | /** 291 | * Set options for the Haml parser. 292 | */ 293 | function _peroxide_set_haml_options($theme = array(), $options = array()) { 294 | $set_options = &drupal_static(__FUNCTION__); 295 | 296 | // If no theme was passed in then return the options that have been set 297 | if (!empty($set_options)) { 298 | return $set_options; 299 | } 300 | 301 | // Merge options from theme's info file with the defaults 302 | $set_options = array_merge(_peroxide_default_haml_options(), $options); 303 | 304 | // Allow modules & running theme to alter Haml parser options 305 | peroxide_alter('haml_options', $set_options, $theme); 306 | 307 | return $set_options; 308 | } 309 | 310 | /** 311 | * Get options for the Haml parser 312 | */ 313 | function _peroxide_get_haml_options() { 314 | return _peroxide_set_haml_options(); 315 | } 316 | 317 | /** 318 | * Default options for the Haml parser. 319 | */ 320 | function _peroxide_default_haml_options() { 321 | $options = array( 322 | 'style' => 'nested', 323 | ); 324 | 325 | return $options; 326 | } 327 | 328 | 329 | /** 330 | * Set options for the sass parser 331 | */ 332 | function _peroxide_set_sass_options($theme = array(), $options = array()) { 333 | $set_options = drupal_static(__FUNCTION__); 334 | 335 | if(!empty($set_options)) { 336 | return $set_options; 337 | } 338 | 339 | // Merge options from theme's info file with the defaults 340 | $set_options = array_merge(_peroxide_default_sass_options($theme), $options); 341 | 342 | // Allow modules & running theme to alter options 343 | peroxide_alter('sass_options', $set_options, $theme); 344 | 345 | return $set_options; 346 | } 347 | 348 | /** 349 | * Retrieve options for the Sass parser. 350 | */ 351 | function _peroxide_get_sass_options($theme) { 352 | return _peroxide_set_sass_options($theme); 353 | } 354 | 355 | /** 356 | * Default options for the Sass parser. 357 | */ 358 | function _peroxide_default_sass_options() { 359 | $options = array( 360 | 'cache_location' => file_directory_temp(), 361 | 'css_location' => '/css', 362 | 'extensions' => array( 363 | 'compass' => array(), 364 | ), 365 | ); 366 | 367 | return $options; 368 | } 369 | 370 | /** 371 | * A function to allow alteration of underlying parser options by 372 | * the theme using peroxide at runtime. Also allows modules 373 | * to alter options as well. 374 | * 375 | * @param $hook 376 | * The name of the alteration hook (e.g. haml_options) 377 | * @param $theme 378 | * Information for the theme. 379 | * @param $options 380 | * The options for the underlying parser. 381 | */ 382 | function peroxide_alter($hook, &$options, $theme) { 383 | $hook = 'peroxide_' . $hook; 384 | 385 | // Allow modules to alter options 386 | drupal_alter($hook, $options, $theme); 387 | 388 | // Allow theme to alter options 389 | $fun = $theme->name . '_' . $hook . '_alter'; 390 | if (function_exists($fun)) { 391 | $fun($options, $theme); 392 | } 393 | } 394 | 395 | /** 396 | * Finds the relative path from $root to $dest. 397 | * 398 | * @param $dest 399 | * The destination. 400 | * @param $root 401 | * The starting point. 402 | * @param $dir_sep 403 | * The string used to separate directories. Defaults to '/'. 404 | */ 405 | function peroxide_calculate_path_difference($dest, $root = '', $dir_sep = '/') { 406 | $root = explode($dir_sep, $root); 407 | $dest = explode($dir_sep, $dest); 408 | $path = '.'; 409 | $fix = ''; 410 | $diff = 0; 411 | 412 | // calculate the relative path 413 | for ($i = -1; ++$i < max(($rC = count($root)), ($dC = count($dest))); ) { 414 | if (isset($root[$i]) and isset($dest[$i])) { 415 | if ($diff) { 416 | $path .= $dir_sep. '..'; 417 | $fix .= $dir_sep. $dest[$i]; 418 | continue; 419 | } 420 | if ($root[$i] != $dest[$i]) { 421 | $diff = 1; 422 | $path .= $dir_sep. '..'; 423 | $fix .= $dir_sep. $dest[$i]; 424 | continue; 425 | } 426 | } 427 | else if (!isset($root[$i]) and isset($dest[$i])) { 428 | for($j = $i-1; ++$j < $dC;) { 429 | $fix .= $dir_sep. $dest[$j]; 430 | } 431 | break; 432 | } 433 | else if (isset($root[$i]) and !isset($dest[$i])) { 434 | for($j = $i-1; ++$j < $rC;) { 435 | $fix = $dir_sep. '..'. $fix; 436 | } 437 | break; 438 | } 439 | } 440 | $path .= $fix; 441 | 442 | // clean the path of all unnecessary relative path components (".", "..") 443 | $path = preg_replace('~/\./~', '/', $path); // resolve "." 444 | 445 | // resolve ".." 446 | $parts = array(); 447 | foreach (explode('/', preg_replace('~/+~', '/', $path)) as $part) { 448 | if ($part == '..' && count($parts) > 0 && $parts[count($parts) - 1] != '..') { 449 | $x = array_pop($parts); 450 | } 451 | else if ($part != '' && $part != '.') { 452 | $parts[] = $part; 453 | } 454 | } 455 | return implode('/', $parts); 456 | } 457 | --------------------------------------------------------------------------------