Remove product literal strings in "pht()", part 18
[phabricator.git] / src / applications / celerity / CelerityResourceTransformer.php
blob6d86a8806a0f3a66b8f7161e938ded3c04409fd7
1 <?php
3 final class CelerityResourceTransformer extends Phobject {
5 private $minify;
6 private $rawURIMap;
7 private $celerityMap;
8 private $translateURICallback;
9 private $currentPath;
10 private $postprocessorKey;
11 private $variableMap;
13 public function setPostprocessorKey($postprocessor_key) {
14 $this->postprocessorKey = $postprocessor_key;
15 return $this;
18 public function getPostprocessorKey() {
19 return $this->postprocessorKey;
22 public function setTranslateURICallback($translate_uricallback) {
23 $this->translateURICallback = $translate_uricallback;
24 return $this;
27 public function setMinify($minify) {
28 $this->minify = $minify;
29 return $this;
32 public function setCelerityMap(CelerityResourceMap $celerity_map) {
33 $this->celerityMap = $celerity_map;
34 return $this;
37 public function setRawURIMap(array $raw_urimap) {
38 $this->rawURIMap = $raw_urimap;
39 return $this;
42 public function getRawURIMap() {
43 return $this->rawURIMap;
46 /**
47 * @phutil-external-symbol function jsShrink
49 public function transformResource($path, $data) {
50 $type = self::getResourceType($path);
52 switch ($type) {
53 case 'css':
54 $data = $this->replaceCSSPrintRules($path, $data);
55 $data = $this->replaceCSSVariables($path, $data);
56 $data = preg_replace_callback(
57 '@url\s*\((\s*[\'"]?.*?)\)@s',
58 nonempty(
59 $this->translateURICallback,
60 array($this, 'translateResourceURI')),
61 $data);
62 break;
65 if (!$this->minify) {
66 return $data;
69 // Some resources won't survive minification (like d3.min.js), and are
70 // marked so as not to be minified.
71 if (strpos($data, '@'.'do-not-minify') !== false) {
72 return $data;
75 switch ($type) {
76 case 'css':
77 // Remove comments.
78 $data = preg_replace('@/\*.*?\*/@s', '', $data);
79 // Remove whitespace around symbols.
80 $data = preg_replace('@\s*([{}:;,])\s*@', '\1', $data);
81 // Remove unnecessary semicolons.
82 $data = preg_replace('@;}@', '}', $data);
83 // Replace #rrggbb with #rgb when possible.
84 $data = preg_replace(
85 '@#([a-f0-9])\1([a-f0-9])\2([a-f0-9])\3@i',
86 '#\1\2\3',
87 $data);
88 $data = trim($data);
89 break;
90 case 'js':
92 // If `jsxmin` is available, use it. jsxmin is the Javelin minifier and
93 // produces the smallest output, but is complicated to build.
94 if (Filesystem::binaryExists('jsxmin')) {
95 $future = new ExecFuture('jsxmin __DEV__:0');
96 $future->write($data);
97 list($err, $result) = $future->resolve();
98 if (!$err) {
99 $data = $result;
100 break;
104 // If `jsxmin` is not available, use `JsShrink`, which doesn't compress
105 // quite as well but is always available.
106 $root = dirname(phutil_get_library_root('phabricator'));
107 require_once $root.'/externals/JsShrink/jsShrink.php';
108 $data = jsShrink($data);
110 break;
113 return $data;
116 public static function getResourceType($path) {
117 return last(explode('.', $path));
120 public function translateResourceURI(array $matches) {
121 $uri = trim($matches[1], "'\" \r\t\n");
122 $tail = '';
124 // If the resource URI has a query string or anchor, strip it off before
125 // we go looking for the resource. We'll stitch it back on later. This
126 // primarily affects FontAwesome.
128 $parts = preg_split('/(?=[?#])/', $uri, 2);
129 if (count($parts) == 2) {
130 $uri = $parts[0];
131 $tail = $parts[1];
134 $alternatives = array_unique(
135 array(
136 $uri,
137 ltrim($uri, '/'),
140 foreach ($alternatives as $alternative) {
141 if ($this->rawURIMap !== null) {
142 if (isset($this->rawURIMap[$alternative])) {
143 $uri = $this->rawURIMap[$alternative];
144 break;
148 if ($this->celerityMap) {
149 $resource_uri = $this->celerityMap->getURIForName($alternative);
150 if ($resource_uri) {
151 // Check if we can use a data URI for this resource. If not, just
152 // use a normal Celerity URI.
153 $data_uri = $this->generateDataURI($alternative);
154 if ($data_uri) {
155 $uri = $data_uri;
156 } else {
157 $uri = $resource_uri;
159 break;
164 return 'url('.$uri.$tail.')';
167 private function replaceCSSVariables($path, $data) {
168 $this->currentPath = $path;
169 return preg_replace_callback(
170 '/{\$([^}]+)}/',
171 array($this, 'replaceCSSVariable'),
172 $data);
175 private function replaceCSSPrintRules($path, $data) {
176 $this->currentPath = $path;
177 return preg_replace_callback(
178 '/!print\s+(.+?{.+?})/s',
179 array($this, 'replaceCSSPrintRule'),
180 $data);
183 public function getCSSVariableMap() {
184 $postprocessor_key = $this->getPostprocessorKey();
185 $postprocessor = CelerityPostprocessor::getPostprocessor(
186 $postprocessor_key);
188 if (!$postprocessor) {
189 $postprocessor = CelerityPostprocessor::getPostprocessor(
190 CelerityDefaultPostprocessor::POSTPROCESSOR_KEY);
193 return $postprocessor->getVariables();
196 public function replaceCSSVariable($matches) {
197 if (!$this->variableMap) {
198 $this->variableMap = $this->getCSSVariableMap();
201 $var_name = $matches[1];
202 if (empty($this->variableMap[$var_name])) {
203 $path = $this->currentPath;
204 throw new Exception(
205 pht(
206 "CSS file '%s' has unknown variable '%s'.",
207 $path,
208 $var_name));
211 return $this->variableMap[$var_name];
214 public function replaceCSSPrintRule($matches) {
215 $rule = $matches[1];
217 $rules = array();
218 $rules[] = '.printable '.$rule;
219 $rules[] = "@media print {\n ".str_replace("\n", "\n ", $rule)."\n}\n";
221 return implode("\n\n", $rules);
226 * Attempt to generate a data URI for a resource. We'll generate a data URI
227 * if the resource is a valid resource of an appropriate type, and is
228 * small enough. Otherwise, this method will return `null` and we'll end up
229 * using a normal URI instead.
231 * @param string Resource name to attempt to generate a data URI for.
232 * @return string|null Data URI, or null if we declined to generate one.
234 private function generateDataURI($resource_name) {
235 $ext = last(explode('.', $resource_name));
236 switch ($ext) {
237 case 'png':
238 $type = 'image/png';
239 break;
240 case 'gif':
241 $type = 'image/gif';
242 break;
243 case 'jpg':
244 $type = 'image/jpeg';
245 break;
246 default:
247 return null;
250 // In IE8, 32KB is the maximum supported URI length.
251 $maximum_data_size = (1024 * 32);
253 $data = $this->celerityMap->getResourceDataForName($resource_name);
254 if (strlen($data) >= $maximum_data_size) {
255 // If the data is already too large on its own, just bail before
256 // encoding it.
257 return null;
260 $uri = 'data:'.$type.';base64,'.base64_encode($data);
261 if (strlen($uri) >= $maximum_data_size) {
262 return null;
265 return $uri;