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