566 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			566 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| /**
 | |
|  * @file An admin-only utility to test image styles and effects.
 | |
|  *
 | |
|  * It provides a page Test Suite in Administration > Configuration > Media >
 | |
|  * Image Styles (admin/config/media/image-styles/testsuite) that displays
 | |
|  * results for all existing image styles for all toolkits as well as for a set
 | |
|  * of test image styles defined in the various modules.
 | |
|  */
 | |
| 
 | |
| include_once('imagecache_testsuite.features.inc');
 | |
| 
 | |
| /**
 | |
|  * Implementation of hook_menu().
 | |
|  */
 | |
| function imagecache_testsuite_menu() {
 | |
|   $items = array();
 | |
|   $items['admin/config/media/image-styles/testsuite'] = array(
 | |
|     'title' => 'Test Suite',
 | |
|     'page callback' => 'imagecache_testsuite_page',
 | |
|     'access arguments' => array('administer image styles'),
 | |
|     'type' => MENU_LOCAL_TASK,
 | |
|     'weight' => 10,
 | |
|   );
 | |
|   $items['admin/config/media/image-styles/testsuite/%/%'] = array(
 | |
|     'title' => 'Test Suite Image',
 | |
|     'page callback' => 'imagecache_testsuite_generate',
 | |
|     'page arguments' => array(5, 6),
 | |
|     'access arguments' => array('administer image styles'),
 | |
|     'type' => MENU_CALLBACK,
 | |
|   );
 | |
|   $items['admin/config/media/image-styles/testsuite/positioning_test'] = array(
 | |
|     'title' => 'Positioning Test',
 | |
|     'page callback' => 'imagecache_testsuite_positioning',
 | |
|     'access arguments' => array('administer image styles'),
 | |
|     'type' => MENU_LOCAL_TASK,
 | |
|   );
 | |
|   return $items;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Implementation  of hook_help()
 | |
|  */
 | |
| function imagecache_testsuite_help($path /*, $arg*/) {
 | |
|   switch ($path) {
 | |
|     // @todo: this path does not exist anymore.
 | |
|     case 'admin/build/imagecache/test' :
 | |
|       $output = file_get_contents(drupal_get_path('module', 'imagecache_testsuite') . "/README.txt");
 | |
|       return _filter_autop($output);
 | |
|       break;
 | |
|     case 'admin/config/media/image-styles/testsuite' :
 | |
|       return t("<p>
 | |
|         This page displays a number of examples of image effects.
 | |
|         Illustrated are both the expected result and the actual result.
 | |
|         </p><p>
 | |
|         This page is just for debugging to confirm that this behavior doesn't
 | |
|         change as the code gets updated.
 | |
|         If the two illustrations do not match, there is probably something
 | |
|         that needs fixing.
 | |
|         </p><p>
 | |
|         More actions are provided by each of the imagecache actions submodules
 | |
|         and will be shown as you enable them.
 | |
|         </p>");
 | |
|       break;
 | |
|     case 'admin/config/media/image-styles' :
 | |
|       return t('
 | |
|         A number of styles here are provided by the Imagecache
 | |
|         Testsuite module as examples.
 | |
|         Disable this module to make them go away.
 | |
|       ');
 | |
|       break;
 | |
|   }
 | |
|   return '';
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Returns the test suite page.
 | |
|  *
 | |
|  * The test suite page contians img links to all image derivatives to create as
 | |
|  * part of the test suite.
 | |
|  *
 | |
|  * Samples to test are scanned from:
 | |
|  * - The existing image styles.
 | |
|  * - The file features.inc attached to this module. (@todo: no longer eisting?)
 | |
|  * - Individual *.imagecache_preset.inc files found near any known modules.
 | |
|  * Images illustrating the named preset are looked for also.
 | |
|  *
 | |
|  * Flushes the entire test cache every time anything is done.
 | |
|  *
 | |
|  * @return string
 | |
|  *   The html for the page.
 | |
|  */
 | |
| function imagecache_testsuite_page() {
 | |
|   module_load_include('inc', 'image', 'image.admin');
 | |
|   module_load_include('inc', 'image', 'image.effects');
 | |
| 
 | |
|   $tests = array_merge(image_styles(), imagecache_testsuite_get_tests());
 | |
|   $toolkits = image_get_available_toolkits();
 | |
| 
 | |
|   // Present the all-in-one overview page.
 | |
|   $sample_folders = imagecache_testsuite_get_folders();
 | |
| 
 | |
|   // Draw the admin table.
 | |
|   $test_table = array();
 | |
|   foreach ($tests as $style_name => $style) {
 | |
|     // Firstly, remove any previous images for the current style
 | |
|     image_style_flush($style);
 | |
| 
 | |
|     $row = array();
 | |
|     $row_class = 'test';
 | |
|     $details_list = array();
 | |
| 
 | |
|     // Render the details.
 | |
|     foreach ($style['effects'] as $effect) {
 | |
|       if (!isset($effect['name'])) {
 | |
|         // badness
 | |
|         watchdog('imagecache_testsuite', 'invalid testcase within %style_name. No effect name', array('%style_name' => $style_name), WATCHDOG_ERROR);
 | |
|         $details_list[] = '<div>Unidentified effect</div>';
 | |
|         $row_class = 'error';
 | |
|         continue;
 | |
|       }
 | |
|       $effect_definition = image_effect_definition_load($effect['name']);
 | |
|       if (function_exists($effect_definition['effect callback'])) {
 | |
|         $description = "<strong>{$effect_definition['label']}</strong> ";
 | |
|         $description .= isset($effect_definition['summary theme']) ? theme($effect_definition['summary theme'], array('data' => $effect['data'])) : '';
 | |
|         $details_list[] = "<div>$description</div>";
 | |
|       }
 | |
|       else {
 | |
|         // Probably an action that requires a module that is not installed.
 | |
|         $strings = array(
 | |
|           '%action' => $effect['name'],
 | |
|           '%module' => $effect['module'],
 | |
|         );
 | |
|         $details_list[$effect['name']] = t("<div><b>%action unavailable</b>. Please enable %module module.</div>", $strings);
 | |
|         $row_class = 'error';
 | |
|       }
 | |
|     }
 | |
|     $row['details'] = "<h3>{$style['name']}</h3><p>" . implode($details_list) . "</p>";
 | |
| 
 | |
|     // Look for a sample image. May also be defined by the definition itself,
 | |
|     // but normally assume a file named after the image style, in (one of the)
 | |
|     // directories with test styles.
 | |
|     foreach ($sample_folders as $sample_folder) {
 | |
|       if (file_exists("{$sample_folder}/{$style_name}.png")) {
 | |
|         $style['sample'] = "{$sample_folder}/{$style_name}.png";
 | |
|       }
 | |
|       elseif (file_exists("{$sample_folder}/{$style_name}.jpg")) {
 | |
|         $style['sample'] = "{$sample_folder}/{$style_name}.jpg";
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (isset($style['sample']) && file_exists($style['sample'])) {
 | |
|       $sample_img = theme('image', array('path' => $style['sample']));
 | |
|       // I was having trouble with permissions on an OSX dev machine.
 | |
|       if (!is_readable($style['sample'])) {
 | |
|         $sample_img = "FILE UNREADABLE: {$style['sample']}";
 | |
|       }
 | |
|     }
 | |
|     else {
 | |
|       $sample_img = "[no sample]";
 | |
|     }
 | |
|     $row['sample'] = $sample_img;
 | |
| 
 | |
|     // Generate a result for each available toolkit.
 | |
|     foreach ($toolkits as $toolkit => $toolkit_info) {
 | |
|       $test_url = "admin/config/media/image-styles/testsuite/$style_name/$toolkit";
 | |
|       $test_img = theme('image', array(
 | |
|         'path' => $test_url,
 | |
|         'alt' => "$style_name/$toolkit"
 | |
|       ));
 | |
|       $row[$toolkit] = l($test_img, $test_url, array('html' => TRUE));
 | |
|     }
 | |
|     $test_table[$style_name] = array(
 | |
|       'data' => $row,
 | |
|       'class' => array($row_class)
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   $header = array_merge(array('test', 'sample'), array_keys($toolkits));
 | |
|   $output = theme('table', array(
 | |
|     'header' => $header,
 | |
|     'rows' => $test_table,
 | |
|     'id' => 'imagecache-testsuite'
 | |
|   ));
 | |
| 
 | |
|   // @todo: zebra striping can be disabled in D7.
 | |
|   // Default system zebra-striping fails to show my transparency on white.
 | |
|   drupal_add_html_head('<style  type="text/css" >#imagecache-testsuite tr.even{background-color:#EEEEEE !important;} #imagecache-testsuite td{vertical-align:top;}  #imagecache-testsuite tr.error{background-color:#FFCCCC !important;}</style>');
 | |
| 
 | |
|   return $output;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Returns the requested image derivative.
 | |
|  *
 | |
|  * If the image derivative generation is successful, the function does not
 | |
|  * return but exits processing using drupal_exit().
 | |
|  *
 | |
|  * Flushes the entire test cache every time anything is done.
 | |
|  *
 | |
|  * @param string $test_id
 | |
|  *   The id of the test to generate the derivative for.
 | |
|  * @param string $toolkit
 | |
|  *   The toolkit to use, or empty for the default toolkit
 | |
|  *
 | |
|  * @return string|bool
 | |
|  *   - The html for the page ($test_id is empty)
 | |
|  *   - False when the image derivative could not be created.
 | |
|  */
 | |
| function imagecache_testsuite_generate($test_id = '', $toolkit = '') {
 | |
|   module_load_include('inc', 'image', 'image.admin');
 | |
|   module_load_include('inc', 'image', 'image.effects');
 | |
| 
 | |
|   if (empty($toolkit)) {
 | |
|     $toolkit = image_get_toolkit();
 | |
|   }
 | |
|   else {
 | |
|     // Set the toolkit for this invocation only, so do not use variable_set.
 | |
|     global $conf;
 | |
|     $conf['image_toolkit'] = $toolkit;
 | |
|     if ($toolkit === 'gd') {
 | |
|       // This seems not to be done automatically elsewhere.
 | |
|       include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'system') . '/' . 'image.gd.inc';
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   $target = 'module://imagecache_testsuite/sample.jpg';
 | |
|   $tests = array_merge(image_styles(), imagecache_testsuite_get_tests());
 | |
| 
 | |
|   // Run the process and return the image, @see image_style_create_derivative().
 | |
|   $style = $tests[$test_id];
 | |
|   if (!$style) {
 | |
|     trigger_error("Unknown test style preset '$test_id' ", E_USER_ERROR);
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   // @todo: should we let the image style system do its work and just interfere on hook_init with setting the toolkit?
 | |
|   // @todo: this would make the page generator easier as well and keep it working with secure image derivatives.
 | |
|   // Start emulating image_style_create_derivative()
 | |
|   // The main difference being I determine the toolkit I want to use.
 | |
|   // SOME of this code is probably redundant, was a lot of copy&paste without true understanding of the new image.module
 | |
|   if (!$image = image_load($target, $toolkit)) {
 | |
|     trigger_error("Failed to open original image $target with toolkit $toolkit", E_USER_ERROR);
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   // Need to save the result before returning it - to stay compatible with imagemagick
 | |
|   $filename = "$test_id-$toolkit.{$image->info['extension']}";
 | |
|   $derivative_uri = image_style_path($style['name'], $filename);
 | |
|   $directory = dirname($derivative_uri);
 | |
|   file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
 | |
|   watchdog('imagecache_testsuite', 'Checking a save dir %dir', array('%dir' => dirname($derivative_uri)), WATCHDOG_DEBUG);
 | |
| 
 | |
|   // Imagemagick is not quite right? place a file where the file is supposed to go
 | |
|   // before I put the real path there? else drupal_realpath() says nuh.
 | |
|   #file_save_data('touch this for imagemagick', $derivative_uri, FILE_EXISTS_REPLACE);
 | |
| 
 | |
|   foreach ($style['effects'] as $effect) {
 | |
|     // Need to load the full effect definitions, our test ones don't know all the callback info
 | |
|     $effect_definition = image_effect_definition_load($effect['name']);
 | |
|     if (empty($effect_definition)) {
 | |
|       watchdog('imagecache_testsuite', 'I have no idea what %name is', array('%name' => $effect['name']), WATCHDOG_ERROR);
 | |
|       continue;
 | |
|     }
 | |
|     $full_effect = array_merge($effect_definition, array('data' => $effect['data']));
 | |
| 
 | |
|     // @todo: effects that involve other images (overlay, underlay) will load that image with the default toolkit which may differ from the toolkit tested here.
 | |
|     if (!image_effect_apply($image, $full_effect)) {
 | |
|       watchdog('imagecache_testsuite', 'action: %action (%callback) failed for %src', array(
 | |
|         '%action' => $full_effect['label'],
 | |
|         '%src' => $target,
 | |
|         '%callback' => $full_effect['effect callback']
 | |
|       ), WATCHDOG_ERROR);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (!image_save($image, $derivative_uri)) {
 | |
|     watchdog('imagecache_testsuite', 'saving image %label failed for %derivative_uri', array(
 | |
|       '%derivative_uri' => $derivative_uri,
 | |
|       '%label' => isset($style['label']) ? $style['label'] : $style['name']
 | |
|     ), WATCHDOG_ERROR);
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   if ($result_image = image_load($derivative_uri)) {
 | |
|     file_transfer($result_image->source, array(
 | |
|       'Content-Type' => $result_image->info['mime_type'],
 | |
|       'Content-Length' => $result_image->info['file_size']
 | |
|     ));
 | |
|     drupal_exit();
 | |
|   }
 | |
|   return "Failed to load the expected result from $derivative_uri";
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Implements hook_image_default_styles().
 | |
|  *
 | |
|  * Lists all our individual test cases and makes them available
 | |
|  * as enabled styles
 | |
|  */
 | |
| function imagecache_testsuite_image_default_styles() {
 | |
|   $styles = imagecache_testsuite_get_tests();
 | |
| 
 | |
|   // Need to filter out the invalid test cases
 | |
|   // (ones that use unavailable actions)
 | |
|   // or the core complains with notices.
 | |
| //  foreach ($styles as $id => $style) {
 | |
| //    foreach ($style['effects'] as $delta => $action) {
 | |
| //      if (!empty($action['module']) && ($action['module'] != 'imagecache') && !module_exists($action['module'])) {
 | |
| //        unset($styles[$id]);
 | |
| //        break;
 | |
| //      }
 | |
| //    }
 | |
| //  }
 | |
|   return $styles;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Retrieve the list of presets, each of which contain actions and action
 | |
|  * definitions.
 | |
|  *
 | |
|  * Scans all the module folders for files named *.imagecache_preset.inc
 | |
|  *
 | |
|  * It seems that the required shape in D7 is
 | |
|  * $style=>array(
 | |
|  *   'effects' => array(
 | |
|  *     0 => array('name' => 'something', 'data' => array())
 | |
|  *   )
 | |
|  * )
 | |
|  */
 | |
| function imagecache_testsuite_get_tests() {
 | |
|   $presets = array();
 | |
|   $folders = imagecache_testsuite_get_folders();
 | |
|   foreach ($folders as $folder) {
 | |
|     $preset_files = file_scan_directory($folder, "/.*.imagecache_preset.inc/");
 | |
|     // Setting filepath in this scope allows the tests to know where they are.
 | |
|     // The inc files may use it to create their rules.
 | |
|     $filepath = $folder;
 | |
|     foreach ($preset_files as $preset_file) {
 | |
|       include_once($preset_file->uri);
 | |
|     }
 | |
|   }
 | |
|   uasort($presets, 'element_sort');
 | |
|   return $presets;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Places to scan for test presets and sample images.
 | |
|  *
 | |
|  * @return array
 | |
|  *   an array of folder names of everything that implements imagecache_actions.
 | |
|  */
 | |
| function imagecache_testsuite_get_folders() {
 | |
|   $folders = array(drupal_get_path('module', 'imagecache_testsuite'));
 | |
|   foreach (module_implements('image_effect_info') as $module_name) {
 | |
|     $folders[] = drupal_get_path('module', $module_name) . '/tests';
 | |
|   }
 | |
|   return $folders;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Display a page demonstrating a number of positioning tests
 | |
|  *
 | |
|  * Tests both styles of positioning - the x=, y= original, used in most places,
 | |
|  * pls the css-like left=, top= version also.
 | |
|  */
 | |
| function imagecache_testsuite_positioning() {
 | |
|   module_load_include('inc', 'imagecache_actions', 'utility');
 | |
|   drupal_set_title("Testing the positioning algorithm");
 | |
|   $tests = imagecache_testsuite_positioning_get_tests();
 | |
|   $table = array();
 | |
|   // $dst_image represents tha field or canvas.
 | |
|   // $src_image is the item being placed on it.
 | |
|   // Both these represent an imageapi-type image resource handle, but contain just dimensions
 | |
|   $src_image = new stdClass();
 | |
|   $src_image->info = array('width' => 75, 'height' => 100);
 | |
|   $dst_image = new stdClass();
 | |
|   $dst_image->info = array('width' => 200, 'height' => 150);
 | |
| 
 | |
|   foreach ($tests as $testname => $test) {
 | |
|     // calc it, using either old or new method
 | |
|     if (isset($test['parameters']['x']) || isset($test['parameters']['y'])) {
 | |
|       $result['x'] = imagecache_actions_keyword_filter($test['parameters']['x'], $dst_image->info['width'], $src_image->info['width']);
 | |
|       $result['y'] = imagecache_actions_keyword_filter($test['parameters']['y'], $dst_image->info['height'], $src_image->info['height']);
 | |
|     }
 | |
|     else {
 | |
|       // use css style
 | |
|       $result = imagecache_actions_calculate_relative_position($dst_image, $src_image, $test['parameters']);
 | |
|     }
 | |
|     $expected_illustration = theme_positioning_test($test['expected']['x'], $test['expected']['y']);
 | |
|     $result_illustration = theme_positioning_test($result['x'], $result['y']);
 | |
|     $row = array();
 | |
|     $row['name'] = array('data' => '<h3>' . $testname . '</h3>' . $test['description']);
 | |
|     $row['parameters'] = theme_positioning_parameters($test['parameters']);
 | |
|     $row['expected'] = theme_positioning_parameters($test['expected']);
 | |
|     $row['expected_image'] = $expected_illustration;
 | |
|     $row['result'] = theme_positioning_parameters($result);
 | |
|     $row['result_image'] = $result_illustration;
 | |
|     $table[] = $row;
 | |
|   }
 | |
|   return 'Result of test:' . theme('table', array(
 | |
|     'test',
 | |
|     'parameters',
 | |
|     'expected',
 | |
|     'image',
 | |
|     'result',
 | |
|     'actual image',
 | |
|     'status'
 | |
|   ), $table);
 | |
| 
 | |
| }
 | |
| 
 | |
| function theme_positioning_test($x, $y) {
 | |
|   $inner = "<div style='background-color:red; width:75px; height:100px; position:absolute; left:{$x}px; top:{$y}px'>";
 | |
|   $outer = "<div style='background-color:blue; width:200px; height:150px; position:absolute; left:25px; top:25px'><div style='position:relative'>$inner</div></div>";
 | |
|   $wrapper = "<div style='background-color:#CCCCCC; width:250px; height:200px; position:relative'>$outer</div>";
 | |
|   return $wrapper;
 | |
| }
 | |
| 
 | |
| function theme_positioning_parameters($parameters) {
 | |
|   $outputs = array();
 | |
|   foreach ($parameters as $key => $value) {
 | |
|     $outputs[] = "[$key] => $value";
 | |
|   }
 | |
|   return '<pre>' . join("\n", $outputs) . '</pre>';
 | |
| }
 | |
| 
 | |
| function imagecache_testsuite_positioning_get_tests() {
 | |
|   return array(
 | |
|     'base' => array(
 | |
|       'parameters' => array(
 | |
|         'x' => '0',
 | |
|         'y' => '0',
 | |
|       ),
 | |
|       'description' => '0 is top left.',
 | |
|       'expected' => array(
 | |
|         'x' => '0',
 | |
|         'y' => '0',
 | |
|       ),
 | |
|     ),
 | |
|     'numbers' => array(
 | |
|       'parameters' => array(
 | |
|         'x' => '50',
 | |
|         'y' => '-50',
 | |
|       ),
 | |
|       'description' => 'Basic numbers indicate distance and direction from top left.',
 | |
|       'expected' => array(
 | |
|         'x' => '50',
 | |
|         'y' => '-50',
 | |
|       ),
 | |
|     ),
 | |
|     'keywords' => array(
 | |
|       'parameters' => array(
 | |
|         'x' => 'center',
 | |
|         'y' => 'bottom',
 | |
|       ),
 | |
|       'description' => "Plain keywords will align against the region",
 | |
|       'expected' => array(
 | |
|         'x' => '62.5',
 | |
|         'y' => '50',
 | |
|       ),
 | |
|     ),
 | |
|     'keyword with offsets' => array(
 | |
|       'parameters' => array(
 | |
|         'x' => 'right+10',
 | |
|         'y' => 'bottom+10',
 | |
|       ),
 | |
|       'description' => "Keywords can be used with offsets. Positive numbers are in from the named side",
 | |
|       'expected' => array(
 | |
|         'x' => '115',
 | |
|         'y' => '40',
 | |
|       ),
 | |
|     ),
 | |
|     'keyword with negative offsets' => array(
 | |
|       'parameters' => array(
 | |
|         'x' => 'right-10',
 | |
|         'y' => 'bottom-10',
 | |
|       ),
 | |
|       'description' => "Negative numbers place the item outside the boundry",
 | |
|       'expected' => array(
 | |
|         'x' => '135',
 | |
|         'y' => '60',
 | |
|       ),
 | |
|     ),
 | |
|     'percent' => array(
 | |
|       'parameters' => array(
 | |
|         'x' => '50%',
 | |
|         'y' => '50%',
 | |
|       ),
 | |
|       'description' => "Percentages on their own will CENTER on both the source and destination items",
 | |
|       'expected' => array(
 | |
|         'x' => '62.5',
 | |
|         'y' => '25',
 | |
|       ),
 | |
|     ),
 | |
|     'keyword with percent' => array(
 | |
|       'parameters' => array(
 | |
|         'x' => 'right+10%',
 | |
|         'y' => 'bottom+10%',
 | |
|       ),
 | |
|       'description' => "Percentages can be used with keywords, though the placed image will be centered on the calculated position.",
 | |
|       'expected' => array(
 | |
|         'x' => '142.5',
 | |
|         'y' => '85',
 | |
|       ),
 | |
|     ),
 | |
|     'css styles' => array(
 | |
|       'parameters' => array(
 | |
|         'left' => '10px',
 | |
|         'bottom' => '10px',
 | |
|       ),
 | |
|       'description' => "A different method uses css-like parameters.",
 | |
|       'expected' => array(
 | |
|         'x' => '10',
 | |
|         'y' => '40',
 | |
|       ),
 | |
|     ),
 | |
|     'css negatives' => array(
 | |
|       'parameters' => array(
 | |
|         'left' => '-10px',
 | |
|         'bottom' => '-10px',
 | |
|       ),
 | |
|       'description' => "Negative numbers from sides always move outside of the boundries.",
 | |
|       'expected' => array(
 | |
|         'x' => '-10',
 | |
|         'y' => '60',
 | |
|       ),
 | |
|     ),
 | |
|     'css with percents' => array(
 | |
|       'parameters' => array(
 | |
|         'right' => '+10%',
 | |
|         'bottom' => '+10%',
 | |
|       ),
 | |
|       'description' => "Using percents with sides calculates the percent location on the base, then centers the source item on that point.",
 | |
|       'expected' => array(
 | |
|         'x' => '142.5',
 | |
|         'y' => '85',
 | |
|       ),
 | |
|     ),
 | |
|     'css centering' => array(
 | |
|       'parameters' => array(
 | |
|         'right' => '50%',
 | |
|         'top' => '50%',
 | |
|       ),
 | |
|       'description' => "The auto-centering that happens when percents are used means you can easily center things at 50%.",
 | |
|       'expected' => array(
 | |
|         'x' => '62.5',
 | |
|         'y' => '25',
 | |
|       ),
 | |
|     ),
 | |
|     'css positioning' => array(
 | |
|       'parameters' => array(
 | |
|         'right' => 'left+20',
 | |
|         'top' => 'bottom-20',
 | |
|       ),
 | |
|       'description' => "It's also possible to use keywords there, though it's not smart to do so",
 | |
|       'expected' => array(
 | |
|         'x' => '-55',
 | |
|         'y' => '130',
 | |
|       ),
 | |
|     ),
 | |
| 
 | |
|   );
 | |
| }
 | 
