PageRenderTime 68ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/src/applications/celerity/CelerityResourceTransformer.php

http://github.com/facebook/phabricator
PHP | 268 lines | 197 code | 43 blank | 28 comment | 20 complexity | d9f537f1f7698212951d55cd4be20922 MD5 | raw file
Possible License(s): JSON, MPL-2.0-no-copyleft-exception, Apache-2.0, BSD-3-Clause, LGPL-2.0, MIT, LGPL-2.1, LGPL-3.0
  1. <?php
  2. final class CelerityResourceTransformer extends Phobject {
  3. private $minify;
  4. private $rawURIMap;
  5. private $celerityMap;
  6. private $translateURICallback;
  7. private $currentPath;
  8. private $postprocessorKey;
  9. private $variableMap;
  10. public function setPostprocessorKey($postprocessor_key) {
  11. $this->postprocessorKey = $postprocessor_key;
  12. return $this;
  13. }
  14. public function getPostprocessorKey() {
  15. return $this->postprocessorKey;
  16. }
  17. public function setTranslateURICallback($translate_uricallback) {
  18. $this->translateURICallback = $translate_uricallback;
  19. return $this;
  20. }
  21. public function setMinify($minify) {
  22. $this->minify = $minify;
  23. return $this;
  24. }
  25. public function setCelerityMap(CelerityResourceMap $celerity_map) {
  26. $this->celerityMap = $celerity_map;
  27. return $this;
  28. }
  29. public function setRawURIMap(array $raw_urimap) {
  30. $this->rawURIMap = $raw_urimap;
  31. return $this;
  32. }
  33. public function getRawURIMap() {
  34. return $this->rawURIMap;
  35. }
  36. /**
  37. * @phutil-external-symbol function jsShrink
  38. */
  39. public function transformResource($path, $data) {
  40. $type = self::getResourceType($path);
  41. switch ($type) {
  42. case 'css':
  43. $data = $this->replaceCSSPrintRules($path, $data);
  44. $data = $this->replaceCSSVariables($path, $data);
  45. $data = preg_replace_callback(
  46. '@url\s*\((\s*[\'"]?.*?)\)@s',
  47. nonempty(
  48. $this->translateURICallback,
  49. array($this, 'translateResourceURI')),
  50. $data);
  51. break;
  52. }
  53. if (!$this->minify) {
  54. return $data;
  55. }
  56. // Some resources won't survive minification (like d3.min.js), and are
  57. // marked so as not to be minified.
  58. if (strpos($data, '@'.'do-not-minify') !== false) {
  59. return $data;
  60. }
  61. switch ($type) {
  62. case 'css':
  63. // Remove comments.
  64. $data = preg_replace('@/\*.*?\*/@s', '', $data);
  65. // Remove whitespace around symbols.
  66. $data = preg_replace('@\s*([{}:;,])\s*@', '\1', $data);
  67. // Remove unnecessary semicolons.
  68. $data = preg_replace('@;}@', '}', $data);
  69. // Replace #rrggbb with #rgb when possible.
  70. $data = preg_replace(
  71. '@#([a-f0-9])\1([a-f0-9])\2([a-f0-9])\3@i',
  72. '#\1\2\3',
  73. $data);
  74. $data = trim($data);
  75. break;
  76. case 'js':
  77. // If `jsxmin` is available, use it. jsxmin is the Javelin minifier and
  78. // produces the smallest output, but is complicated to build.
  79. if (Filesystem::binaryExists('jsxmin')) {
  80. $future = new ExecFuture('jsxmin __DEV__:0');
  81. $future->write($data);
  82. list($err, $result) = $future->resolve();
  83. if (!$err) {
  84. $data = $result;
  85. break;
  86. }
  87. }
  88. // If `jsxmin` is not available, use `JsShrink`, which doesn't compress
  89. // quite as well but is always available.
  90. $root = dirname(phutil_get_library_root('phabricator'));
  91. require_once $root.'/externals/JsShrink/jsShrink.php';
  92. $data = jsShrink($data);
  93. break;
  94. }
  95. return $data;
  96. }
  97. public static function getResourceType($path) {
  98. return last(explode('.', $path));
  99. }
  100. public function translateResourceURI(array $matches) {
  101. $uri = trim($matches[1], "'\" \r\t\n");
  102. $tail = '';
  103. // If the resource URI has a query string or anchor, strip it off before
  104. // we go looking for the resource. We'll stitch it back on later. This
  105. // primarily affects FontAwesome.
  106. $parts = preg_split('/(?=[?#])/', $uri, 2);
  107. if (count($parts) == 2) {
  108. $uri = $parts[0];
  109. $tail = $parts[1];
  110. }
  111. $alternatives = array_unique(
  112. array(
  113. $uri,
  114. ltrim($uri, '/'),
  115. ));
  116. foreach ($alternatives as $alternative) {
  117. if ($this->rawURIMap !== null) {
  118. if (isset($this->rawURIMap[$alternative])) {
  119. $uri = $this->rawURIMap[$alternative];
  120. break;
  121. }
  122. }
  123. if ($this->celerityMap) {
  124. $resource_uri = $this->celerityMap->getURIForName($alternative);
  125. if ($resource_uri) {
  126. // Check if we can use a data URI for this resource. If not, just
  127. // use a normal Celerity URI.
  128. $data_uri = $this->generateDataURI($alternative);
  129. if ($data_uri) {
  130. $uri = $data_uri;
  131. } else {
  132. $uri = $resource_uri;
  133. }
  134. break;
  135. }
  136. }
  137. }
  138. return 'url('.$uri.$tail.')';
  139. }
  140. private function replaceCSSVariables($path, $data) {
  141. $this->currentPath = $path;
  142. return preg_replace_callback(
  143. '/{\$([^}]+)}/',
  144. array($this, 'replaceCSSVariable'),
  145. $data);
  146. }
  147. private function replaceCSSPrintRules($path, $data) {
  148. $this->currentPath = $path;
  149. return preg_replace_callback(
  150. '/!print\s+(.+?{.+?})/s',
  151. array($this, 'replaceCSSPrintRule'),
  152. $data);
  153. }
  154. public function getCSSVariableMap() {
  155. $postprocessor_key = $this->getPostprocessorKey();
  156. $postprocessor = CelerityPostprocessor::getPostprocessor(
  157. $postprocessor_key);
  158. if (!$postprocessor) {
  159. $postprocessor = CelerityPostprocessor::getPostprocessor(
  160. CelerityDefaultPostprocessor::POSTPROCESSOR_KEY);
  161. }
  162. return $postprocessor->getVariables();
  163. }
  164. public function replaceCSSVariable($matches) {
  165. if (!$this->variableMap) {
  166. $this->variableMap = $this->getCSSVariableMap();
  167. }
  168. $var_name = $matches[1];
  169. if (empty($this->variableMap[$var_name])) {
  170. $path = $this->currentPath;
  171. throw new Exception(
  172. pht(
  173. "CSS file '%s' has unknown variable '%s'.",
  174. $path,
  175. $var_name));
  176. }
  177. return $this->variableMap[$var_name];
  178. }
  179. public function replaceCSSPrintRule($matches) {
  180. $rule = $matches[1];
  181. $rules = array();
  182. $rules[] = '.printable '.$rule;
  183. $rules[] = "@media print {\n ".str_replace("\n", "\n ", $rule)."\n}\n";
  184. return implode("\n\n", $rules);
  185. }
  186. /**
  187. * Attempt to generate a data URI for a resource. We'll generate a data URI
  188. * if the resource is a valid resource of an appropriate type, and is
  189. * small enough. Otherwise, this method will return `null` and we'll end up
  190. * using a normal URI instead.
  191. *
  192. * @param string Resource name to attempt to generate a data URI for.
  193. * @return string|null Data URI, or null if we declined to generate one.
  194. */
  195. private function generateDataURI($resource_name) {
  196. $ext = last(explode('.', $resource_name));
  197. switch ($ext) {
  198. case 'png':
  199. $type = 'image/png';
  200. break;
  201. case 'gif':
  202. $type = 'image/gif';
  203. break;
  204. case 'jpg':
  205. $type = 'image/jpeg';
  206. break;
  207. default:
  208. return null;
  209. }
  210. // In IE8, 32KB is the maximum supported URI length.
  211. $maximum_data_size = (1024 * 32);
  212. $data = $this->celerityMap->getResourceDataForName($resource_name);
  213. if (strlen($data) >= $maximum_data_size) {
  214. // If the data is already too large on its own, just bail before
  215. // encoding it.
  216. return null;
  217. }
  218. $uri = 'data:'.$type.';base64,'.base64_encode($data);
  219. if (strlen($uri) >= $maximum_data_size) {
  220. return null;
  221. }
  222. return $uri;
  223. }
  224. }