PageRenderTime 114ms CodeModel.GetById 25ms RepoModel.GetById 12ms app.codeStats 0ms

/css/cc-css.php

https://github.com/tany/cc-css
PHP | 539 lines | 414 code | 98 blank | 27 comment | 59 complexity | 4df2d77abf64f7e791e665bdefaacc6b MD5 | raw file
  1. <?php
  2. /**
  3. * ===================================================================
  4. * Copyright (c) 2013 tany
  5. * Licensed under The MIT License <http://opensource.org/licenses/MIT>
  6. * ===================================================================
  7. */
  8. // ------ call ------ //
  9. $css = new CC_CSS();
  10. //$css->output('../css-output'); // output directory
  11. $css->prepend('usual.cc.css'); // auto prepend file
  12. $css->cache()->decode()->flush(); // output..
  13. // ------ class ------ //
  14. class CC_CSS {
  15. public
  16. $meta = array("@charset \"UTF-8\";\n");
  17. protected
  18. $_output = null;
  19. protected
  20. $_cache = null;
  21. protected
  22. $_mtime = null;
  23. protected
  24. $_file = null;
  25. protected
  26. $_uri = null;
  27. protected
  28. $_prepended = array();
  29. protected
  30. $_current = array();
  31. protected
  32. $_contents = array();
  33. protected
  34. $_css = '';
  35. protected
  36. $_vars = array();
  37. protected
  38. $_errors = array();
  39. public function __construct() {
  40. $this->_uri = dirname($_SERVER['SCRIPT_NAME']);
  41. $this->_mtime = filemtime(__FILE__);
  42. }
  43. public function output($dir) {
  44. $this->_output = $dir;
  45. }
  46. public function prepend($file) {
  47. $this->_readFile($file);
  48. $this->_prepended[] = $file;
  49. return $this;
  50. }
  51. public function cache($expires = '+3 days') {
  52. $this->_cache = $expires;
  53. return $this;
  54. }
  55. public function decode() {
  56. $file = isset($_REQUEST['file']) ? "{$_REQUEST['file']}.cc.css" : null;
  57. if (strpos($file, '..') !== false || strpos($file, '://') !== false || $file[0] === '/' || !is_file($file)) {
  58. header('HTTP/1.1 404 Not Found');
  59. exit;
  60. }
  61. $this->_file = $file;
  62. $this->_readFile($file);
  63. if ($this->_cache) {
  64. $headers = apache_request_headers();
  65. if (isset($headers['If-Modified-Since']) && (strtotime($headers['If-Modified-Since']) == $this->_mtime)) {
  66. $this->_cache = true;
  67. return $this;
  68. }
  69. }
  70. foreach ($this->_prepended as $file) {
  71. $this->_css .= $this->_decodeFile($file);
  72. }
  73. $this->_css .= $this->_decodeFile($this->_file);
  74. return $this;
  75. }
  76. public function flush() {
  77. // cache
  78. if (count($this->_errors) == 0) {
  79. if ($this->_cache === true) {
  80. header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $this->_mtime) . ' GMT', true, 304);
  81. return $this;
  82. } else if ($this->_cache) {
  83. $expires = new DateTime($this->_cache);
  84. header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $this->_mtime) . ' GMT');
  85. header('Cache-Control: max-age=' . ($expires->format('U') - time()));
  86. }
  87. }
  88. $css = implode('', array_unique($this->meta)) . "\n{$this->_css}";
  89. $css = preg_replace('/\n{2,}/m', "\n\n", $css);
  90. if (count($this->_errors)) {
  91. $hr = str_repeat('=', 70);
  92. $err = "/*\n\nNotice: compile error\n{$hr} \n- " . implode("\n- ", $this->_errors) . "\n{$hr} \n\n*/\n\n";
  93. $css = preg_replace('/^(?!@charset|@import)/m', $err . '$1', $css, 1);
  94. }
  95. if ($this->_output) {
  96. $this->_outputFile($css);
  97. }
  98. header('Content-type: text/css; charset=utf-8');
  99. //header('Content-type: text/plain; charset=utf-8');
  100. print $css;
  101. }
  102. protected function _readFile($file, $src = null) {
  103. if (!is_file($file = $this->_fixPath($src, $file))) {
  104. return $this->_error("no such file: {$file} in {$src}");
  105. }
  106. if (!isset($this->_contents[$file])) {
  107. $this->_contents[$file] = file_get_contents($file);
  108. $this->_mtime = max($this->_mtime, filemtime($file));
  109. if (preg_match_all('/^\s*@include ["\'](.*?)["\'];/m', $this->_contents[$file], $m)) {
  110. foreach ($m[1] as $_file) {
  111. $this->_readFile($_file, $file);
  112. }
  113. }
  114. }
  115. }
  116. protected function _outputFile($css) {
  117. $base = $this->_output;
  118. $file = "{$base}/" . preg_replace('/\.cc\.css$/', '.css', $this->_file);
  119. $dir = dirname($file);
  120. if (!is_dir($base)) {
  121. mkdir($base);
  122. chmod($base, 0757);
  123. }
  124. if (!is_dir($dir) || !is_writable($dir)) {
  125. $dir = $base;
  126. foreach (explode('/', dirname($this->_file)) as $name) {
  127. $dir .= "/{$name}";
  128. if (!is_dir($dir)) mkdir($dir);
  129. chmod($dir, 0757);
  130. }
  131. }
  132. if (is_file($file)) {
  133. if (!is_writable($file)) chmod($file, 0646);
  134. if (file_get_contents($file) !== $css) file_put_contents($file, $css);
  135. } else {
  136. file_put_contents($file, $css);
  137. chmod($file, 0646);
  138. }
  139. }
  140. protected function _decodeFile($file) {
  141. if (!isset($this->_contents[$file])) {
  142. return null;
  143. }
  144. array_unshift($this->_current, $file);
  145. $css = $this->_contents[$file];
  146. $css = preg_replace('/^@charset.*?(\n|$)/', '', $css); // remove @charset
  147. $css = preg_replace('/(^\s*|\s+)\/\/.*$/m', '', $css); // remove comment
  148. if (count($this->_current) > 1) { // rewrite url on @include
  149. $css = preg_replace_callback('/^@import ["\'](.*?)["\'];/m', array($this, '_importCallback'), $css);
  150. $css = preg_replace_callback('/url\(["\']?(.*?)["\']?\)/', array($this, '_urlCallback'), $css);
  151. }
  152. if (preg_match_all('/^@import .*?(?:\r\n|\n|$)/m', $css, $m)) { // stack @import
  153. $this->meta = array_merge($this->meta, $m[0]);
  154. $css = preg_replace('/^(@import .*?(\r\n|\n|$))/m', '', $css);
  155. }
  156. $xml = htmlspecialchars($css);
  157. $xml = preg_replace('/\s*,(\r\n|\n)\s*/ms', ', ', $xml);
  158. $xml = preg_replace('/((?:^|\{|\}|;|)\s*)(.*?)\s*\{/m', '$1<s name="$2">', $xml);
  159. $xml = preg_replace('/\}/', '</s>', $xml);
  160. $doc = new DOMDocument;
  161. $doc->loadXML("<xml>{$xml}</xml>");
  162. $css = $this->_decodeNode($doc->firstChild);
  163. $css = htmlspecialchars_decode($css);
  164. array_shift($this->_current);
  165. return $css;
  166. }
  167. protected function _decodeNode($node, $path = null) {
  168. $css = '';
  169. if ($node instanceof DOMText) {
  170. $value = $node->nodeValue;
  171. // @include
  172. if (preg_match_all('/^\s*@include ["\'](.*?)["\'];/m', $value, $m)) {
  173. foreach ($m[1] as $i => $file) {
  174. $file = $this->_fixPath($this->_current[0], $file);
  175. $value = preg_replace('/' . preg_quote($m[0][$i], '/') . '/', $this->_decodeFile($file), $value, 1);
  176. }
  177. }
  178. // $var or make css
  179. if (preg_match_all('/\$([a-zA-Z\-_][\w\-]*)\s*(?::\s*(.*?))?;/', $value, $m)) {
  180. foreach ($m[1] as $i => $key) {
  181. $method = 'css_' . str_replace('-', '_', $key);
  182. if (isset($this->_vars[$key])) {
  183. $args = trim($m[2][$i]) ? preg_split('/\s*,\s*/', $m[2][$i]) : array();
  184. $xml = $this->_vars[$key][1];
  185. foreach ($this->_vars[$key][0] as $j => $arg) {
  186. $arg = isset($args[$j]) && $args[$j] !== 'null' ? $args[$j] : $arg;
  187. $xml = preg_replace('/\$' . ($j + 1) . '/', $arg, $xml);
  188. }
  189. $value = preg_replace('/' . preg_quote($m[0][$i], '/') . '/', $xml, $value, 1);
  190. } else if (method_exists($this, $method)) {
  191. $args = trim($m[2][$i]) ? preg_split('/\s*,\s*/', $m[2][$i]) : array();
  192. $res = call_user_func_array(array($this, $method), $args);
  193. $value = preg_replace('/' . preg_quote($m[0][$i], '/') . '/', $res, $value, 1);
  194. } else {
  195. $value = preg_replace('/' . preg_quote($m[0][$i], '/') . '/', '', $value);
  196. $this->_error('undefined variable: $' . "{$key} in {$this->_current[0]}");
  197. }
  198. }
  199. $doc = new DOMDocument;
  200. $doc->loadXML("<xml>{$value}</xml>");
  201. $node = $doc->firstChild;
  202. } else if ($path) {
  203. $value = trim($value);
  204. $css .= implode(", ", $path) . " {\n" . preg_replace('/^\s*/m', ' ', $value) . "\n}\n";
  205. // foreach ($path as $p) {
  206. // $css[] = $p ? "{$p} {\n" . preg_replace('/^\s*/m', ' ', $value) . "\n}\n" : "{$value}\n";
  207. // }
  208. } else {
  209. $css .= $value;
  210. }
  211. }
  212. if (!($node instanceof DOMElement)) {
  213. return $css;
  214. }
  215. $name = $node->getAttribute('name');
  216. // @media
  217. if ($name && $name[0] === '@') {
  218. $node->setAttribute('name', '');
  219. return "{$name} {\n" . preg_replace('/^/m', ' ', $this->_decodeNode($node)) . "}\n";
  220. }
  221. // $var
  222. if ($key = preg_filter('/.*\$([\w\-]+).*/', '$1', $name)) {
  223. $xml = array();
  224. foreach ($node->childNodes as $cn) {
  225. $xml[] = $cn->C14N();
  226. }
  227. $args = preg_split('/\s*,\s*/', preg_filter('/.*\((.*?)\).*/', '$1', $name));
  228. $this->_vars[$key] = array($args, implode('', $xml));
  229. $name = trim(preg_replace('/(^|,)\s*\$[\w\-]+\s*(\(.*?\))?(,|$)/', '$1', $name), ',');
  230. if (!$name) return $css;
  231. }
  232. foreach ($node->childNodes as $child) {
  233. if (($child instanceof DOMText) && !trim($child->nodeValue)) {
  234. continue;
  235. }
  236. if ($path && $name) {
  237. $next = array();
  238. foreach (preg_split('/\s*,\s*/', $name) as $n) {
  239. foreach ($path as $p) {
  240. $next[] = str_replace(' :', ':', trim("{$p} {$n}"));
  241. }
  242. }
  243. } else if ($path) {
  244. $next = $path;
  245. } else if ($name) {
  246. $next = preg_split('/\s*,\s*/', $name);
  247. } else {
  248. $next = null;
  249. }
  250. $css .= $this->_decodeNode($child, $next);
  251. }
  252. return $css;
  253. }
  254. protected function _fixPath($src, $dst) {
  255. if ($dst[0] === '/') {
  256. return preg_replace('/^' . preg_quote($this->_uri . '/', '/') . '/', '', $dst);
  257. }
  258. $path = $src ? dirname($src) . "/{$dst}" : $dst;
  259. $path = str_replace('/./', '/', $path);
  260. $path = preg_replace('/[^\/]+\/\.\.\//', '', $path);
  261. return $path;
  262. }
  263. protected function _fixUri($path) {
  264. $path = str_replace('/./', '/', $path);
  265. $path = str_replace('//', '/', $path);
  266. $path = preg_replace('/[^\/]+\/\.\.\//', '', $path);
  267. $path = preg_replace('/^\.\//', '', $path);
  268. return $path;
  269. }
  270. protected function _importCallback($m) {
  271. $file = $m[1];
  272. if ($file[0] === '/' || strpos($file, '//') !== false) {
  273. return $m[0];
  274. }
  275. $file = dirname($this->_current[0]) . "/{$file}";
  276. return '@import "'. $this->_fixUri($file) . '";';
  277. }
  278. protected function _urlCallback($m) {
  279. $file = $m[1];
  280. if ($file[0] === '/' || strpos($file, '//') !== false) {
  281. return $m[0];
  282. }
  283. $file = "{$this->_uri}/" . dirname($this->_current[0]) . "/{$file}";
  284. return 'url(' . $this->_fixUri($file) . ')';
  285. }
  286. protected function _error($msg = null) {
  287. $this->_errors[] = $msg;
  288. return false;
  289. }
  290. // ------ vendor prefix ------ //
  291. protected function css_vendor_prefix($name, $value) {
  292. return implode("\n", array(
  293. "{$name}: {$value};",
  294. "-o-{$name}: {$value};",
  295. "-ms-{$name}: {$value};",
  296. "-moz-{$name}: {$value};",
  297. "-webkit-{$name}: {$value};",
  298. ));
  299. }
  300. // ------ background ------ //
  301. protected function css_background_clip($value = 'border') {
  302. $value = str_replace('-box', '', $value);
  303. return implode("\n", array(
  304. "background-clip: {$value}-box;",
  305. "-o-background-clip: {$value}-box;",
  306. "-ms-background-clip: {$value}-box;",
  307. "-moz-background-clip: {$value};",
  308. "-webkit-background-clip: {$value}-box;",
  309. ));
  310. }
  311. protected function css_background_origin($value = 'padding') {
  312. $value = str_replace('-box', '', $value);
  313. return implode("\n", array(
  314. "background-origin: {$value}-box;",
  315. "-o-background-origin: {$value}-box;",
  316. "-ms-background-origin: {$value}-box;",
  317. "-moz-background-origin: {$value};",
  318. "-webkit-background-origin: {$value}-box;",
  319. ));
  320. }
  321. protected function css_background_size($value = 'auto') {
  322. return $this->css_vendor_prefix('background-size', $value);
  323. }
  324. // ------ border ------ //
  325. protected function css_border_radius($value = '0') {
  326. return $this->css_vendor_prefix('border-radius', $value);
  327. }
  328. protected function css_border_top_left_radius($value = '0') {
  329. return $this->css_vendor_prefix('border-top-left-radius', $value);
  330. }
  331. protected function css_border_top_right_radius($value = '0') {
  332. return $this->css_vendor_prefix('border-top-right-radius', $value);
  333. }
  334. protected function css_border_bottom_left_radius($value = '0') {
  335. return $this->css_vendor_prefix('border-bottom-left-radius', $value);
  336. }
  337. protected function css_border_bottom_right_radius($value = '0') {
  338. return $this->css_vendor_prefix('border-bottom-right-radius', $value);
  339. }
  340. protected function css_border_image($value = 'none') {
  341. return $this->css_vendor_prefix('border-image', $value);
  342. }
  343. // ------ border ------ //
  344. protected function css_border_image_source($value = 'none') {
  345. return $this->css_vendor_prefix('border_image_source', $value);
  346. }
  347. protected function css_box_shadow($value = 'none') {
  348. return $this->css_vendor_prefix('box-shadow', $value);
  349. }
  350. // ------ transform ------ //
  351. protected function css_transform($value = 'none') {
  352. return $this->css_vendor_prefix('transform', $value);
  353. }
  354. // ------ transition ------ //
  355. protected function css_transition($value = 'none 0 ease 0') {
  356. return $this->css_vendor_prefix('transition', $value);
  357. }
  358. protected function css_transition_property($value = 'none') {
  359. return $this->css_vendor_prefix('transition-property', $value);
  360. }
  361. protected function css_transition_duration($value = '0') {
  362. return $this->css_vendor_prefix('transition-duration', $value);
  363. }
  364. protected function css_transition_timing_function($value = 'ease') {
  365. return $this->css_vendor_prefix('transition-timing-function', $value);
  366. }
  367. protected function css_transition_delay($value = '0') {
  368. return $this->css_vendor_prefix('transition-delay', $value);
  369. }
  370. // ------ animation ------ //
  371. protected function css_animation($value = 'none 0 ease 0 1 normal') {
  372. return $this->css_vendor_prefix('animation', $value);
  373. }
  374. protected function css_animation_name($value = 'none') {
  375. return $this->css_vendor_prefix('animation-name', $value);
  376. }
  377. protected function css_animation_duration($value = '0') {
  378. return $this->css_vendor_prefix('animation-duration', $value);
  379. }
  380. protected function css_animation_timing_function($value = 'ease') {
  381. return $this->css_vendor_prefix('animation-timing-function', $value);
  382. }
  383. protected function css_animation_iteration_count($value = '1') {
  384. return $this->css_vendor_prefix('animation-iteration-count', $value);
  385. }
  386. protected function css_animation_direction($value = 'normal') {
  387. return $this->css_vendor_prefix('animation-direction', $value);
  388. }
  389. protected function css_animation_play_state($value = 'none') {
  390. return $this->css_vendor_prefix('animation-play-state', $value);
  391. }
  392. protected function css_animation_delay($value = '0') {
  393. return $this->css_vendor_prefix('animation-delay', $value);
  394. }
  395. // ------ gradation ------ //
  396. protected function css_linear_gradient($value = 'none') {
  397. $value = implode(', ', func_get_args());
  398. return implode("\n", array(
  399. "background: linear-gradient({$value});",
  400. "background: -o-linear-gradient({$value});",
  401. "background: -ms-linear-gradient({$value});",
  402. "background: -moz-linear-gradient({$value});",
  403. "background: -webkit-linear-gradient({$value});",
  404. ));
  405. }
  406. protected function css_radial_gradient($value = 'none') {
  407. $value = implode(', ', func_get_args());
  408. return implode("\n", array(
  409. "background: radial-gradient({$value});",
  410. "background: -o-radial-gradient({$value});",
  411. "background: -ms-radial-gradient({$value});",
  412. "background: -moz-radial-gradient({$value});",
  413. "background: -webkit-radial-gradient({$value});",
  414. ));
  415. }
  416. //------ box layout ------ //
  417. protected function css_box_orient($value = 'inline-axis') {
  418. return $this->css_vendor_prefix('box-orient', $value);
  419. }
  420. protected function css_box_direction($value = 'normal') {
  421. return $this->css_vendor_prefix('box-direction', $value);
  422. }
  423. protected function css_box_ordinal_group($value = '1') {
  424. return $this->css_vendor_prefix('box-ordinal-group', $value);
  425. }
  426. protected function css_box_align($value = 'stretch') {
  427. return $this->css_vendor_prefix('box-align', $value);
  428. }
  429. protected function css_box_flex($value = '0.0') {
  430. return $this->css_vendor_prefix('box-flex', $value);
  431. }
  432. protected function css_box_flex_group($value = '1') {
  433. return $this->css_vendor_prefix('box-flex-group', $value);
  434. }
  435. protected function css_box_pack($value = 'start') {
  436. return $this->css_vendor_prefix('box-pack', $value);
  437. }
  438. protected function css_box_lines($value = 'single') {
  439. return $this->css_vendor_prefix('box-lines', $value);
  440. }
  441. }