less.module 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. <?php
  2. /**
  3. * @file
  4. * Handles compiling of .less files.
  5. *
  6. * The theme system allows for nearly all output of the Drupal system to be
  7. * customized by user themes.
  8. */
  9. define('LESS_PERMISSION', 'administer less');
  10. /**
  11. * Implements hook_menu().
  12. */
  13. function less_menu() {
  14. $items = array();
  15. $items['admin/config/development/less'] = array(
  16. 'title' => 'LESS settings',
  17. 'description' => 'Administer LESS settings',
  18. 'page callback' => 'drupal_get_form',
  19. 'page arguments' => array('less_settings'),
  20. 'access arguments' => array(LESS_PERMISSION),
  21. 'file' => 'less.admin.inc',
  22. 'type' => MENU_NORMAL_ITEM,
  23. );
  24. return $items;
  25. }
  26. /**
  27. * Implements hook_permission().
  28. */
  29. function less_permission() {
  30. return array(
  31. LESS_PERMISSION => array(
  32. 'title' => t('Administer LESS'),
  33. 'description' => t('Access the LESS settings page and view debug messages.'),
  34. ),
  35. );
  36. }
  37. /**
  38. * Builds the less cache
  39. */
  40. function _less_pre_render($styles) {
  41. $less_devel = variable_get('less_devel', FALSE);
  42. $less_dir = variable_get('less_dir', '');
  43. // Force regeneration LESS files if developer mode is enabled
  44. if ($less_devel || !$less_dir) {
  45. $less_dir = _less_new_dir();
  46. if ($less_devel && user_access(LESS_PERMISSION) && flood_is_allowed('less_devel_warning', 1)) {
  47. flood_register_event('less_devel_warning');
  48. drupal_set_message(t('LESS files are being regenerated on every request. Remember to <a href="!url">turn off</a> this feature on production websites.', array("!url" => url('admin/config/development/less'))), 'status');
  49. }
  50. }
  51. $less_path = 'public://less/' . $less_dir;
  52. foreach ($styles['#items'] as $key => $info) {
  53. $input_file = $info['data'];
  54. if (drupal_substr($input_file, -5) == '.less') {
  55. $file_uri = file_uri_target($input_file);
  56. $css_path = $less_path . '/' . dirname($file_uri ? $file_uri : $input_file);
  57. if (!is_dir($css_path) && !file_prepare_directory($css_path, FILE_CREATE_DIRECTORY)) {
  58. // There is a problem with the directory.
  59. $param = array('%dir' => $css_path);
  60. if (user_access(LESS_PERMISSION)) {
  61. drupal_set_message(t('LESS could not create a directory in %dir', $param), 'error');
  62. }
  63. watchdog('LESS', t('LESS could not create a directory in %dir', $param), array(), WATCHDOG_ERROR);
  64. return;
  65. }
  66. $output_file = $css_path . '/' . basename($input_file, '.less');
  67. // correct file names of files not following the .css.less naming convention
  68. if (drupal_substr($output_file, -4) != '.css') {
  69. $output_file .= '.css';
  70. }
  71. if (!is_file($output_file)) {
  72. if (_less_inc()) {
  73. $less = new lessc();
  74. $contents = drupal_load_stylesheet($input_file, FALSE);
  75. // Build the base URL of this CSS file: start with the full URL.
  76. $css_base_url = file_create_url($input_file);
  77. // Move to the parent.
  78. $css_base_url = substr($css_base_url, 0, strrpos($css_base_url, '/'));
  79. // Simplify to a relative URL if the stylesheet URL starts with the
  80. // base URL of the website.
  81. if (substr($css_base_url, 0, strlen($GLOBALS['base_root'])) == $GLOBALS['base_root']) {
  82. $css_base_url = substr($css_base_url, strlen($GLOBALS['base_root']));
  83. }
  84. _drupal_build_css_path(NULL, $css_base_url . '/');
  85. // Prefix all paths within this CSS file, ignoring external and absolute paths.
  86. $data = preg_replace_callback('/url\(\s*[\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\s*\)/i', '_drupal_build_css_path', $contents);
  87. try {
  88. $output_data = $less->parse($data);
  89. file_unmanaged_save_data($output_data, $output_file, FILE_EXISTS_REPLACE);
  90. }
  91. catch (Exception $e) {
  92. $message = 'LESS error: @message, %input_file';
  93. $message_vars = array('@message' => $e->getMessage(), '%input_file' => $input_file);
  94. watchdog('LESS', $message, $message_vars, WATCHDOG_ERROR);
  95. if (user_access(LESS_PERMISSION)) {
  96. drupal_set_message(t($message, $message_vars), 'error');
  97. }
  98. }
  99. }
  100. }
  101. if (is_file($output_file)) {
  102. $styles['#items'][$key]['data'] = $output_file;
  103. }
  104. }
  105. }
  106. return $styles;
  107. }
  108. /**
  109. * Implements hook_flush_caches().
  110. *
  111. * Flushes compiled LESS files during cache flush, except during cron.
  112. *
  113. * @return An array of cache table names.
  114. */
  115. function less_flush_caches() {
  116. if (!drupal_static('less_cron')) {
  117. _less_new_dir();
  118. }
  119. return array();
  120. }
  121. /**
  122. * Implements hook_cron_queue_info().
  123. */
  124. function less_cron_queue_info() {
  125. drupal_static('less_cron', TRUE);
  126. }
  127. /**
  128. * Helper function to create a new less dir.
  129. */
  130. function _less_new_dir() {
  131. $less_dir = uniqid('', TRUE);
  132. $less_path = 'public://less/' . $less_dir;
  133. file_prepare_directory($less_path, FILE_CREATE_DIRECTORY);
  134. variable_set('less_dir', $less_dir);
  135. return $less_dir;
  136. }
  137. /**
  138. * Implements hook_cron().
  139. */
  140. function less_cron() {
  141. $less_dir = variable_get('less_dir', '');
  142. $file_scan_options = array(
  143. 'nomask' => '/(\.\.?|CVS|' . preg_quote($less_dir) . ')$/', //adding current dir to excludes
  144. 'recurse' => FALSE,
  145. );
  146. $found_files = file_scan_directory('public://less', '/^.+$/', $file_scan_options);
  147. foreach ($found_files as $found_file) {
  148. file_unmanaged_delete_recursive($found_file->uri);
  149. }
  150. }
  151. function less_element_info_alter(&$type) {
  152. array_unshift($type['styles']['#pre_render'], '_less_pre_render');
  153. }
  154. /**
  155. * Finds and loads the lessphp library
  156. */
  157. function _less_inc() {
  158. static $loaded = NULL;
  159. if (!isset($loaded)) {
  160. // locations to check for lessphp, by order of preference
  161. $include_locations = array();
  162. // Composer created path
  163. $include_locations[] = dirname(__FILE__) . '/vendor/autoload.php';
  164. // load libraries module for during install
  165. module_load_include('module', 'libraries');
  166. if (function_exists('libraries_get_path')) {
  167. // add libraries supported path
  168. $include_locations[] = libraries_get_path('lessphp') . '/lessc.inc.php';
  169. }
  170. // add legacy path as final possible location
  171. $include_locations[] = dirname(__FILE__) . '/lessphp/lessc.inc.php';
  172. foreach ($include_locations as $include_location) {
  173. if (is_file($include_location)) {
  174. require_once $include_location;
  175. break;
  176. }
  177. }
  178. $loaded = class_exists('lessc', TRUE);
  179. }
  180. return $loaded;
  181. }