PageRenderTime 57ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/protected/classes/Minify.php

https://github.com/seanrowe/Swarm-Admin-OS
PHP | 377 lines | 265 code | 54 blank | 58 comment | 52 complexity | 53c61e5ab113f82b78a504d9696ee21b MD5 | raw file
  1. <?php
  2. class Minify
  3. {
  4. /**
  5. * Holds the static instance of the minify object
  6. * @var Minify
  7. */
  8. protected static $instance = null;
  9. /**
  10. * Used in the css minifier section
  11. * @var boolean
  12. */
  13. protected $in_hack = false;
  14. /**
  15. * Returns a static instance of the Minify object
  16. * @return Minify
  17. */
  18. public static function instance()
  19. {
  20. if (null === self::$instance)
  21. {
  22. self::$instance = new Minify();
  23. }
  24. return self::$instance;
  25. }
  26. public function run($type, $page)
  27. {
  28. $key = md5($type . '-' . $page);
  29. $rval = Yii::app()->fileCache->get($key);
  30. if (false !== $rval)
  31. {
  32. return $rval;
  33. }
  34. $page_files = isset(Yii::app()->params[$type]['page'][$page]) ? Yii::app()->params[$type]['page'][$page] : array();
  35. $global_files = array();
  36. if (false === Yii::app()->request->getParam('skip_global', false))
  37. {
  38. $global_files = Yii::app()->params[$type]['global'];
  39. }
  40. $files = array_merge($global_files, $page_files);
  41. $browser = Yii::app()->request->getBrowser();
  42. $rval = '';
  43. $dependencies = array(
  44. new CFileCacheDependency(Yii::app()->basePath . DS . 'config' . DS . 'js.php'),
  45. new CFileCacheDependency(Yii::app()->basePath . DS . 'config' . DS . 'css.php')
  46. );
  47. foreach ($files as $file)
  48. {
  49. if (true === isset($file['browser']))
  50. {
  51. if ($browser['name'] != $file['browser']['name'])
  52. {
  53. continue;
  54. }
  55. if (true === isset($file['browser']['version']))
  56. {
  57. if ($browser['version'] != $file['browser']['version'])
  58. {
  59. continue;
  60. }
  61. }
  62. }
  63. if (true === isset($file['request']))
  64. {
  65. $should_continue = false;
  66. foreach ($file['request'] as $key => $value)
  67. {
  68. if ($value != Yii::app()->request->getParam($key, null))
  69. {
  70. $should_continue = true;
  71. break;
  72. }
  73. }
  74. if (true === $should_continue)
  75. {
  76. continue;
  77. }
  78. }
  79. if (false === isset($file['combine_only']))
  80. {
  81. $file['combine_only'] = false;
  82. }
  83. if (false === isset($file['php']))
  84. {
  85. $file['php'] = false;
  86. }
  87. $file['file'] = Yii::app()->params['docRoot'] . DS . trim($file['file']);
  88. if (true === file_exists($file['file']))
  89. {
  90. if (true === $file['php'])
  91. {
  92. ob_start();
  93. require($file['file']);
  94. $str = ob_get_contents();
  95. ob_end_clean();
  96. }
  97. else
  98. {
  99. $str = file_get_contents($file['file']);
  100. }
  101. $rval .= ($file['combine_only'] ? $str : $this->$type($str));
  102. if (';' != substr($rval, -1) && 'js' == $type)
  103. {
  104. $rval .= ';';
  105. }
  106. $rval .= "\n";
  107. }
  108. else
  109. {
  110. throw new Exception('File not found: ' . $file['file']);
  111. }
  112. $dependencies[] = new CFileCacheDependency($file['file']);
  113. }
  114. Yii::app()->fileCache->set($key, $rval, 0, new CChainedCacheDependency($dependencies));
  115. return $rval;
  116. }
  117. protected function js($js)
  118. {
  119. defined('TOKEN_END') || define('TOKEN_END', 1);
  120. defined('TOKEN_NUMBER') || define('TOKEN_NUMBER', 2);
  121. defined('TOKEN_IDENTIFIER') || define('TOKEN_IDENTIFIER', 3);
  122. defined('TOKEN_STRING') || define('TOKEN_STRING', 4);
  123. defined('TOKEN_REGEXP') || define('TOKEN_REGEXP', 5);
  124. defined('TOKEN_NEWLINE') || define('TOKEN_NEWLINE', 6);
  125. defined('TOKEN_CONDCOMMENT_START') || define('TOKEN_CONDCOMMENT_START', 7);
  126. defined('TOKEN_CONDCOMMENT_END') || define('TOKEN_CONDCOMMENT_END', 8);
  127. defined('JS_SCRIPT') || define('JS_SCRIPT', 100);
  128. defined('JS_BLOCK') || define('JS_BLOCK', 101);
  129. defined('JS_LABEL') || define('JS_LABEL', 102);
  130. defined('JS_FOR_IN') || define('JS_FOR_IN', 103);
  131. defined('JS_CALL') || define('JS_CALL', 104);
  132. defined('JS_NEW_WITH_ARGS') || define('JS_NEW_WITH_ARGS', 105);
  133. defined('JS_INDEX') || define('JS_INDEX', 106);
  134. defined('JS_ARRAY_INIT') || define('JS_ARRAY_INIT', 107);
  135. defined('JS_OBJECT_INIT') || define('JS_OBJECT_INIT', 108);
  136. defined('JS_PROPERTY_INIT') || define('JS_PROPERTY_INIT', 109);
  137. defined('JS_GETTER') || define('JS_GETTER', 110);
  138. defined('JS_SETTER') || define('JS_SETTER', 111);
  139. defined('JS_GROUP') || define('JS_GROUP', 112);
  140. defined('JS_LIST') || define('JS_LIST', 113);
  141. defined('DECLARED_FORM') || define('DECLARED_FORM', 0);
  142. defined('EXPRESSED_FORM') || define('EXPRESSED_FORM', 1);
  143. defined('EXPRESSED_FORM') || define('EXPRESSED_FORM', 2);
  144. return JSMinPlus::minify($js);
  145. }
  146. protected function css($css)
  147. {
  148. $css = str_replace("\r\n", "\n", $css);
  149. // preserve empty comment after '>'
  150. // http://www.webdevout.net/css-hacks#in_css-selectors
  151. $css = preg_replace('@>/\\*\\s*\\*/@', '>/*keep*/', $css);
  152. // preserve empty comment between property and value
  153. // http://css-discuss.incutio.com/?page=BoxModelHack
  154. $css = preg_replace('@/\\*\\s*\\*/\\s*:@', '/*keep*/:', $css);
  155. $css = preg_replace('@:\\s*/\\*\\s*\\*/@', ':/*keep*/', $css);
  156. // apply callback to all valid comments (and strip out surrounding ws
  157. $css = preg_replace_callback('@\\s*/\\*([\\s\\S]*?)\\*/\\s*@', array($this, 'commentCallback'), $css);
  158. // remove ws around { } and last semicolon in declaration block
  159. $css = preg_replace('/\\s*{\\s*/', '{', $css);
  160. $css = preg_replace('/;?\\s*}\\s*/', '}', $css);
  161. // remove ws surrounding semicolons
  162. $css = preg_replace('/\\s*;\\s*/', ';', $css);
  163. // remove ws around urls
  164. $css = preg_replace('/
  165. url\\( # url(
  166. \\s*
  167. ([^\\)]+?) # 1 = the URL (really just a bunch of non right parenthesis)
  168. \\s*
  169. \\) # )
  170. /x', 'url($1)', $css
  171. );
  172. // remove ws between rules and colons
  173. $css = preg_replace('/
  174. \\s*
  175. ([{;]) # 1 = beginning of block or rule separator
  176. \\s*
  177. ([\\*_]?[\\w\\-]+) # 2 = property (and maybe IE filter)
  178. \\s*
  179. :
  180. \\s*
  181. (\\b|[#\'"-]) # 3 = first character of a value
  182. /x', '$1$2:$3', $css
  183. );
  184. // remove ws in selectors
  185. $css = preg_replace_callback('/
  186. (?: # non-capture
  187. \\s*
  188. [^~>+,\\s]+ # selector part
  189. \\s*
  190. [,>+~] # combinators
  191. )+
  192. \\s*
  193. [^~>+,\\s]+ # selector part
  194. { # open declaration block
  195. /x'
  196. ,array($this, 'selectorsCallback'), $css
  197. );
  198. // minimize hex colors
  199. $css = preg_replace('/([^=])#([a-f\\d])\\2([a-f\\d])\\3([a-f\\d])\\4([\\s;\\}])/i', '$1#$2$3$4$5', $css);
  200. // remove spaces between font families
  201. $css = preg_replace_callback('/font-family:([^;}]+)([;}])/', array($this, 'fontFamilyCallback'), $css);
  202. $css = preg_replace('/@import\\s+url/', '@import url', $css);
  203. // replace any ws involving newlines with a single newline
  204. $css = preg_replace('/[ \\t]*\\n+\\s*/', "\n", $css);
  205. // separate common descendent selectors w/ newlines (to limit line lengths)
  206. $css = preg_replace('/([\\w#\\.\\*]+)\\s+([\\w#\\.\\*]+){/', "$1\n$2{", $css);
  207. // Use newline after 1st numeric value (to limit line lengths).
  208. $css = preg_replace('/
  209. ((?:padding|margin|border|outline):\\d+(?:px|em)?) # 1 = prop : 1st numeric value
  210. \\s+
  211. /x'
  212. ,"$1\n", $css
  213. );
  214. // prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/
  215. $css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css);
  216. return trim($css);
  217. }
  218. /**
  219. * Replace what looks like a set of selectors
  220. *
  221. * @param array $regex_matches regex matches
  222. *
  223. * @return string
  224. */
  225. protected function selectorsCallback(array $regex_matches)
  226. {
  227. // remove ws around the combinators
  228. return preg_replace('/\\s*([,>+~])\\s*/', '$1', $regex_matches[0]);
  229. }
  230. /**
  231. * Process a comment and return a replacement
  232. *
  233. * @param array $regex_matches regex matches
  234. *
  235. * @return string
  236. */
  237. protected function commentCallback(array $regex_matches)
  238. {
  239. $has_surrounding_ws = (trim($regex_matches[0]) !== $regex_matches[1]);
  240. $regex_matches = $regex_matches[1];
  241. // $regex_matches is the comment content w/o the surrounding tokens,
  242. // but the return value will replace the entire comment.
  243. if ($regex_matches === 'keep')
  244. {
  245. return '/**/';
  246. }
  247. if ($regex_matches === '" "')
  248. {
  249. // component of http://tantek.com/CSS/Examples/midpass.html
  250. return '/*" "*/';
  251. }
  252. if (preg_match('@";\\}\\s*\\}/\\*\\s+@', $regex_matches))
  253. {
  254. // component of http://tantek.com/CSS/Examples/midpass.html
  255. return '/*";}}/* */';
  256. }
  257. if ($this->in_hack)
  258. {
  259. // inversion: feeding only to one browser
  260. if (preg_match('@
  261. ^/ # comment started like /*/
  262. \\s*
  263. (\\S[\\s\\S]+?) # has at least some non-ws content
  264. \\s*
  265. /\\* # ends like /*/ or /**/
  266. @x', $regex_matches, $n))
  267. {
  268. // end hack mode after this comment, but preserve the hack and comment content
  269. $this->in_hack = false;
  270. return "/*/{$n[1]}/**/";
  271. }
  272. }
  273. // comment ends like \*/
  274. if (substr($regex_matches, -1) === '\\')
  275. {
  276. // begin hack mode and preserve hack
  277. $this->in_hack = true;
  278. return '/*\\*/';
  279. }
  280. // comment looks like /*/ foo */
  281. if ($regex_matches !== '' && $regex_matches[0] === '/')
  282. {
  283. // begin hack mode and preserve hack
  284. $this->in_hack = true;
  285. return '/*/*/';
  286. }
  287. if ($this->in_hack)
  288. {
  289. // a regular comment ends hack mode but should be preserved
  290. $this->in_hack = false;
  291. return '/**/';
  292. }
  293. // Issue 107: if there's any surrounding whitespace, it may be important, so
  294. // replace the comment with a single space
  295. return $has_surrounding_ws // remove all other comments
  296. ? ' '
  297. : '';
  298. }
  299. /**
  300. * Process a font-family listing and return a replacement
  301. *
  302. * @param array $regex_matches regex matches
  303. *
  304. * @return string
  305. */
  306. protected function fontFamilyCallback(array $regex_matches)
  307. {
  308. $regex_matches[1] = preg_replace('/
  309. \\s*
  310. (
  311. "[^"]+" # 1 = family in double qutoes
  312. |\'[^\']+\' # or 1 = family in single quotes
  313. |[\\w\\-]+ # or 1 = unquoted family
  314. )
  315. \\s*
  316. /x', '$1', $regex_matches[1]);
  317. return 'font-family:' . $regex_matches[1] . $regex_matches[2];
  318. }
  319. }
  320. ?>