@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at upstream/main 269 lines 7.3 kB view raw
1<?php 2 3final class CelerityResourceTransformer extends Phobject { 4 5 private $minify; 6 private $rawURIMap; 7 private $celerityMap; 8 private $translateURICallback; 9 private $currentPath; 10 private $postprocessorKey; 11 private $variableMap; 12 13 public function setPostprocessorKey($postprocessor_key) { 14 $this->postprocessorKey = $postprocessor_key; 15 return $this; 16 } 17 18 public function getPostprocessorKey() { 19 return $this->postprocessorKey; 20 } 21 22 public function setTranslateURICallback($translate_uricallback) { 23 $this->translateURICallback = $translate_uricallback; 24 return $this; 25 } 26 27 public function setMinify($minify) { 28 $this->minify = $minify; 29 return $this; 30 } 31 32 public function setCelerityMap(CelerityResourceMap $celerity_map) { 33 $this->celerityMap = $celerity_map; 34 return $this; 35 } 36 37 public function setRawURIMap(array $raw_urimap) { 38 $this->rawURIMap = $raw_urimap; 39 return $this; 40 } 41 42 public function getRawURIMap() { 43 return $this->rawURIMap; 44 } 45 46 /** 47 * @phutil-external-symbol function jsShrink 48 */ 49 public function transformResource($path, $data) { 50 $type = self::getResourceType($path); 51 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; 63 } 64 65 if (!$this->minify) { 66 return $data; 67 } 68 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; 73 } 74 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': 91 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; 101 } 102 } 103 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); 109 110 break; 111 } 112 113 return $data; 114 } 115 116 public static function getResourceType($path) { 117 return last(explode('.', $path)); 118 } 119 120 public function translateResourceURI(array $matches) { 121 $uri = trim($matches[1], "'\" \r\t\n"); 122 $tail = ''; 123 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. 127 128 $parts = preg_split('/(?=[?#])/', $uri, 2); 129 if (count($parts) == 2) { 130 $uri = $parts[0]; 131 $tail = $parts[1]; 132 } 133 134 $alternatives = array_unique( 135 array( 136 $uri, 137 ltrim($uri, '/'), 138 )); 139 140 foreach ($alternatives as $alternative) { 141 if ($this->rawURIMap !== null) { 142 if (isset($this->rawURIMap[$alternative])) { 143 $uri = $this->rawURIMap[$alternative]; 144 break; 145 } 146 } 147 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; 158 } 159 break; 160 } 161 } 162 } 163 164 return 'url('.$uri.$tail.')'; 165 } 166 167 private function replaceCSSVariables($path, $data) { 168 $this->currentPath = $path; 169 return preg_replace_callback( 170 '/{\$([^}]+)}/', 171 array($this, 'replaceCSSVariable'), 172 $data); 173 } 174 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); 181 } 182 183 public function getCSSVariableMap() { 184 $postprocessor_key = $this->getPostprocessorKey(); 185 $postprocessor = CelerityPostprocessor::getPostprocessor( 186 $postprocessor_key); 187 188 if (!$postprocessor) { 189 $postprocessor = CelerityPostprocessor::getPostprocessor( 190 CelerityDefaultPostprocessor::POSTPROCESSOR_KEY); 191 } 192 193 return $postprocessor->getVariables(); 194 } 195 196 public function replaceCSSVariable($matches) { 197 if (!$this->variableMap) { 198 $this->variableMap = $this->getCSSVariableMap(); 199 } 200 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)); 209 } 210 211 return $this->variableMap[$var_name]; 212 } 213 214 public function replaceCSSPrintRule($matches) { 215 $rule = $matches[1]; 216 217 $rules = array(); 218 $rules[] = '.printable '.$rule; 219 $rules[] = "@media print {\n ".str_replace("\n", "\n ", $rule)."\n}\n"; 220 221 return implode("\n\n", $rules); 222 } 223 224 225 /** 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. 230 * 231 * @param string $resource_name Resource name to attempt to generate a data 232 * URI for. 233 * @return string|null Data URI, or null if we declined to generate one. 234 */ 235 private function generateDataURI($resource_name) { 236 $ext = last(explode('.', $resource_name)); 237 switch ($ext) { 238 case 'png': 239 $type = 'image/png'; 240 break; 241 case 'gif': 242 $type = 'image/gif'; 243 break; 244 case 'jpg': 245 $type = 'image/jpeg'; 246 break; 247 default: 248 return null; 249 } 250 251 // In IE8, 32KB is the maximum supported URI length. 252 $maximum_data_size = (1024 * 32); 253 254 $data = $this->celerityMap->getResourceDataForName($resource_name); 255 if (strlen($data) >= $maximum_data_size) { 256 // If the data is already too large on its own, just bail before 257 // encoding it. 258 return null; 259 } 260 261 $uri = 'data:'.$type.';base64,'.base64_encode($data); 262 if (strlen($uri) >= $maximum_data_size) { 263 return null; 264 } 265 266 return $uri; 267 } 268 269}