浏览代码

InlineCssLib

euromark 12 年之前
父节点
当前提交
c2949ceb7d
共有 2 个文件被更改,包括 353 次插入0 次删除
  1. 292 0
      Lib/InlineCssLib.php
  2. 61 0
      Test/Case/Lib/InlineCssLibTest.php

+ 292 - 0
Lib/InlineCssLib.php

@@ -0,0 +1,292 @@
+<?php
+
+/**
+ * Wrapper for Inline CSS replacement
+ * Useful for sending HTML emails
+ *
+ * Note: requires vendors CssToInline or emogrifier!
+ *
+ * @author Mark Scherer
+ * @copyright Mark Scherer
+ * @license MIT
+ * 2012-02-19 ms
+ */
+class InlineCssLib {
+
+	const ENGINE_CSS_TO_INLINE = 'cssToInline';
+
+	const ENGINE_EMOGRIFIER = 'emogrifier';
+
+	protected $_defaults = array(
+		'engine' => self::ENGINE_CSS_TO_INLINE,
+		'cleanup' => true,
+		'useInlineStylesBlock' => true,
+		'xhtmlOutput' => false,
+		'removeCss' => true,
+		'debug' => false,
+	);
+
+	public $settings = array();
+
+	/**
+	 * startup
+	 */
+	public function __construct($settings = array()) {
+		$defaults = am($this->_defaults, (array) Configure::read('InlineCss'));
+		$this->settings = array_merge($defaults, $settings);
+		if (!method_exists($this, '_process'.ucfirst($this->settings['engine']))) {
+			throw new InternalErrorException('Engine does not exist');
+		}
+	}
+
+	/**
+	 * @return string Result
+	 * 2012-01-29 ms
+	 */
+	public function process($html, $css = null) {
+		if (($html = trim($html)) === '') {
+			return $html;
+		}
+		$method = '_process'.ucfirst($this->settings['engine']);
+		return $this->{$method}($html, $css);
+	}
+
+	/**
+	 * @return string Result
+	 * 2012-01-29 ms
+	 */
+	protected function _processEmogrifier($html, $css) {
+		$css .= $this->_extractAndRemoveCss($html);
+		App::import('Vendor', 'Emogrifier', array('file' => 'emogrifier'.DS.'emogrifier.php'));
+		$Emogrifier = new Emogrifier($html, $css);
+
+		return @$Emogrifier->emogrify();
+	}
+
+	/**
+	 * Process css blocks to inline css
+	 * Also works for html snippets (without <html>)
+	 *
+	 * @return string HTML output
+	 */
+	protected function _processCssToInline($html, $css) {
+		App::import('Vendor', 'CssToInline', array('file' => 'css_to_inline_styles'.DS.'css_to_inline_styles.php'));
+
+		//fix issue with <html> being added
+		$separator = '~~~~~~~~~~~~~~~~~~~~';
+		if (strpos($html, '<html>') === false) {
+			$incomplete = true;
+			$html = $separator . $html . $separator;
+		}
+
+		$CssToInlineStyles = new CSSToInlineStyles($html, $css);
+		if ($this->settings['cleanup']) {
+			$CssToInlineStyles->setCleanup();
+		}
+		if ($this->settings['useInlineStylesBlock']) {
+			$CssToInlineStyles->setUseInlineStylesBlock();
+		}
+		if ($this->settings['removeCss']) {
+			$CssToInlineStyles->setStripOriginalStyleTags();
+		}
+		if ($this->settings['debug']) {
+			CakeLog::write('css', $html);
+		}
+		$html = $CssToInlineStyles->convert($this->settings['xhtmlOutput']);
+		if ($this->settings['removeCss']) {
+			//$html = preg_replace('/\<style(.*)\>(.*)\<\/style\>/i', '', $html);
+			$html = $this->stripOnly($html, array('style', 'script'), true);
+			//CakeLog::write('css', $html);
+		}
+
+		if (!empty($incomplete)) {
+			$html = substr($html, strpos($html, $separator) + 20);
+			$html = substr($html, 0, strpos($html, $separator));
+			$html = trim($html);
+		}
+		return $html;
+	}
+
+	/**
+	 * some reverse function of strip_tags with blacklisting instead of whitelisting
+	 * //maybe move to Tools.Utility/String/Text?
+	 *
+	 * @return string $cleanedStr
+	 * 2012-01-29 ms
+	 */
+	public function stripOnly($str, $tags, $stripContent = false) {
+		$content = '';
+		if(!is_array($tags)) {
+			$tags = (strpos($str, '>') !== false ? explode('>', str_replace('<', '', $tags)) : array($tags));
+			if(end($tags) == '') array_pop($tags);
+		}
+		foreach($tags as $tag) {
+			if ($stripContent)
+				 $content = '(.+</'.$tag.'[^>]*>|)';
+			 $str = preg_replace('#</?'.$tag.'[^>]*>'.$content.'#is', '', $str);
+		}
+		return $str;
+	}
+
+	/**
+	 * _extractAndRemoveCss - extracts any CSS from the rendered view and
+	 * removes it from the $html
+	 *
+	 * @return string
+	 */
+	protected function _extractAndRemoveCss($html) {
+		$css = null;
+
+		$DOM = new DOMDocument;
+		$DOM->loadHTML($html);
+
+		// DOM removal queue
+		$remove_doms = array();
+
+		// catch <link> style sheet content
+		$links = $DOM->getElementsByTagName('link');
+
+		foreach($links as $link) {
+			if ($link->hasAttribute('href') && preg_match("/\.css$/i", $link->getAttribute('href'))) {
+
+				// find the css file and load contents
+				if ($link->hasAttribute('media')) {
+					foreach($this->media_types as $css_link_media) {
+						if (strstr($link->getAttribute('media'), $css_link_media)) {
+							$css .= $this->_findAndLoadCssFile($link->getAttribute('href'))."\n\n";
+							$remove_doms[] = $link;
+						}
+					}
+				} else {
+					$css .= $this->_findAndLoadCssFile($link->getAttribute('href'))."\n\n";
+					$remove_doms[] = $link;
+				}
+			}
+		}
+
+		// Catch embeded <style> and @import CSS content
+		$styles = $DOM->getElementsByTagName('style');
+
+		// Style
+		foreach ($styles as $style) {
+			if ($style->hasAttribute('media')) {
+				foreach($this->media_types as $css_link_media) {
+					if (strstr($style->getAttribute('media'), $css_link_media)) {
+						$css .= $this->_parseInlineCssAndLoadImports($style->nodeValue);
+						$remove_doms[] = $style;
+					}
+				}
+			} else {
+				$css .= $this->_parseInlineCssAndLoadImports($style->nodeValue);
+				$remove_doms[] = $style;
+			}
+		}
+
+		// Remove
+		if ($this->settings['removeCss']) {
+			foreach ($remove_doms as $remove_dom) {
+				try {
+					$remove_dom->parentNode->removeChild($remove_dom);
+				} catch (DOMException $e) {}
+			}
+			$html = $DOM->saveHTML();
+		}
+
+		return $html;
+	}
+
+	/**
+	 * _findAndLoadCssFile - finds the appropriate css file within the CSS path
+	 *
+	 * @param string $cssHref
+	 * @return string Content
+	 */
+	protected function _findAndLoadCssFile($cssHref) {
+		$css_filenames = array_merge($this->_globRecursive(CSS.'*.Css'), $this->_globRecursive(CSS.'*.CSS'), $this->_globRecursive(CSS.'*.css'));
+
+		// Build an array of the ever more path specific $cssHref location
+		$cssHrefs = split(DS, $cssHref);
+		$cssHref_paths = array();
+		for($i=count($cssHrefs)-1; $i>0; $i--) {
+			if(isset($cssHref_paths[count($cssHref_paths)-1])) {
+				$cssHref_paths[] = $cssHrefs[$i].DS.$cssHref_paths[count($cssHref_paths)-1];
+			}
+			else {
+				$cssHref_paths[] = $cssHrefs[$i];
+			}
+		}
+
+		// the longest string match will be the match we are looking for
+		$best_css_filename = null;
+		$best_css_match_length = 0;
+		foreach($css_filenames as $css_filename) {
+			foreach($cssHref_paths as $cssHref_path) {
+				$regex = "/".str_replace('/','\/', str_replace('.', '\.', $cssHref_path))."/";
+				if(preg_match($regex, $css_filename, $match)) {
+					if(strlen($match[0]) > $best_css_match_length) {
+						$best_css_match_length = strlen($match[0]);
+						$best_css_filename = $css_filename;
+					}
+				}
+			}
+		}
+
+		$css = null;
+		if(!empty($best_css_filename) && is_file($best_css_filename)) {
+			$css = file_get_contents($best_css_filename);
+		}
+
+		return $css;
+	}
+
+	/**
+	 * _globRecursive
+	 *
+	 * @param string $pattern
+	 * @param int $flags
+	 * @return array
+	 */
+	protected function _globRecursive($pattern, $flags = 0) {
+
+		$files = glob($pattern, $flags);
+
+		foreach (glob(dirname($pattern) . '/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir) {
+			$files = array_merge($files, $this->_globRecursive($dir . '/' . basename($pattern), $flags));
+		}
+
+		return $files;
+	}
+
+	/**
+	 * _parseInlineCssAndLoadImports
+	 *
+	 * @param string Input
+	 * @return string Result
+	 */
+	protected function _parseInlineCssAndLoadImports($css) {
+		// Remove any <!-- --> comment tags - they are valid in HTML but we probably
+		// don't want to be commenting out CSS
+		$css = str_replace('-->', '', str_replace('<!--', '', $css))."\n\n";
+
+		// Load up the @import CSS if any exists
+		preg_match_all("/\@import.*?url\((.*?)\)/i", $css, $matches);
+
+		if (isset($matches[1]) && is_array($matches[1])) {
+			// First remove the @imports
+			$css = preg_replace("/\@import.*?url\(.*?\).*?;/i", '', $css);
+
+			foreach ($matches[1] as $url) {
+				if (preg_match("/^http/i", $url)) {
+					if ($this->import_external_css) {
+						$css .= file_get_contents($url);
+					}
+				}	else {
+					$css .= $this->_findAndLoadCssFile($url);
+				}
+			}
+		}
+
+		return $css;
+	}
+
+}

+ 61 - 0
Test/Case/Lib/InlineCssLibTest.php

@@ -0,0 +1,61 @@
+<?php
+App::uses('InlineCssLib', 'Tools.Lib');
+App::uses('MyCakeTestCase', 'Tools.TestSuite');
+
+class InlineCssLibTest extends MyCakeTestCase {
+
+	public function setUp() {
+		//Configure::write('InlineCss.engine', 'cssToInline');
+		$this->InlineCss = new InlineCssLib();
+
+		parent::setUp();
+	}
+
+	public function testProcess() {
+		$res = $this->InlineCss->process($this->testHtml);
+		$this->debug($this->testHtml);
+		$this->debug($res);
+	}
+
+	public function testProcessPlainPiece() {
+		$html = 'blabla
+	<style>
+div#container { margin: 1em auto; }
+h1 { font-weight: bold; font-size: 2em; }
+p { margin-bottom: 1em; font-family: sans-serif; text-align: justify; }
+p.small { font-size: 70%; }
+	</style>
+	<div id="container">
+		<h1>Sample Page Title</h1>
+		<p>Bacon ipsum dolor sit amet in cow elit, in t-bone qui meatloaf corned beef aute ullamco minim. Consequat swine short ribs pastrami jerky.</p>
+		<p class="small">Some small note!</p>
+	</div>
+bla';
+
+		$res = $this->InlineCss->process($html);
+		$this->debug($html);
+		$this->debug($res);
+	}
+
+	public $testHtml = '<!doctype html>
+<html lang="en">
+<head>
+	<style>
+body { font: 11px/20px Georgia, "Times New Roman", Times, serif; }
+div#container { margin: 1em auto; }
+h1 { font-weight: bold; font-size: 2em; }
+p { margin-bottom: 1em; font-family: sans-serif; text-align: justify; }
+p.small { font-size: 70%; }
+	</style>
+	<title>Example</title>
+</head>
+<body>
+	<div id="container">
+		<h1>Sample Page Title</h1>
+		<p>Bacon ipsum dolor sit amet in cow elit, in t-bone qui meatloaf corned beef aute ullamco minim. Consequat swine short ribs pastrami jerky.</p>
+		<p class="small">Some small note!</p>
+	</div>
+</body>
+</html>';
+
+}