less.process.inc 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. <?php
  2. /**
  3. * @file
  4. * Contains functions related to compiling .less files.
  5. */
  6. /**
  7. * Attach LESS settings to each file as appropriate.
  8. *
  9. * @param array[] $item
  10. * @param string $key
  11. */
  12. function _less_attach_settings(&$item, $key) {
  13. $defaults = array(
  14. 'less' => less_get_settings(), // Bare defaults for LESS.
  15. );
  16. // These items must be reset for consistent operation.
  17. $nullify = array(
  18. 'less' => array(
  19. 'output_file' => NULL,
  20. 'build_required' => NULL,
  21. ),
  22. );
  23. // Merge in any info from $item.
  24. $item = array_replace_recursive($defaults, $item, $nullify);
  25. $item['less']['input_file'] = $item['data'];
  26. $less_settings = less_get_settings(_less_file_owner($item['less']['input_file']));
  27. // array_replace_recursive() works on keys, flip to not use numeric keys.
  28. $less_settings['paths'] = array_flip($less_settings['paths']);
  29. $item['less']['paths'] = array_flip($item['less']['paths']);
  30. // Merge defaults with any per file settings.
  31. $item['less'] = array_replace_recursive($less_settings, $item['less']);
  32. // First array_flips before merge removed duplicates, so just flip back.
  33. $item['less']['paths'] = array_flip($item['less']['paths']);
  34. }
  35. /**
  36. * Determine output filename and add it to the settings array.
  37. *
  38. * @param array[] $item
  39. * @param string $key
  40. */
  41. function _less_output_path(&$item, $key) {
  42. $input_file = $item['less']['input_file'];
  43. $less_settings = $item['less'];
  44. // array_multisort() the data so that the hash returns the same hash regardless order of data.
  45. array_multisort($less_settings);
  46. $output_path_array = array(
  47. '!less_output_dir' => LESS_DIRECTORY,
  48. // Strip '.css' extension of filenames following the RTL extension pattern.
  49. '!input_file_basename' => basename(basename($input_file, '.less'), '.css'),
  50. // drupal_json_encode() is used because serialize() throws an error with lambda functions.
  51. '!settings_hash' => drupal_hash_base64(drupal_json_encode($less_settings)),
  52. );
  53. $output_path = format_string('!less_output_dir/!input_file_basename.!settings_hash.css', $output_path_array);
  54. $item['less']['output_file'] = $output_path;
  55. }
  56. /**
  57. * Check if the file needs to be rebuilt based on changes to @import'ed files.
  58. *
  59. * @param array[] $item
  60. * @param string $key
  61. */
  62. function _less_check_build(&$item, $key) {
  63. $input_file = $item['less']['input_file'];
  64. $build_required = FALSE;
  65. // Set $rebuild if this file or its children have been modified.
  66. if ($less_file_cache = cache_get('less:devel:' . drupal_hash_base64($input_file))) {
  67. // Iterate over each file and check if there are any changes.
  68. foreach ($less_file_cache->data as $filepath => $filemtime) {
  69. // Only rebuild if there has been a change to a file.
  70. if (is_file($filepath) && filemtime($filepath) > $filemtime) {
  71. $build_required = TRUE;
  72. break;
  73. }
  74. }
  75. }
  76. else {
  77. // No cache data, force a rebuild for later comparison.
  78. $build_required = TRUE;
  79. }
  80. $item['less']['build_required'] = $build_required;
  81. }
  82. /**
  83. * Process a .less file and save the compiled styles.
  84. *
  85. * @param array[] $item
  86. * @param string $key
  87. *
  88. * @see \LessEngineInterface
  89. */
  90. function _less_process_file(&$item, $key) {
  91. $less_settings = $item['less'];
  92. // $output_file doesn't exist or is flagged for build.
  93. if (!is_file($item['less']['output_file']) || !empty($item['less']['build_required'])) {
  94. $output_data = NULL;
  95. try {
  96. $engine = less_get_engine($less_settings['input_file']);
  97. $engine->setImportDirectories($less_settings['paths']);
  98. if ($less_settings[LESS_DEVEL]) {
  99. $engine->setSourceMaps($less_settings[LESS_SOURCE_MAPS], DRUPAL_ROOT, base_path());
  100. }
  101. $engine->modifyVariables($less_settings['variables']);
  102. $output_data = $engine->compile();
  103. if ($less_settings[LESS_DEVEL]) {
  104. _less_cache_dependencies($less_settings['input_file'], $engine->getDependencies());
  105. }
  106. }
  107. catch (Exception $e) {
  108. $message_vars = array(
  109. '@message' => $e->getMessage(),
  110. '%input_file' => $item['less']['input_file'],
  111. );
  112. watchdog('LESS', 'LESS error: @message, %input_file', $message_vars, WATCHDOG_ERROR);
  113. if (user_access(LESS_PERMISSION)) {
  114. drupal_set_message(t('LESS error: @message, %input_file', $message_vars), 'error');
  115. }
  116. }
  117. if (isset($output_data)) {
  118. // Fix paths for images as .css is in different location.
  119. $output_data = _less_rewrite_paths($item['less']['input_file'], $output_data);
  120. // Ensure the destination directory exists.
  121. if (_less_ensure_directory(dirname($item['less']['output_file']))) {
  122. file_unmanaged_save_data($output_data, $item['less']['output_file'], FILE_EXISTS_REPLACE);
  123. }
  124. }
  125. if (is_file($item['less']['output_file']) && $item['less'][LESS_AUTOPREFIXER]) {
  126. if (($lessautoprefixer_library = libraries_load('lessautoprefixer')) && $lessautoprefixer_library['installed']) {
  127. try {
  128. LessAutoprefixer::create(drupal_realpath($item['less']['output_file']))->compile();
  129. }
  130. catch (Exception $e) {
  131. $message_vars = array(
  132. '@message' => $e->getMessage(),
  133. '%input_file' => $item['less']['output_file'],
  134. );
  135. watchdog('LESS', 'Autoprefixer error: @message, %input_file', $message_vars, WATCHDOG_ERROR);
  136. if (user_access(LESS_PERMISSION)) {
  137. drupal_set_message(t('Autoprefixer error: @message, %input_file', $message_vars), 'error');
  138. }
  139. }
  140. }
  141. }
  142. }
  143. if (is_file($item['less']['output_file'])) {
  144. // Set render path of the stylesheet to the compiled output.
  145. $item['data'] = $item['less']['output_file'];
  146. }
  147. }
  148. /**
  149. * @param array[] $item
  150. * @param string $key
  151. */
  152. function _less_store_cache_info(&$item, $key) {
  153. // Only match when output_file exists.
  154. if ($item['data'] === $item['less']['output_file']) {
  155. $less_watch_cache = $item;
  156. $less_watch_cache['data'] = $item['less']['input_file'];
  157. cache_set('less:watch:' . drupal_hash_base64(file_create_url($item['less']['output_file'])), $less_watch_cache);
  158. // 'preprocess' being FALSE generates a discreet <link /> rather than an @import.
  159. $item['preprocess'] = FALSE;
  160. }
  161. }
  162. /**
  163. * Normalize keeping track of changed files.
  164. *
  165. * @param string $input_file
  166. * Path of source file.
  167. * @param string[] $dependencies
  168. * Array of files that are @import'ed in $input_file, recursively.
  169. */
  170. function _less_cache_dependencies($input_file, $dependencies = array()) {
  171. // Add $input_file to $dependencies as it is not in return from some engines.
  172. $dependencies = array_merge(array($input_file), (array) $dependencies);
  173. $watched_files = array();
  174. foreach ($dependencies as $dependency) {
  175. // Full path on file should enforce uniqueness in associative array.
  176. $watched_files[drupal_realpath($dependency)] = filemtime($dependency);
  177. }
  178. cache_set('less:devel:' . drupal_hash_base64($input_file), $watched_files);
  179. }
  180. /**
  181. * Copied functionality from drupal_build_css_cache() for our own purposes.
  182. *
  183. * This function processes $contents and rewrites relative paths to be absolute
  184. * from web root. This is mainly used to ensure that compiled .less files still
  185. * reference images at their original paths.
  186. *
  187. * @param string $input_filepath
  188. * @param string $contents
  189. *
  190. * @return string
  191. * Processed styles with replaced paths.
  192. *
  193. * @see drupal_build_css_cache()
  194. */
  195. function _less_rewrite_paths($input_filepath, $contents) {
  196. $output = '';
  197. // Build the base URL of this CSS file: start with the full URL.
  198. $css_base_url = file_create_url($input_filepath);
  199. // Move to the parent.
  200. $css_base_url = substr($css_base_url, 0, strrpos($css_base_url, '/'));
  201. // Simplify to a relative URL if the stylesheet URL starts with the
  202. // base URL of the website.
  203. if (substr($css_base_url, 0, strlen($GLOBALS['base_root'])) == $GLOBALS['base_root']) {
  204. $css_base_url = substr($css_base_url, strlen($GLOBALS['base_root']));
  205. }
  206. _drupal_build_css_path(NULL, $css_base_url . '/');
  207. // Anchor all paths in the CSS with its base URL, ignoring external and absolute paths.
  208. $output .= preg_replace_callback('/url\(\s*[\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\s*\)/i', '_drupal_build_css_path', $contents);
  209. return $output;
  210. }