PageRenderTime 61ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 1ms

/lib/csslib.php

https://bitbucket.org/ngmares/moodle
PHP | 4781 lines | 2406 code | 405 blank | 1970 comment | 482 complexity | 81f7fa13994cb0ad9399fcde3ec855bb MD5 | raw file
Possible License(s): LGPL-2.1, AGPL-3.0, MPL-2.0-no-copyleft-exception, GPL-3.0, Apache-2.0, BSD-3-Clause

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

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