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

/administrator/components/com_widgetkit/helpers/assetfilter.php

https://bitbucket.org/organicdevelopment/joomla-2.5
PHP | 892 lines | 471 code | 121 blank | 300 comment | 61 complexity | 5c2a5f17bda3e6886051be5a4d074c35 MD5 | raw file
Possible License(s): LGPL-3.0, GPL-2.0, MIT, BSD-3-Clause, LGPL-2.1
  1. <?php
  2. /**
  3. * @package Widgetkit
  4. * @author YOOtheme http://www.yootheme.com
  5. * @copyright Copyright (C) YOOtheme GmbH
  6. * @license http://www.gnu.org/licenses/gpl.html GNU/GPL
  7. */
  8. /*
  9. Class: AssetFilterWidgetkitHelper
  10. Asset filter helper class, to filter assets
  11. */
  12. class AssetFilterWidgetkitHelper extends WidgetkitHelper {
  13. /*
  14. Function: create
  15. Create filter object(s)
  16. Parameters:
  17. $filters - String|Array
  18. Returns:
  19. Mixed
  20. */
  21. public function create($filters = array()) {
  22. $prefix = 'WidgetkitAssetFilter';
  23. // one filter
  24. if (is_string($filters)) {
  25. $class = $prefix.$filters;
  26. return new $class();
  27. }
  28. // multiple filter
  29. $collection = new WidgetkitAssetFilterCollection();
  30. foreach ($filters as $name) {
  31. $class = $prefix.$name;
  32. $collection->add(new $class());
  33. }
  34. return $collection;
  35. }
  36. }
  37. /*
  38. Interface: WidgetkitAssetFilterInterface
  39. Asset filter interface
  40. */
  41. interface WidgetkitAssetFilterInterface {
  42. public function filterLoad($asset);
  43. public function filterContent($asset);
  44. }
  45. /*
  46. Class: WidgetkitAssetFilterCollection
  47. Asset filter collection
  48. */
  49. class WidgetkitAssetFilterCollection implements WidgetkitAssetFilterInterface, Iterator {
  50. protected $filters;
  51. /*
  52. Function: __construct
  53. Class Constructor.
  54. */
  55. public function __construct() {
  56. $this->filters = new SplObjectStorage();
  57. }
  58. /*
  59. Function: filterLoad
  60. On load filter callback
  61. Parameters:
  62. $asset - Object
  63. Returns:
  64. Void
  65. */
  66. public function filterLoad($asset) {
  67. foreach ($this->filters as $filter) {
  68. $filter->filterLoad($asset);
  69. }
  70. }
  71. /*
  72. Function: filterContent
  73. On content filter callback
  74. Parameters:
  75. $asset - Object
  76. Returns:
  77. Void
  78. */
  79. public function filterContent($asset) {
  80. foreach ($this->filters as $filter) {
  81. $filter->filterContent($asset);
  82. }
  83. }
  84. /*
  85. Function: add
  86. Add filter to collection
  87. Parameters:
  88. $filter - Object
  89. Returns:
  90. Void
  91. */
  92. public function add($filter) {
  93. if ($filter instanceof Traversable) {
  94. foreach ($filter as $f) {
  95. $this->add($f);
  96. }
  97. } else {
  98. $this->filters->attach($filter);
  99. }
  100. }
  101. /*
  102. Function: remove
  103. Remove filter from collection
  104. Parameters:
  105. $filter - Object
  106. Returns:
  107. Void
  108. */
  109. public function remove($filter) {
  110. $this->filters->detach($filter);
  111. }
  112. /* Iterator interface implementation */
  113. public function current() {
  114. return $this->filters->current();
  115. }
  116. public function key() {
  117. return $this->filters->key();
  118. }
  119. public function valid() {
  120. return $this->filters->valid();
  121. }
  122. public function next() {
  123. $this->filters->next();
  124. }
  125. public function rewind() {
  126. $this->filters->rewind();
  127. }
  128. }
  129. /*
  130. Class: WidgetkitAssetFilterCSSImportResolver
  131. Stylesheet import resolver, replaces @imports with it's content
  132. */
  133. class WidgetkitAssetFilterCSSImportResolver implements WidgetkitAssetFilterInterface {
  134. /*
  135. Function: filterLoad
  136. On load filter callback
  137. Parameters:
  138. $asset - Object
  139. Returns:
  140. Void
  141. */
  142. public function filterLoad($asset) {
  143. // is file asset?
  144. if (!is_a($asset, 'WidgetkitFileAsset')) {
  145. return;
  146. }
  147. // resolve @import rules
  148. $content = $this->load($asset->getPath(), $asset->getContent());
  149. // move unresolved @import rules to the top
  150. $regexp = '/@import[^;]+;/i';
  151. if (preg_match_all($regexp, $content, $matches)) {
  152. $content = preg_replace($regexp, '', $content);
  153. $content = implode("\n", $matches[0])."\n".$content;
  154. }
  155. $asset->setContent($content);
  156. }
  157. /*
  158. Function: filterContent
  159. On content filter callback
  160. Parameters:
  161. $asset - Object
  162. Returns:
  163. Void
  164. */
  165. public function filterContent($asset) {}
  166. /*
  167. Function: load
  168. Load file and get it's content
  169. Parameters:
  170. $file - String
  171. $content - String
  172. Returns:
  173. String
  174. */
  175. protected function load($file, $content = '') {
  176. static $path;
  177. $oldpath = $path;
  178. if ($path && !strpos($file, '://')) {
  179. $file = realpath($path.'/'.$file);
  180. }
  181. $path = dirname($file);
  182. // get content from file, if not already set
  183. if (!$content && file_exists($file)) {
  184. $content = @file_get_contents($file);
  185. }
  186. // remove multiple charset declarations and resolve @imports to its actual content
  187. if ($content) {
  188. $content = preg_replace('/^@charset\s+[\'"](\S*)\b[\'"];/i', '', $content);
  189. $content = preg_replace_callback('/@import\s*(?:url\(\s*)?[\'"]?(?![a-z]+:)([^\'"\()]+)[\'"]?\s*\)?\s*;/', array($this, '_load'), $content);
  190. }
  191. $path = $oldpath;
  192. return $content;
  193. }
  194. /*
  195. Function: _load
  196. Load file recursively and fix url paths
  197. Parameters:
  198. $matches - Array
  199. Returns:
  200. String
  201. */
  202. protected function _load($matches) {
  203. $filename = $matches[1];
  204. // resolve @import rules recursively
  205. $file = $this->load($matches[1]);
  206. // get file's directory remove '.' if its the current directory
  207. $directory = dirname($matches[1]);
  208. $directory = $directory == '.' ? '' : $directory . '/';
  209. // add directory file's to urls paths
  210. return preg_replace('/url\s*\(([\'"]?)(?![a-z]+:|\/+)/i', 'url(\1' . $directory, $file);
  211. }
  212. }
  213. /*
  214. Class: WidgetkitAssetFilterCSSRewriteURL
  215. Rewrite stylesheet urls, rewrites relative urls to absolute urls
  216. */
  217. class WidgetkitAssetFilterCSSRewriteURL implements WidgetkitAssetFilterInterface {
  218. protected static $path;
  219. /*
  220. Function: filterLoad
  221. On load filter callback
  222. Parameters:
  223. $asset - Object
  224. Returns:
  225. Void
  226. */
  227. public function filterLoad($asset) {
  228. // has url?
  229. if (!$asset->getUrl()) {
  230. return;
  231. }
  232. // set base path
  233. self::$path = dirname($asset->getUrl()).'/';
  234. $asset->setContent(preg_replace_callback('/url\(\s*[\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\s*\)/i', array($this, 'rewrite'), $asset->getContent()));
  235. }
  236. /*
  237. Function: filterContent
  238. On content filter callback
  239. Parameters:
  240. $asset - Object
  241. Returns:
  242. Void
  243. */
  244. public function filterContent($asset) {}
  245. /*
  246. Function: rewrite
  247. Rewrite url callback
  248. Parameters:
  249. $matches - Array
  250. Returns:
  251. String
  252. */
  253. protected function rewrite($matches) {
  254. // prefix with base and remove '../' segments if possible
  255. $path = self::$path.$matches[1];
  256. $last = '';
  257. while ($path != $last) {
  258. $last = $path;
  259. $path = preg_replace('`(^|/)(?!\.\./)([^/]+)/\.\./`', '$1', $path);
  260. }
  261. return 'url("'.$path.'")';
  262. }
  263. }
  264. /*
  265. Class: WidgetkitAssetFilterCSSImageBase64
  266. Replace stylesheets image urls with base64 image strings
  267. */
  268. class WidgetkitAssetFilterCSSImageBase64 implements WidgetkitAssetFilterInterface {
  269. /*
  270. Function: filterLoad
  271. On load filter callback
  272. Parameters:
  273. $asset - Object
  274. Returns:
  275. Void
  276. */
  277. public function filterLoad($asset) {}
  278. /*
  279. Function: filterContent
  280. On content filter callback
  281. Parameters:
  282. $asset - Object
  283. Returns:
  284. Void
  285. */
  286. public function filterContent($asset) {
  287. $images = array();
  288. $content = $asset->getContent();
  289. // get images and the related path
  290. if (preg_match_all('/url\(\s*[\'"]?([^\'"]+)[\'"]?\s*\)/Ui', $asset->getContent(), $matches)) {
  291. foreach ($matches[0] as $i => $url) {
  292. if ($path = realpath($asset['base_path'].'/'.ltrim(preg_replace('/'.preg_quote($asset['base_url'], '/').'/', '', $matches[1][$i], 1), '/'))) {
  293. $images[$url] = $path;
  294. }
  295. }
  296. }
  297. // check if image exists and filesize < 10kb
  298. foreach ($images as $url => $path) {
  299. if (filesize($path) <= 10240 && preg_match('/\.(gif|png|jpg)$/i', $path, $extension)) {
  300. $content = str_replace($url, sprintf('url(data:image/%s;base64,%s)', str_replace('jpg', 'jpeg', strtolower($extension[1])), base64_encode(file_get_contents($path))), $content);
  301. }
  302. }
  303. $asset->setContent($content);
  304. }
  305. }
  306. /*
  307. Class: WidgetkitAssetFilterCSSCompressor
  308. Stylesheet compressor, minifies css
  309. Based on Minify_CSS_Compressor (http://code.google.com/p/minify/, Stephen Clay <steve@mrclay.org>, New BSD License)
  310. */
  311. class WidgetkitAssetFilterCSSCompressor implements WidgetkitAssetFilterInterface {
  312. /**
  313. * @var bool Are we "in" a hack?
  314. *
  315. * I.e. are some browsers targetted until the next comment?
  316. */
  317. protected $_inHack = false;
  318. /**
  319. * Filter callbacks
  320. */
  321. public function filterLoad($asset) {}
  322. public function filterContent($asset) {
  323. $asset->setContent($this->process($asset->getContent()));
  324. }
  325. /**
  326. * Minify a CSS string
  327. *
  328. * @param string $css
  329. *
  330. * @return string
  331. */
  332. public function process($css) {
  333. $css = str_replace("\r\n", "\n", $css);
  334. // preserve empty comment after '>'
  335. // http://www.webdevout.net/css-hacks#in_css-selectors
  336. $css = preg_replace('@>/\\*\\s*\\*/@', '>/*keep*/', $css);
  337. // preserve empty comment between property and value
  338. // http://css-discuss.incutio.com/?page=BoxModelHack
  339. $css = preg_replace('@/\\*\\s*\\*/\\s*:@', '/*keep*/:', $css);
  340. $css = preg_replace('@:\\s*/\\*\\s*\\*/@', ':/*keep*/', $css);
  341. // apply callback to all valid comments (and strip out surrounding ws
  342. $css = preg_replace_callback('@\\s*/\\*([\\s\\S]*?)\\*/\\s*@'
  343. ,array($this, '_commentCB'), $css);
  344. // remove ws around { } and last semicolon in declaration block
  345. $css = preg_replace('/\\s*{\\s*/', '{', $css);
  346. $css = preg_replace('/;?\\s*}\\s*/', '}', $css);
  347. // remove ws surrounding semicolons
  348. $css = preg_replace('/\\s*;\\s*/', ';', $css);
  349. // remove ws around urls
  350. $css = preg_replace('/
  351. url\\( # url(
  352. \\s*
  353. ([^\\)]+?) # 1 = the URL (really just a bunch of non right parenthesis)
  354. \\s*
  355. \\) # )
  356. /x', 'url($1)', $css);
  357. // remove ws between rules and colons
  358. $css = preg_replace('/
  359. \\s*
  360. ([{;]) # 1 = beginning of block or rule separator
  361. \\s*
  362. ([\\*_]?[\\w\\-]+) # 2 = property (and maybe IE filter)
  363. \\s*
  364. :
  365. \\s*
  366. (\\b|[#\'"-]) # 3 = first character of a value
  367. /x', '$1$2:$3', $css);
  368. // remove ws in selectors
  369. $css = preg_replace_callback('/
  370. (?: # non-capture
  371. \\s*
  372. [^~>+,\\s]+ # selector part
  373. \\s*
  374. [,>+~] # combinators
  375. )+
  376. \\s*
  377. [^~>+,\\s]+ # selector part
  378. { # open declaration block
  379. /x'
  380. ,array($this, '_selectorsCB'), $css);
  381. // minimize hex colors
  382. $css = preg_replace('/([^=])#([a-f\\d])\\2([a-f\\d])\\3([a-f\\d])\\4([\\s;\\}])/i'
  383. , '$1#$2$3$4$5', $css);
  384. // remove spaces between font families
  385. $css = preg_replace_callback('/font-family:([^;}]+)([;}])/'
  386. ,array($this, '_fontFamilyCB'), $css);
  387. $css = preg_replace('/@import\\s+url/', '@import url', $css);
  388. // replace any ws involving newlines with a single newline
  389. $css = preg_replace('/[ \\t]*\\n+\\s*/', "\n", $css);
  390. // separate common descendent selectors w/ newlines (to limit line lengths)
  391. $css = preg_replace('/([\\w#\\.\\*]+)\\s+([\\w#\\.\\*]+){/', "$1\n$2{", $css);
  392. // Use newline after 1st numeric value (to limit line lengths).
  393. $css = preg_replace('/
  394. ((?:padding|margin|border|outline):\\d+(?:px|em)?) # 1 = prop : 1st numeric value
  395. \\s+
  396. /x'
  397. ,"$1\n", $css);
  398. // prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/
  399. $css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css);
  400. return trim($css);
  401. }
  402. /**
  403. * Replace what looks like a set of selectors
  404. *
  405. * @param array $m regex matches
  406. *
  407. * @return string
  408. */
  409. protected function _selectorsCB($m) {
  410. // remove ws around the combinators
  411. return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]);
  412. }
  413. /**
  414. * Process a comment and return a replacement
  415. *
  416. * @param array $m regex matches
  417. *
  418. * @return string
  419. */
  420. protected function _commentCB($m) {
  421. $hasSurroundingWs = (trim($m[0]) !== $m[1]);
  422. $m = $m[1];
  423. // $m is the comment content w/o the surrounding tokens,
  424. // but the return value will replace the entire comment.
  425. if ($m === 'keep') {
  426. return '/**/';
  427. }
  428. if ($m === '" "') {
  429. // component of http://tantek.com/CSS/Examples/midpass.html
  430. return '/*" "*/';
  431. }
  432. if (preg_match('@";\\}\\s*\\}/\\*\\s+@', $m)) {
  433. // component of http://tantek.com/CSS/Examples/midpass.html
  434. return '/*";}}/* */';
  435. }
  436. if ($this->_inHack) {
  437. // inversion: feeding only to one browser
  438. if (preg_match('@
  439. ^/ # comment started like /*/
  440. \\s*
  441. (\\S[\\s\\S]+?) # has at least some non-ws content
  442. \\s*
  443. /\\* # ends like /*/ or /**/
  444. @x', $m, $n)) {
  445. // end hack mode after this comment, but preserve the hack and comment content
  446. $this->_inHack = false;
  447. return "/*/{$n[1]}/**/";
  448. }
  449. }
  450. if (substr($m, -1) === '\\') { // comment ends like \*/
  451. // begin hack mode and preserve hack
  452. $this->_inHack = true;
  453. return '/*\\*/';
  454. }
  455. if ($m !== '' && $m[0] === '/') { // comment looks like /*/ foo */
  456. // begin hack mode and preserve hack
  457. $this->_inHack = true;
  458. return '/*/*/';
  459. }
  460. if ($this->_inHack) {
  461. // a regular comment ends hack mode but should be preserved
  462. $this->_inHack = false;
  463. return '/**/';
  464. }
  465. // Issue 107: if there's any surrounding whitespace, it may be important, so
  466. // replace the comment with a single space
  467. return $hasSurroundingWs // remove all other comments
  468. ? ' '
  469. : '';
  470. }
  471. /**
  472. * Process a font-family listing and return a replacement
  473. *
  474. * @param array $m regex matches
  475. *
  476. * @return string
  477. */
  478. protected function _fontFamilyCB($m) {
  479. $m[1] = preg_replace('/
  480. \\s*
  481. (
  482. "[^"]+" # 1 = family in double qutoes
  483. |\'[^\']+\' # or 1 = family in single quotes
  484. |[\\w\\-]+ # or 1 = unquoted family
  485. )
  486. \\s*
  487. /x', '$1', $m[1]);
  488. return 'font-family:' . $m[1] . $m[2];
  489. }
  490. }
  491. /*
  492. Class: WidgetkitAssetFilterJSCompressor
  493. Javascript compressor, minifies javascript
  494. Based on JSMin (http://code.google.com/p/jsmin-php, 2008 Ryan Grove <ryan@wonko.com>, MIT License)
  495. */
  496. class WidgetkitAssetFilterJSCompressor implements WidgetkitAssetFilterInterface {
  497. const ORD_LF = 10;
  498. const ORD_SPACE = 32;
  499. const ACTION_KEEP_A = 1;
  500. const ACTION_DELETE_A = 2;
  501. const ACTION_DELETE_A_B = 3;
  502. protected $a;
  503. protected $b;
  504. protected $input;
  505. protected $inputIndex;
  506. protected $inputLength;
  507. protected $lookAhead;
  508. protected $output;
  509. /**
  510. * Filter callbacks
  511. */
  512. public function filterLoad($asset) {}
  513. public function filterContent($asset) {
  514. $asset->setContent($this->process($asset->getContent()));
  515. }
  516. /**
  517. * Minify a Javascript string
  518. *
  519. * @param string $script
  520. * @return string
  521. */
  522. public function process($script) {
  523. // init vars
  524. $this->a = "\n";
  525. $this->b = '';
  526. $this->input = str_replace("\r\n", "\n", $script);
  527. $this->inputIndex = 0;
  528. $this->inputLength = strlen($this->input);
  529. $this->lookAhead = null;
  530. $this->output = '';
  531. try {
  532. $script = trim($this->min());
  533. } catch (Exception $e) {}
  534. return $script;
  535. }
  536. /**
  537. * Perform minification, return result
  538. */
  539. public function min() {
  540. if ($this->output !== '') { // min already run
  541. return $this->output;
  542. }
  543. $this->action(self::ACTION_DELETE_A_B);
  544. while ($this->a !== null) {
  545. // determine next command
  546. $command = self::ACTION_KEEP_A; // default
  547. if ($this->a === ' ') {
  548. if (! $this->isAlphaNum($this->b)) {
  549. $command = self::ACTION_DELETE_A;
  550. }
  551. } elseif ($this->a === "\n") {
  552. if ($this->b === ' ') {
  553. $command = self::ACTION_DELETE_A_B;
  554. } elseif (false === strpos('{[(+-', $this->b)
  555. && ! $this->isAlphaNum($this->b)) {
  556. $command = self::ACTION_DELETE_A;
  557. }
  558. } elseif (! $this->isAlphaNum($this->a)) {
  559. if ($this->b === ' '
  560. || ($this->b === "\n"
  561. && (false === strpos('}])+-"\'', $this->a)))) {
  562. $command = self::ACTION_DELETE_A_B;
  563. }
  564. }
  565. $this->action($command);
  566. }
  567. $this->output = trim($this->output);
  568. return $this->output;
  569. }
  570. /**
  571. * ACTION_KEEP_A = Output A. Copy B to A. Get the next B.
  572. * ACTION_DELETE_A = Copy B to A. Get the next B.
  573. * ACTION_DELETE_A_B = Get the next B.
  574. */
  575. protected function action($command) {
  576. switch ($command) {
  577. case self::ACTION_KEEP_A:
  578. $this->output .= $this->a;
  579. // fallthrough
  580. case self::ACTION_DELETE_A:
  581. $this->a = $this->b;
  582. if ($this->a === "'" || $this->a === '"') { // string literal
  583. $str = $this->a; // in case needed for exception
  584. while (true) {
  585. $this->output .= $this->a;
  586. $this->a = $this->get();
  587. if ($this->a === $this->b) { // end quote
  588. break;
  589. }
  590. if (ord($this->a) <= self::ORD_LF) {
  591. throw new Exception(
  592. 'Unterminated String: ' . var_export($str, true));
  593. }
  594. $str .= $this->a;
  595. if ($this->a === '\\') {
  596. $this->output .= $this->a;
  597. $this->a = $this->get();
  598. $str .= $this->a;
  599. }
  600. }
  601. }
  602. // fallthrough
  603. case self::ACTION_DELETE_A_B:
  604. $this->b = $this->next();
  605. if ($this->b === '/' && $this->isRegexpLiteral()) { // RegExp literal
  606. $this->output .= $this->a . $this->b;
  607. $pattern = '/'; // in case needed for exception
  608. while (true) {
  609. $this->a = $this->get();
  610. $pattern .= $this->a;
  611. if ($this->a === '/') { // end pattern
  612. break; // while (true)
  613. } elseif ($this->a === '\\') {
  614. $this->output .= $this->a;
  615. $this->a = $this->get();
  616. $pattern .= $this->a;
  617. } elseif (ord($this->a) <= self::ORD_LF) {
  618. throw new Exception(
  619. 'Unterminated RegExp: '. var_export($pattern, true));
  620. }
  621. $this->output .= $this->a;
  622. }
  623. $this->b = $this->next();
  624. }
  625. // end case ACTION_DELETE_A_B
  626. }
  627. }
  628. protected function isRegexpLiteral() {
  629. if (false !== strpos("\n{;(,=:[!&|?", $this->a)) { // we aren't dividing
  630. return true;
  631. }
  632. if (' ' === $this->a) {
  633. $length = strlen($this->output);
  634. if ($length < 2) { // weird edge case
  635. return true;
  636. }
  637. // you can't divide a keyword
  638. if (preg_match('/(?:case|else|in|return|typeof)$/', $this->output, $m)) {
  639. if ($this->output === $m[0]) { // odd but could happen
  640. return true;
  641. }
  642. // make sure it's a keyword, not end of an identifier
  643. $charBeforeKeyword = substr($this->output, $length - strlen($m[0]) - 1, 1);
  644. if (! $this->isAlphaNum($charBeforeKeyword)) {
  645. return true;
  646. }
  647. }
  648. }
  649. return false;
  650. }
  651. /**
  652. * Get next char. Convert ctrl char to space.
  653. */
  654. protected function get() {
  655. $c = $this->lookAhead;
  656. $this->lookAhead = null;
  657. if ($c === null) {
  658. if ($this->inputIndex < $this->inputLength) {
  659. $c = $this->input[$this->inputIndex];
  660. $this->inputIndex += 1;
  661. } else {
  662. return null;
  663. }
  664. }
  665. if ($c === "\r" || $c === "\n") {
  666. return "\n";
  667. }
  668. if (ord($c) < self::ORD_SPACE) { // control char
  669. return ' ';
  670. }
  671. return $c;
  672. }
  673. /**
  674. * Get next char. If is ctrl character, translate to a space or newline.
  675. */
  676. protected function peek() {
  677. $this->lookAhead = $this->get();
  678. return $this->lookAhead;
  679. }
  680. /**
  681. * Is $c a letter, digit, underscore, dollar sign, escape, or non-ASCII?
  682. */
  683. protected function isAlphaNum($c) {
  684. return (preg_match('/^[0-9a-zA-Z_\\$\\\\]$/', $c) || ord($c) > 126);
  685. }
  686. protected function singleLineComment() {
  687. $comment = '';
  688. while (true) {
  689. $get = $this->get();
  690. $comment .= $get;
  691. if (ord($get) <= self::ORD_LF) { // EOL reached
  692. // if IE conditional comment
  693. if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
  694. return "/{$comment}";
  695. }
  696. return $get;
  697. }
  698. }
  699. }
  700. protected function multipleLineComment() {
  701. $this->get();
  702. $comment = '';
  703. while (true) {
  704. $get = $this->get();
  705. if ($get === '*') {
  706. if ($this->peek() === '/') { // end of comment reached
  707. $this->get();
  708. // if comment preserved by YUI Compressor
  709. if (0 === strpos($comment, '!')) {
  710. return "\n/*" . substr($comment, 1) . "*/\n";
  711. }
  712. // if IE conditional comment
  713. if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
  714. return "/*{$comment}*/";
  715. }
  716. return ' ';
  717. }
  718. } elseif ($get === null) {
  719. throw new Exception('Unterminated Comment: ' . var_export('/*' . $comment, true));
  720. }
  721. $comment .= $get;
  722. }
  723. }
  724. /**
  725. * Get the next character, skipping over comments.
  726. * Some comments may be preserved.
  727. */
  728. protected function next() {
  729. $get = $this->get();
  730. if ($get !== '/') {
  731. return $get;
  732. }
  733. switch ($this->peek()) {
  734. case '/': return $this->singleLineComment();
  735. case '*': return $this->multipleLineComment();
  736. default: return $get;
  737. }
  738. }
  739. }