PageRenderTime 54ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/csslib.php

https://bitbucket.org/synergylearning/campusconnect
PHP | 4999 lines | 2531 code | 408 blank | 2060 comment | 513 complexity | 7bb012abb03559126e97ab4ff43083e8 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, LGPL-2.1, Apache-2.0, BSD-3-Clause, AGPL-3.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * This file contains CSS related class, and function for the CSS optimiser
  18. *
  19. * Please see the {@link css_optimiser} class for greater detail.
  20. *
  21. * @package core
  22. * @subpackage cssoptimiser
  23. * @copyright 2012 Sam Hemelryk
  24. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  25. */
  26. defined('MOODLE_INTERNAL') || die();
  27. /**
  28. * Stores CSS in a file at the given path.
  29. *
  30. * This function either succeeds or throws an exception.
  31. *
  32. * @param theme_config $theme The theme that the CSS belongs to.
  33. * @param string $csspath The path to store the CSS at.
  34. * @param array $cssfiles The CSS files to store.
  35. * @param bool $chunk If set to true these files will be chunked to ensure
  36. * that no one file contains more than 4095 selectors.
  37. * @param string $chunkurl If the CSS is be chunked then we need to know the URL
  38. * to use for the chunked files.
  39. */
  40. function css_store_css(theme_config $theme, $csspath, array $cssfiles, $chunk = false, $chunkurl = null) {
  41. global $CFG;
  42. $css = '';
  43. foreach ($cssfiles as $file) {
  44. $css .= file_get_contents($file)."\n";
  45. }
  46. // Check if both the CSS optimiser is enabled and the theme supports it.
  47. if (!empty($CFG->enablecssoptimiser) && $theme->supportscssoptimisation) {
  48. // This is an experimental feature introduced in Moodle 2.3
  49. // The CSS optimiser organises the CSS in order to reduce the overall number
  50. // of rules and styles being sent to the client. It does this by collating
  51. // the CSS before it is cached removing excess styles and rules and stripping
  52. // out any extraneous content such as comments and empty rules.
  53. $optimiser = new css_optimiser;
  54. $css = $theme->post_process($css);
  55. $css = $optimiser->process($css);
  56. // If cssoptimisestats is set then stats from the optimisation are collected
  57. // and output at the beginning of the CSS.
  58. if (!empty($CFG->cssoptimiserstats)) {
  59. $css = $optimiser->output_stats_css().$css;
  60. }
  61. } else {
  62. // This is the default behaviour.
  63. // The cssoptimise setting was introduced in Moodle 2.3 and will hopefully
  64. // in the future be changed from an experimental setting to the default.
  65. // The css_minify_css will method will use the Minify library remove
  66. // comments, additional whitespace and other minor measures to reduce the
  67. // the overall CSS being sent.
  68. // However it has the distinct disadvantage of having to minify the CSS
  69. // before running the post process functions. Potentially things may break
  70. // here if theme designers try to push things with CSS post processing.
  71. $css = $theme->post_process($css);
  72. $css = core_minify::css($css);
  73. }
  74. clearstatcache();
  75. if (!file_exists(dirname($csspath))) {
  76. @mkdir(dirname($csspath), $CFG->directorypermissions, true);
  77. }
  78. // Prevent serving of incomplete file from concurrent request,
  79. // the rename() should be more atomic than fwrite().
  80. ignore_user_abort(true);
  81. // First up write out the single file for all those using decent browsers.
  82. css_write_file($csspath, $css);
  83. if ($chunk) {
  84. // If we need to chunk the CSS for browsers that are sub-par.
  85. $css = css_chunk_by_selector_count($css, $chunkurl);
  86. $files = count($css);
  87. $count = 1;
  88. foreach ($css as $content) {
  89. if ($count === $files) {
  90. // If there is more than one file and this IS the last file.
  91. $filename = preg_replace('#\.css$#', '.0.css', $csspath);
  92. } else {
  93. // If there is more than one file and this is not the last file.
  94. $filename = preg_replace('#\.css$#', '.'.$count.'.css', $csspath);
  95. }
  96. $count++;
  97. css_write_file($filename, $content);
  98. }
  99. }
  100. ignore_user_abort(false);
  101. if (connection_aborted()) {
  102. die;
  103. }
  104. }
  105. /**
  106. * Writes a CSS file.
  107. *
  108. * @param string $filename
  109. * @param string $content
  110. */
  111. function css_write_file($filename, $content) {
  112. global $CFG;
  113. if ($fp = fopen($filename.'.tmp', 'xb')) {
  114. fwrite($fp, $content);
  115. fclose($fp);
  116. rename($filename.'.tmp', $filename);
  117. @chmod($filename, $CFG->filepermissions);
  118. @unlink($filename.'.tmp'); // Just in case anything fails.
  119. }
  120. }
  121. /**
  122. * Takes CSS and chunks it if the number of selectors within it exceeds $maxselectors.
  123. *
  124. * The chunking will not split a group of selectors, or a media query. That means that
  125. * if n > $maxselectors and there are n selectors grouped together,
  126. * they will not be chunked and you could end up with more selectors than desired.
  127. * The same applies for a media query that has more than n selectors.
  128. *
  129. * Also, as we do not split group of selectors or media queries, the chunking might
  130. * not be as optimal as it could be, having files with less selectors than it could
  131. * potentially contain.
  132. *
  133. * String functions used here are not compliant with unicode characters. But that is
  134. * not an issue as the syntax of CSS is using ASCII codes. Even if we have unicode
  135. * characters in comments, or in the property 'content: ""', it will behave correcly.
  136. *
  137. * Please note that this strips out the comments if chunking happens.
  138. *
  139. * @param string $css The CSS to chunk.
  140. * @param string $importurl The URL to use for import statements.
  141. * @param int $maxselectors The number of selectors to limit a chunk to.
  142. * @param int $buffer Not used any more.
  143. * @return array An array of CSS chunks.
  144. */
  145. function css_chunk_by_selector_count($css, $importurl, $maxselectors = 4095, $buffer = 50) {
  146. // Check if we need to chunk this CSS file.
  147. $count = substr_count($css, ',') + substr_count($css, '{');
  148. if ($count < $maxselectors) {
  149. // The number of selectors is less then the max - we're fine.
  150. return array($css);
  151. }
  152. $chunks = array(); // The final chunks.
  153. $offsets = array(); // The indexes to chunk at.
  154. $offset = 0; // The current offset.
  155. $selectorcount = 0; // The number of selectors since the last split.
  156. $lastvalidoffset = 0; // The last valid index to split at.
  157. $lastvalidoffsetselectorcount = 0; // The number of selectors used at the time were could split.
  158. $inrule = 0; // The number of rules we are in, should not be greater than 1.
  159. $inmedia = false; // Whether or not we are in a media query.
  160. $mediacoming = false; // Whether or not we are expeting a media query.
  161. $currentoffseterror = null; // Not null when we have recorded an error for the current split.
  162. $offseterrors = array(); // The offsets where we found errors.
  163. // Remove the comments. Because it's easier, safer and probably a lot of other good reasons.
  164. $css = preg_replace('#/\*(.*?)\*/#s', '', $css);
  165. $strlen = strlen($css);
  166. // Walk through the CSS content character by character.
  167. for ($i = 1; $i <= $strlen; $i++) {
  168. $char = $css[$i - 1];
  169. $offset = $i;
  170. // Is that a media query that I see coming towards us?
  171. if ($char === '@') {
  172. if (!$inmedia && substr($css, $offset, 5) === 'media') {
  173. $mediacoming = true;
  174. }
  175. }
  176. // So we are entering a rule or a media query...
  177. if ($char === '{') {
  178. if ($mediacoming) {
  179. $inmedia = true;
  180. $mediacoming = false;
  181. } else {
  182. $inrule++;
  183. $selectorcount++;
  184. }
  185. }
  186. // Let's count the number of selectors, but only if we are not in a rule, or in
  187. // the definition of a media query, as they can contain commas too.
  188. if (!$mediacoming && !$inrule && $char === ',') {
  189. $selectorcount++;
  190. }
  191. // We reached the end of something.
  192. if ($char === '}') {
  193. // Oh, we are in a media query.
  194. if ($inmedia) {
  195. if (!$inrule) {
  196. // This is the end of the media query.
  197. $inmedia = false;
  198. } else {
  199. // We were in a rule, in the media query.
  200. $inrule--;
  201. }
  202. } else {
  203. $inrule--;
  204. // Handle stupid broken CSS where there are too many } brackets,
  205. // as this can cause it to break (with chunking) where it would
  206. // coincidentally have worked otherwise.
  207. if ($inrule < 0) {
  208. $inrule = 0;
  209. }
  210. }
  211. // We are not in a media query, and there is no pending rule, it is safe to split here.
  212. if (!$inmedia && !$inrule) {
  213. $lastvalidoffset = $offset;
  214. $lastvalidoffsetselectorcount = $selectorcount;
  215. }
  216. }
  217. // Alright, this is splitting time...
  218. if ($selectorcount > $maxselectors) {
  219. if (!$lastvalidoffset) {
  220. // We must have reached more selectors into one set than we were allowed. That means that either
  221. // the chunk size value is too small, or that we have a gigantic group of selectors, or that a media
  222. // query contains more selectors than the chunk size. We have to ignore this because we do not
  223. // support split inside a group of selectors or media query.
  224. if ($currentoffseterror === null) {
  225. $currentoffseterror = $offset;
  226. $offseterrors[] = $currentoffseterror;
  227. }
  228. } else {
  229. // We identify the offset to split at and reset the number of selectors found from there.
  230. $offsets[] = $lastvalidoffset;
  231. $selectorcount = $selectorcount - $lastvalidoffsetselectorcount;
  232. $lastvalidoffset = 0;
  233. $currentoffseterror = null;
  234. }
  235. }
  236. }
  237. // Report offset errors.
  238. if (!empty($offseterrors)) {
  239. debugging('Could not find a safe place to split at offset(s): ' . implode(', ', $offseterrors) . '. Those were ignored.',
  240. DEBUG_DEVELOPER);
  241. }
  242. // Now that we have got the offets, we can chunk the CSS.
  243. $offsetcount = count($offsets);
  244. foreach ($offsets as $key => $index) {
  245. $start = 0;
  246. if ($key > 0) {
  247. $start = $offsets[$key - 1];
  248. }
  249. // From somewhere up to the offset.
  250. $chunks[] = substr($css, $start, $index - $start);
  251. }
  252. // Add the last chunk (if there is one), from the last offset to the end of the string.
  253. if (end($offsets) != $strlen) {
  254. $chunks[] = substr($css, end($offsets));
  255. }
  256. // The array $chunks now contains CSS split into perfect sized chunks.
  257. // Import statements can only appear at the very top of a CSS file.
  258. // Imported sheets are applied in the the order they are imported and
  259. // are followed by the contents of the CSS.
  260. // This is terrible for performance.
  261. // It means we must put the import statements at the top of the last chunk
  262. // to ensure that things are always applied in the correct order.
  263. // This way the chunked files are included in the order they were chunked
  264. // followed by the contents of the final chunk in the actual sheet.
  265. $importcss = '';
  266. $slashargs = strpos($importurl, '.php?') === false;
  267. $parts = count($chunks);
  268. for ($i = 1; $i < $parts; $i++) {
  269. if ($slashargs) {
  270. $importcss .= "@import url({$importurl}/chunk{$i});\n";
  271. } else {
  272. $importcss .= "@import url({$importurl}&chunk={$i});\n";
  273. }
  274. }
  275. $importcss .= end($chunks);
  276. $chunks[key($chunks)] = $importcss;
  277. return $chunks;
  278. }
  279. /**
  280. * Sends a cached CSS file
  281. *
  282. * This function sends the cached CSS file. Remember it is generated on the first
  283. * request, then optimised/minified, and finally cached for serving.
  284. *
  285. * @param string $csspath The path to the CSS file we want to serve.
  286. * @param string $etag The revision to make sure we utilise any caches.
  287. */
  288. function css_send_cached_css($csspath, $etag) {
  289. // 60 days only - the revision may get incremented quite often.
  290. $lifetime = 60*60*24*60;
  291. header('Etag: "'.$etag.'"');
  292. header('Content-Disposition: inline; filename="styles.php"');
  293. header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($csspath)) .' GMT');
  294. header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
  295. header('Pragma: ');
  296. header('Cache-Control: public, max-age='.$lifetime);
  297. header('Accept-Ranges: none');
  298. header('Content-Type: text/css; charset=utf-8');
  299. if (!min_enable_zlib_compression()) {
  300. header('Content-Length: '.filesize($csspath));
  301. }
  302. readfile($csspath);
  303. die;
  304. }
  305. /**
  306. * Sends CSS directly without caching it.
  307. *
  308. * This function takes a raw CSS string, optimises it if required, and then
  309. * serves it.
  310. * Turning both themedesignermode and CSS optimiser on at the same time is awful
  311. * for performance because of the optimiser running here. However it was done so
  312. * that theme designers could utilise the optimised output during development to
  313. * help them optimise their CSS... not that they should write lazy CSS.
  314. *
  315. * @param string $css
  316. */
  317. function css_send_uncached_css($css) {
  318. header('Content-Disposition: inline; filename="styles_debug.php"');
  319. header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
  320. header('Expires: '. gmdate('D, d M Y H:i:s', time() + THEME_DESIGNER_CACHE_LIFETIME) .' GMT');
  321. header('Pragma: ');
  322. header('Accept-Ranges: none');
  323. header('Content-Type: text/css; charset=utf-8');
  324. if (is_array($css)) {
  325. $css = implode("\n\n", $css);
  326. }
  327. echo $css;
  328. die;
  329. }
  330. /**
  331. * Send file not modified headers
  332. *
  333. * @param int $lastmodified
  334. * @param string $etag
  335. */
  336. function css_send_unmodified($lastmodified, $etag) {
  337. // 60 days only - the revision may get incremented quite often.
  338. $lifetime = 60*60*24*60;
  339. header('HTTP/1.1 304 Not Modified');
  340. header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
  341. header('Cache-Control: public, max-age='.$lifetime);
  342. header('Content-Type: text/css; charset=utf-8');
  343. header('Etag: "'.$etag.'"');
  344. if ($lastmodified) {
  345. header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT');
  346. }
  347. die;
  348. }
  349. /**
  350. * Sends a 404 message about CSS not being found.
  351. */
  352. function css_send_css_not_found() {
  353. header('HTTP/1.0 404 not found');
  354. die('CSS was not found, sorry.');
  355. }
  356. /**
  357. * Determines if the given value is a valid CSS colour.
  358. *
  359. * A CSS colour can be one of the following:
  360. * - Hex colour: #AA66BB
  361. * - RGB colour: rgb(0-255, 0-255, 0-255)
  362. * - RGBA colour: rgba(0-255, 0-255, 0-255, 0-1)
  363. * - HSL colour: hsl(0-360, 0-100%, 0-100%)
  364. * - HSLA colour: hsla(0-360, 0-100%, 0-100%, 0-1)
  365. *
  366. * Or a recognised browser colour mapping {@link css_optimiser::$htmlcolours}
  367. *
  368. * @param string $value The colour value to check
  369. * @return bool
  370. */
  371. function css_is_colour($value) {
  372. $value = trim($value);
  373. $hex = '/^#([a-fA-F0-9]{1,3}|[a-fA-F0-9]{6})$/';
  374. $rgb = '#^rgb\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$#i';
  375. $rgba = '#^rgba\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1}(\.\d+)?)\s*\)$#i';
  376. $hsl = '#^hsl\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\%\s*,\s*(\d{1,3})\%\s*\)$#i';
  377. $hsla = '#^hsla\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\%\s*,\s*(\d{1,3})\%\s*,\s*(\d{1}(\.\d+)?)\s*\)$#i';
  378. if (in_array(strtolower($value), array('inherit'))) {
  379. return true;
  380. } else if (preg_match($hex, $value)) {
  381. return true;
  382. } else if (in_array(strtolower($value), array_keys(css_optimiser::$htmlcolours))) {
  383. return true;
  384. } else if (preg_match($rgb, $value, $m) && $m[1] < 256 && $m[2] < 256 && $m[3] < 256) {
  385. // It is an RGB colour.
  386. return true;
  387. } else if (preg_match($rgba, $value, $m) && $m[1] < 256 && $m[2] < 256 && $m[3] < 256) {
  388. // It is an RGBA colour.
  389. return true;
  390. } else if (preg_match($hsl, $value, $m) && $m[1] <= 360 && $m[2] <= 100 && $m[3] <= 100) {
  391. // It is an HSL colour.
  392. return true;
  393. } else if (preg_match($hsla, $value, $m) && $m[1] <= 360 && $m[2] <= 100 && $m[3] <= 100) {
  394. // It is an HSLA colour.
  395. return true;
  396. }
  397. // Doesn't look like a colour.
  398. return false;
  399. }
  400. /**
  401. * Returns true is the passed value looks like a CSS width.
  402. * In order to pass this test the value must be purely numerical or end with a
  403. * valid CSS unit term.
  404. *
  405. * @param string|int $value
  406. * @return boolean
  407. */
  408. function css_is_width($value) {
  409. $value = trim($value);
  410. if (in_array(strtolower($value), array('auto', 'inherit'))) {
  411. return true;
  412. }
  413. if ((string)$value === '0' || preg_match('#^(\-\s*)?(\d*\.)?(\d+)\s*(em|px|pt|\%|in|cm|mm|ex|pc)$#i', $value)) {
  414. return true;
  415. }
  416. return false;
  417. }
  418. /**
  419. * A simple sorting function to sort two array values on the number of items they contain
  420. *
  421. * @param array $a
  422. * @param array $b
  423. * @return int
  424. */
  425. function css_sort_by_count(array $a, array $b) {
  426. $a = count($a);
  427. $b = count($b);
  428. if ($a == $b) {
  429. return 0;
  430. }
  431. return ($a > $b) ? -1 : 1;
  432. }
  433. /**
  434. * A basic CSS optimiser that strips out unwanted things and then processes CSS organising and cleaning styles.
  435. *
  436. * This CSS optimiser works by reading through a CSS string one character at a
  437. * time and building an object structure of the CSS.
  438. * As part of that processing styles are expanded out as much as they can be to
  439. * ensure we collect all mappings, at the end of the processing those styles are
  440. * then combined into an optimised form to keep them as short as possible.
  441. *
  442. * @package core
  443. * @subpackage cssoptimiser
  444. * @copyright 2012 Sam Hemelryk
  445. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  446. */
  447. class css_optimiser {
  448. /**
  449. * Used when the processor is about to start processing.
  450. * Processing states. Used internally.
  451. */
  452. const PROCESSING_START = 0;
  453. /**
  454. * Used when the processor is currently processing a selector.
  455. * Processing states. Used internally.
  456. */
  457. const PROCESSING_SELECTORS = 0;
  458. /**
  459. * Used when the processor is currently processing a style.
  460. * Processing states. Used internally.
  461. */
  462. const PROCESSING_STYLES = 1;
  463. /**
  464. * Used when the processor is currently processing a comment.
  465. * Processing states. Used internally.
  466. */
  467. const PROCESSING_COMMENT = 2;
  468. /**
  469. * Used when the processor is currently processing an @ rule.
  470. * Processing states. Used internally.
  471. */
  472. const PROCESSING_ATRULE = 3;
  473. /**
  474. * The raw string length before optimisation.
  475. * Stats variables set during and after processing
  476. * @var int
  477. */
  478. protected $rawstrlen = 0;
  479. /**
  480. * The number of comments that were removed during optimisation.
  481. * Stats variables set during and after processing
  482. * @var int
  483. */
  484. protected $commentsincss = 0;
  485. /**
  486. * The number of rules in the CSS before optimisation.
  487. * Stats variables set during and after processing
  488. * @var int
  489. */
  490. protected $rawrules = 0;
  491. /**
  492. * The number of selectors using in CSS rules before optimisation.
  493. * Stats variables set during and after processing
  494. * @var int
  495. */
  496. protected $rawselectors = 0;
  497. /**
  498. * The string length after optimisation.
  499. * Stats variables set during and after processing
  500. * @var int
  501. */
  502. protected $optimisedstrlen = 0;
  503. /**
  504. * The number of rules after optimisation.
  505. * Stats variables set during and after processing
  506. * @var int
  507. */
  508. protected $optimisedrules = 0;
  509. /**
  510. * The number of selectors used in rules after optimisation.
  511. * Stats variables set during and after processing
  512. * @var int
  513. */
  514. protected $optimisedselectors = 0;
  515. /**
  516. * The start time of the optimisation.
  517. * Stats variables set during and after processing
  518. * @var int
  519. */
  520. protected $timestart = 0;
  521. /**
  522. * The end time of the optimisation.
  523. * Stats variables set during and after processing
  524. * @var int
  525. */
  526. protected $timecomplete = 0;
  527. /**
  528. * Will be set to any errors that may have occured during processing.
  529. * This is updated only at the end of processing NOT during.
  530. *
  531. * @var array
  532. */
  533. protected $errors = array();
  534. /**
  535. * Processes incoming CSS optimising it and then returning it.
  536. *
  537. * @param string $css The raw CSS to optimise
  538. * @return string The optimised CSS
  539. */
  540. public function process($css) {
  541. // Easiest win there is.
  542. $css = trim($css);
  543. $this->reset_stats();
  544. $this->timestart = microtime(true);
  545. $this->rawstrlen = strlen($css);
  546. // Don't try to process files with no content... it just doesn't make sense.
  547. // But we should produce an error for them, an empty CSS file will lead to a
  548. // useless request for those running theme designer mode.
  549. if ($this->rawstrlen === 0) {
  550. $this->errors[] = 'Skipping file as it has no content.';
  551. return '';
  552. }
  553. // First up we need to remove all line breaks - this allows us to instantly
  554. // reduce our processing requirements and as we will process everything
  555. // into a new structure there's really nothing lost.
  556. $css = preg_replace('#\r?\n#', ' ', $css);
  557. // Next remove the comments... no need to them in an optimised world and
  558. // knowing they're all gone allows us to REALLY make our processing simpler.
  559. $css = preg_replace('#/\*(.*?)\*/#m', '', $css, -1, $this->commentsincss);
  560. $medias = array(
  561. 'all' => new css_media()
  562. );
  563. $imports = array();
  564. $charset = false;
  565. // Keyframes are used for CSS animation they will be processed right at the very end.
  566. $keyframes = array();
  567. $currentprocess = self::PROCESSING_START;
  568. $currentrule = css_rule::init();
  569. $currentselector = css_selector::init();
  570. $inquotes = false; // ' or "
  571. $inbraces = false; // {
  572. $inbrackets = false; // [
  573. $inparenthesis = false; // (
  574. /* @var css_media $currentmedia */
  575. $currentmedia = $medias['all'];
  576. $currentatrule = null;
  577. $suspectatrule = false;
  578. $buffer = '';
  579. $char = null;
  580. // Next we are going to iterate over every single character in $css.
  581. // This is why we removed line breaks and comments!
  582. for ($i = 0; $i < $this->rawstrlen; $i++) {
  583. $lastchar = $char;
  584. $char = substr($css, $i, 1);
  585. if ($char == '@' && $buffer == '') {
  586. $suspectatrule = true;
  587. }
  588. switch ($currentprocess) {
  589. // Start processing an @ rule e.g. @media, @page, @keyframes.
  590. case self::PROCESSING_ATRULE:
  591. switch ($char) {
  592. case ';':
  593. if (!$inbraces) {
  594. $buffer .= $char;
  595. if ($currentatrule == 'import') {
  596. $imports[] = $buffer;
  597. $currentprocess = self::PROCESSING_SELECTORS;
  598. } else if ($currentatrule == 'charset') {
  599. $charset = $buffer;
  600. $currentprocess = self::PROCESSING_SELECTORS;
  601. }
  602. }
  603. if ($currentatrule !== 'media') {
  604. $buffer = '';
  605. $currentatrule = false;
  606. }
  607. // Continue 1: The switch processing chars
  608. // Continue 2: The switch processing the state
  609. // Continue 3: The for loop.
  610. continue 3;
  611. case '{':
  612. $regexmediabasic = '#\s*@media\s*([a-zA-Z0-9]+(\s*,\s*[a-zA-Z0-9]+)*)\s*{#';
  613. $regexadvmedia = '#\s*@media\s*([^{]+)#';
  614. $regexkeyframes = '#@((\-moz\-|\-webkit\-|\-ms\-|\-o\-)?keyframes)\s*([^\s]+)#';
  615. if ($currentatrule == 'media' && preg_match($regexmediabasic, $buffer, $matches)) {
  616. // Basic media declaration.
  617. $mediatypes = str_replace(' ', '', $matches[1]);
  618. if (!array_key_exists($mediatypes, $medias)) {
  619. $medias[$mediatypes] = new css_media($mediatypes);
  620. }
  621. $currentmedia = $medias[$mediatypes];
  622. $currentprocess = self::PROCESSING_SELECTORS;
  623. $buffer = '';
  624. } else if ($currentatrule == 'media' && preg_match($regexadvmedia, $buffer, $matches)) {
  625. // Advanced media query declaration http://www.w3.org/TR/css3-mediaqueries/.
  626. $mediatypes = $matches[1];
  627. $hash = md5($mediatypes);
  628. $medias[$hash] = new css_media($mediatypes);
  629. $currentmedia = $medias[$hash];
  630. $currentprocess = self::PROCESSING_SELECTORS;
  631. $buffer = '';
  632. } else if ($currentatrule == 'keyframes' && preg_match($regexkeyframes, $buffer, $matches)) {
  633. // Keyframes declaration, we treat it exactly like a @media declaration except we don't allow
  634. // them to be overridden to ensure we don't mess anything up. (means we keep everything in order).
  635. $keyframefor = $matches[1];
  636. $keyframename = $matches[3];
  637. $keyframe = new css_keyframe($keyframefor, $keyframename);
  638. $keyframes[] = $keyframe;
  639. $currentmedia = $keyframe;
  640. $currentprocess = self::PROCESSING_SELECTORS;
  641. $buffer = '';
  642. }
  643. // Continue 1: The switch processing chars
  644. // Continue 2: The switch processing the state
  645. // Continue 3: The for loop.
  646. continue 3;
  647. }
  648. break;
  649. // Start processing selectors.
  650. case self::PROCESSING_START:
  651. case self::PROCESSING_SELECTORS:
  652. $regexatrule = '#@(media|import|charset|(\-moz\-|\-webkit\-|\-ms\-|\-o\-)?(keyframes))\s*#';
  653. switch ($char) {
  654. case '[':
  655. $inbrackets ++;
  656. $buffer .= $char;
  657. // Continue 1: The switch processing chars
  658. // Continue 2: The switch processing the state
  659. // Continue 3: The for loop.
  660. continue 3;
  661. case ']':
  662. $inbrackets --;
  663. $buffer .= $char;
  664. // Continue 1: The switch processing chars
  665. // Continue 2: The switch processing the state
  666. // Continue 3: The for loop.
  667. continue 3;
  668. case ' ':
  669. if ($inbrackets) {
  670. // Continue 1: The switch processing chars
  671. // Continue 2: The switch processing the state
  672. // Continue 3: The for loop.
  673. continue 3;
  674. }
  675. if (!empty($buffer)) {
  676. // Check for known @ rules.
  677. if ($suspectatrule && preg_match($regexatrule, $buffer, $matches)) {
  678. $currentatrule = (!empty($matches[3]))?$matches[3]:$matches[1];
  679. $currentprocess = self::PROCESSING_ATRULE;
  680. $buffer .= $char;
  681. } else {
  682. $currentselector->add($buffer);
  683. $buffer = '';
  684. }
  685. }
  686. $suspectatrule = false;
  687. // Continue 1: The switch processing chars
  688. // Continue 2: The switch processing the state
  689. // Continue 3: The for loop.
  690. continue 3;
  691. case '{':
  692. if ($inbrackets) {
  693. // Continue 1: The switch processing chars
  694. // Continue 2: The switch processing the state
  695. // Continue 3: The for loop.
  696. continue 3;
  697. }
  698. // Check for known @ rules.
  699. if ($suspectatrule && preg_match($regexatrule, $buffer, $matches)) {
  700. // Ahh we've been in an @rule, lets rewind one and have the @rule case process this.
  701. $currentatrule = (!empty($matches[3]))?$matches[3]:$matches[1];
  702. $currentprocess = self::PROCESSING_ATRULE;
  703. $i--;
  704. $suspectatrule = false;
  705. // Continue 1: The switch processing chars
  706. // Continue 2: The switch processing the state
  707. // Continue 3: The for loop.
  708. continue 3;
  709. }
  710. if ($buffer !== '') {
  711. $currentselector->add($buffer);
  712. }
  713. $currentrule->add_selector($currentselector);
  714. $currentselector = css_selector::init();
  715. $currentprocess = self::PROCESSING_STYLES;
  716. $buffer = '';
  717. // Continue 1: The switch processing chars
  718. // Continue 2: The switch processing the state
  719. // Continue 3: The for loop.
  720. continue 3;
  721. case '}':
  722. if ($inbrackets) {
  723. // Continue 1: The switch processing chars
  724. // Continue 2: The switch processing the state
  725. // Continue 3: The for loop.
  726. continue 3;
  727. }
  728. if ($currentatrule == 'media') {
  729. $currentmedia = $medias['all'];
  730. $currentatrule = false;
  731. $buffer = '';
  732. } else if (strpos($currentatrule, 'keyframes') !== false) {
  733. $currentmedia = $medias['all'];
  734. $currentatrule = false;
  735. $buffer = '';
  736. }
  737. // Continue 1: The switch processing chars
  738. // Continue 2: The switch processing the state
  739. // Continue 3: The for loop.
  740. continue 3;
  741. case ',':
  742. if ($inbrackets) {
  743. // Continue 1: The switch processing chars
  744. // Continue 2: The switch processing the state
  745. // Continue 3: The for loop.
  746. continue 3;
  747. }
  748. $currentselector->add($buffer);
  749. $currentrule->add_selector($currentselector);
  750. $currentselector = css_selector::init();
  751. $buffer = '';
  752. // Continue 1: The switch processing chars
  753. // Continue 2: The switch processing the state
  754. // Continue 3: The for loop.
  755. continue 3;
  756. }
  757. break;
  758. // Start processing styles.
  759. case self::PROCESSING_STYLES:
  760. if ($char == '"' || $char == "'") {
  761. if ($inquotes === false) {
  762. $inquotes = $char;
  763. }
  764. if ($inquotes === $char && $lastchar !== '\\') {
  765. $inquotes = false;
  766. }
  767. }
  768. if ($inquotes) {
  769. $buffer .= $char;
  770. continue 2;
  771. }
  772. switch ($char) {
  773. case ';':
  774. if ($inparenthesis) {
  775. $buffer .= $char;
  776. // Continue 1: The switch processing chars
  777. // Continue 2: The switch processing the state
  778. // Continue 3: The for loop.
  779. continue 3;
  780. }
  781. $currentrule->add_style($buffer);
  782. $buffer = '';
  783. $inquotes = false;
  784. // Continue 1: The switch processing chars
  785. // Continue 2: The switch processing the state
  786. // Continue 3: The for loop.
  787. continue 3;
  788. case '}':
  789. $currentrule->add_style($buffer);
  790. $this->rawselectors += $currentrule->get_selector_count();
  791. $currentmedia->add_rule($currentrule);
  792. $currentrule = css_rule::init();
  793. $currentprocess = self::PROCESSING_SELECTORS;
  794. $this->rawrules++;
  795. $buffer = '';
  796. $inquotes = false;
  797. $inparenthesis = false;
  798. // Continue 1: The switch processing chars
  799. // Continue 2: The switch processing the state
  800. // Continue 3: The for loop.
  801. continue 3;
  802. case '(':
  803. $inparenthesis = true;
  804. $buffer .= $char;
  805. // Continue 1: The switch processing chars
  806. // Continue 2: The switch processing the state
  807. // Continue 3: The for loop.
  808. continue 3;
  809. case ')':
  810. $inparenthesis = false;
  811. $buffer .= $char;
  812. // Continue 1: The switch processing chars
  813. // Continue 2: The switch processing the state
  814. // Continue 3: The for loop.
  815. continue 3;
  816. }
  817. break;
  818. }
  819. $buffer .= $char;
  820. }
  821. foreach ($medias as $media) {
  822. $this->optimise($media);
  823. }
  824. $css = $this->produce_css($charset, $imports, $medias, $keyframes);
  825. $this->timecomplete = microtime(true);
  826. return trim($css);
  827. }
  828. /**
  829. * Produces CSS for the given charset, imports, media, and keyframes
  830. * @param string $charset
  831. * @param array $imports
  832. * @param css_media[] $medias
  833. * @param css_keyframe[] $keyframes
  834. * @return string
  835. */
  836. protected function produce_css($charset, array $imports, array $medias, array $keyframes) {
  837. $css = '';
  838. if (!empty($charset)) {
  839. $imports[] = $charset;
  840. }
  841. if (!empty($imports)) {
  842. $css .= implode("\n", $imports);
  843. $css .= "\n\n";
  844. }
  845. $cssreset = array();
  846. $cssstandard = array();
  847. $csskeyframes = array();
  848. // Process each media declaration individually.
  849. foreach ($medias as $media) {
  850. // If this declaration applies to all media types.
  851. if (in_array('all', $media->get_types())) {
  852. // Collect all rules that represet reset rules and remove them from the media object at the same time.
  853. // We do this because we prioritise reset rules to the top of a CSS output. This ensures that they
  854. // can't end up out of order because of optimisation.
  855. $resetrules = $media->get_reset_rules(true);
  856. if (!empty($resetrules)) {
  857. $cssreset[] = css_writer::media('all', $resetrules);
  858. }
  859. }
  860. // Get the standard cSS.
  861. $cssstandard[] = $media->out();
  862. }
  863. // Finally if there are any keyframe declarations process them now.
  864. if (count($keyframes) > 0) {
  865. foreach ($keyframes as $keyframe) {
  866. $this->optimisedrules += $keyframe->count_rules();
  867. $this->optimisedselectors += $keyframe->count_selectors();
  868. if ($keyframe->has_errors()) {
  869. $this->errors += $keyframe->get_errors();
  870. }
  871. $csskeyframes[] = $keyframe->out();
  872. }
  873. }
  874. // Join it all together.
  875. $css .= join('', $cssreset);
  876. $css .= join('', $cssstandard);
  877. $css .= join('', $csskeyframes);
  878. // Record the strlenght of the now optimised CSS.
  879. $this->optimisedstrlen = strlen($css);
  880. // Return the now produced CSS.
  881. return $css;
  882. }
  883. /**
  884. * Optimises the CSS rules within a rule collection of one form or another
  885. *
  886. * @param css_rule_collection $media
  887. * @return void This function acts in reference
  888. */
  889. protected function optimise(css_rule_collection $media) {
  890. $media->organise_rules_by_selectors();
  891. $this->optimisedrules += $media->count_rules();
  892. $this->optimisedselectors += $media->count_selectors();
  893. if ($media->has_errors()) {
  894. $this->errors += $media->get_errors();
  895. }
  896. }
  897. /**
  898. * Returns an array of stats from the last processing run
  899. * @return string
  900. */
  901. public function get_stats() {
  902. $stats = array(
  903. 'timestart' => $this->timestart,
  904. 'timecomplete' => $this->timecomplete,
  905. 'timetaken' => round($this->timecomplete - $this->timestart, 4),
  906. 'commentsincss' => $this->commentsincss,
  907. 'rawstrlen' => $this->rawstrlen,
  908. 'rawselectors' => $this->rawselectors,
  909. 'rawrules' => $this->rawrules,
  910. 'optimisedstrlen' => $this->optimisedstrlen,
  911. 'optimisedrules' => $this->optimisedrules,
  912. 'optimisedselectors' => $this->optimisedselectors,
  913. 'improvementstrlen' => '-',
  914. 'improvementrules' => '-',
  915. 'improvementselectors' => '-',
  916. );
  917. // Avoid division by 0 errors by checking we have valid raw values.
  918. if ($this->rawstrlen > 0) {
  919. $stats['improvementstrlen'] = round(100 - ($this->optimisedstrlen / $this->rawstrlen) * 100, 1).'%';
  920. }
  921. if ($this->rawrules > 0) {
  922. $stats['improvementrules'] = round(100 - ($this->optimisedrules / $this->rawrules) * 100, 1).'%';
  923. }
  924. if ($this->rawselectors > 0) {
  925. $stats['improvementselectors'] = round(100 - ($this->optimisedselectors / $this->rawselectors) * 100, 1).'%';
  926. }
  927. return $stats;
  928. }
  929. /**
  930. * Returns true if any errors have occured during processing
  931. *
  932. * @return bool
  933. */
  934. public function has_errors() {
  935. return !empty($this->errors);
  936. }
  937. /**
  938. * Returns an array of errors that have occured
  939. *
  940. * @param bool $clear If set to true the errors will be cleared after being returned.
  941. * @return array
  942. */
  943. public function get_errors($clear = false) {
  944. $errors = $this->errors;
  945. if ($clear) {
  946. // Reset the error array.
  947. $this->errors = array();
  948. }
  949. return $errors;
  950. }
  951. /**
  952. * Returns any errors as a string that can be included in CSS.
  953. *
  954. * @return string
  955. */
  956. public function output_errors_css() {
  957. $computedcss = "/****************************************\n";
  958. $computedcss .= " *--- Errors found during processing ----\n";
  959. foreach ($this->errors as $error) {
  960. $computedcss .= preg_replace('#^#m', '* ', $error);
  961. }
  962. $computedcss .= " ****************************************/\n\n";
  963. return $computedcss;
  964. }
  965. /**
  966. * Returns a string to display stats about the last generation within CSS output
  967. *
  968. * @return string
  969. */
  970. public function output_stats_css() {
  971. $computedcss = "/****************************************\n";
  972. $computedcss .= " *------- CSS Optimisation stats --------\n";
  973. if ($this->rawstrlen === 0) {
  974. $computedcss .= " File not processed as it has no content /\n\n";
  975. $computedcss .= " ****************************************/\n\n";
  976. return $computedcss;
  977. } else if ($this->rawrules === 0) {
  978. $computedcss .= " File contained no rules to be processed /\n\n";
  979. $computedcss .= " ****************************************/\n\n";
  980. return $computedcss;
  981. }
  982. $stats = $this->get_stats();
  983. $computedcss .= " * ".date('r')."\n";
  984. $computedcss .= " * {$stats['commentsincss']} \t comments removed\n";
  985. $computedcss .= " * Optimisation took {$stats['timetaken']} seconds\n";
  986. $computedcss .= " *--------------- before ----------------\n";
  987. $computedcss .= " * {$stats['rawstrlen']} \t chars read in\n";
  988. $computedcss .= " * {$stats['rawrules']} \t rules read in\n";
  989. $computedcss .= " * {$stats['rawselectors']} \t total selectors\n";
  990. $computedcss .= " *---------------- after ----------------\n";
  991. $computedcss .= " * {$stats['optimisedstrlen']} \t chars once optimized\n";
  992. $computedcss .= " * {$stats['optimisedrules']} \t optimized rules\n";
  993. $computedcss .= " * {$stats['optimisedselectors']} \t total selectors once optimized\n";
  994. $computedcss .= " *---------------- stats ----------------\n";
  995. $computedcss .= " * {$stats['improvementstrlen']} \t reduction in chars\n";
  996. $computedcss .= " * {$stats['improvementrules']} \t reduction in rules\n";
  997. $computedcss .= " * {$stats['improvementselectors']} \t reduction in selectors\n";
  998. $computedcss .= " ****************************************/\n\n";
  999. return $computedcss;
  1000. }
  1001. /**
  1002. * Resets the stats ready for another fresh processing
  1003. */
  1004. public function reset_stats() {
  1005. $this->commentsincss = 0;
  1006. $this->optimisedrules = 0;
  1007. $this->optimisedselectors = 0;
  1008. $this->optimisedstrlen = 0;
  1009. $this->rawrules = 0;
  1010. $this->rawselectors = 0;
  1011. $this->rawstrlen = 0;
  1012. $this->timecomplete = 0;
  1013. $this->timestart = 0;
  1014. }
  1015. /**
  1016. * An array of the common HTML colours that are supported by most browsers.
  1017. *
  1018. * This reference table is used to allow us to unify colours, and will aid
  1019. * us in identifying buggy CSS using unsupported colours.
  1020. *
  1021. * @var string[]
  1022. */
  1023. public static $htmlcolours = array(
  1024. 'aliceblue' => '#F0F8FF',
  1025. 'antiquewhite' => '#FAEBD7',
  1026. 'aqua' => '#00FFFF',
  1027. 'aquamarine' => '#7FFFD4',
  1028. 'azure' => '#F0FFFF',
  1029. 'beige' => '#F5F5DC',
  1030. 'bisque' => '#FFE4C4',
  1031. 'black' => '#000000',
  1032. 'blanchedalmond' => '#FFEBCD',
  1033. 'blue' => '#0000FF',
  1034. 'blueviolet' => '#8A2BE2',
  1035. 'brown' => '#A52A2A',
  1036. 'burlywood' => '#DEB887',
  1037. 'cadetblue' => '#5F9EA0',
  1038. 'chartreuse' => '#7FFF00',
  1039. 'chocolate' => '#D2691E',
  1040. 'coral' => '#FF7F50',
  1041. 'cornflowerblue' => '#6495ED',
  1042. 'cornsilk' => '#FFF8DC',
  1043. 'crimson' => '#DC143C',
  1044. 'cyan' => '#00FFFF',
  1045. 'darkblue' => '#00008B',
  1046. 'darkcyan' => '#008B8B',
  1047. 'darkgoldenrod' => '#B8860B',
  1048. 'darkgray' => '#A9A9A9',
  1049. 'darkgrey' => '#A9A9A9',
  1050. 'darkgreen' => '#006400',
  1051. 'darkKhaki' => '#BDB76B',
  1052. 'darkmagenta' => '#8B008B',
  1053. 'darkolivegreen' => '#556B2F',
  1054. 'arkorange' => '#FF8C00',
  1055. 'darkorchid' => '#9932CC',
  1056. 'darkred' => '#8B0000',
  1057. 'darksalmon' => '#E9967A',
  1058. 'darkseagreen' => '#8FBC8F',
  1059. 'darkslateblue' => '#483D8B',
  1060. 'darkslategray' => '#2F4F4F',
  1061. 'darkslategrey' => '#2F4F4F',
  1062. 'darkturquoise' => '#00CED1',
  1063. 'darkviolet' => '#9400D3',
  1064. 'deeppink' => '#FF1493',
  1065. 'deepskyblue' => '#00BFFF',
  1066. 'dimgray' => '#696969',
  1067. 'dimgrey' => '#696969',
  1068. 'dodgerblue' => '#1E90FF',
  1069. 'firebrick' => '#B22222',
  1070. 'floralwhite' => '#FFFAF0',
  1071. 'forestgreen' => '#228B22',
  1072. 'fuchsia' => '#FF00FF',
  1073. 'gainsboro' => '#DCDCDC',
  1074. 'ghostwhite' => '#F8F8FF',
  1075. 'gold' => '#FFD700',
  1076. 'goldenrod' => '#DAA520',
  1077. 'gray' => '#808080',
  1078. 'grey' => '#808080',
  1079. 'green' => '#008000',
  1080. 'greenyellow' => '#ADFF2F',
  1081. 'honeydew' => '#F0FFF0',
  1082. 'hotpink' => '#FF69B4',
  1083. 'indianred ' => '#CD5C5C',
  1084. 'indigo ' => '#4B0082',
  1085. 'ivory' => '#FFFFF0',
  1086. 'khaki' => '#F0E68C',
  1087. 'lavender' => '#E6E6FA',
  1088. 'lavenderblush' => '#FFF0F5',
  1089. 'lawngreen' => '#7CFC00',
  1090. 'lemonchiffon' => '#FFFACD',
  1091. 'lightblue' => '#ADD8E6',
  1092. 'lightcoral' => '#F08080',
  1093. 'lightcyan' => '#E0FFFF',
  1094. 'lightgoldenrodyellow' => '#FAFAD2',
  1095. 'lightgray' => '#D3D3D3',
  1096. 'lightgrey' => '#D3D3D3',
  1097. 'lightgreen' => '#90EE90',
  1098. 'lightpink' => '#FFB6C1',
  1099. 'lightsalmon' => '#FFA07A',
  1100. 'lightseagreen' => '#20B2AA',
  1101. 'lightskyblue' => '#87CEFA',
  1102. 'lightslategray' => '#778899',
  1103. 'lightslategrey' => '#778899',
  1104. 'lightsteelblue' => '#B0C4DE',
  1105. 'lightyellow' => '#FFFFE0',
  1106. 'lime' => '#00FF00',
  1107. 'limegreen' => '#32CD32',
  1108. 'linen' => '#FAF0E6',
  1109. 'magenta' => '#FF00FF',
  1110. 'maroon' => '#800000',
  1111. 'mediumaquamarine' => '#66CDAA',
  1112. 'mediumblue' => '#0000CD',
  1113. 'mediumorchid' => '#BA55D3',
  1114. 'mediumpurple' => '#9370D8',
  1115. 'mediumseagreen' => '#3CB371',
  1116. 'mediumslateblue' => '#7B68EE',
  1117. 'mediumspringgreen' => '#00FA9A',
  1118. 'mediumturquoise' => '#48D1CC',
  1119. 'mediumvioletred' => '#C71585',
  1120. 'midnightblue' => '#191970',
  1121. 'mintcream' => '#F5FFFA',
  1122. 'mistyrose' => '#FFE4E1',
  1123. 'moccasin' => '#FFE4B5',
  1124. 'navajowhite' => '#FFDEAD',
  1125. 'navy' => '#000080',
  1126. 'oldlace' => '#FDF5E6',
  1127. 'olive' => '#808000',
  1128. 'olivedrab' => '#6B8E23',
  1129. 'orange' => '#FFA500',
  1130. 'orangered' => '#FF4500',
  1131. 'orchid' => '#DA70D6',
  1132. 'palegoldenrod' => '#EEE8AA',
  1133. 'palegreen' => '#98FB98',
  1134. 'paleturquoise' => '#AFEEEE',
  1135. 'palevioletred' => '#D87093',
  1136. 'papayawhip' => '#FFEFD5',
  1137. 'peachpuff' => '#FFDAB9',
  1138. 'peru' => '#CD853F',
  1139. 'pink' => '#FFC0CB',
  1140. 'plum' => '#DDA0DD',
  1141. 'powderblue' => '#B0E0E6',
  1142. 'purple' => '#800080',
  1143. 'red' => '#FF0000',
  1144. 'rosybrown' => '#BC8F8F',
  1145. 'royalblue' => '#4169E1',
  1146. 'saddlebrown' => '#8B4513',
  1147. 'salmon' => '#FA8072',
  1148. 'sandybrown' => '#F4A460',
  1149. 'seagreen' => '#2…

Large files files are truncated, but you can click here to view the full file