Your IP : 216.73.216.43


Current Path : /home/cbequiperp/www/libraries/foxcontact/emogrifier/
Upload File :
Current File : /home/cbequiperp/www/libraries/foxcontact/emogrifier/emogrifier.php

<?php namespace Pelago;


class Emogrifier
{
	const ENCODING = 'UTF-8';
	const CACHE_KEY_CSS = 0;
	const CACHE_KEY_SELECTOR = 1;
	const CACHE_KEY_XPATH = 2;
	const CACHE_KEY_CSS_DECLARATION_BLOCK = 3;
	const INDEX = 0;
	const MULTIPLIER = 1;
	const ID_ATTRIBUTE_MATCHER = '/(\\w+)?\\#([\\w\\-]+)/';
	const CLASS_ATTRIBUTE_MATCHER = '/(\\w+|[\\*\\]])?((\\.[\\w\\-]+)+)/';
	private $html = '';
	private $css = '';
	private $unprocessableHtmlTags = array('wbr');
	private $caches = array(self::CACHE_KEY_CSS => array(), self::CACHE_KEY_SELECTOR => array(), self::CACHE_KEY_XPATH => array(), self::CACHE_KEY_CSS_DECLARATION_BLOCK => array());
	private $visitedNodes = array();
	private $styleAttributesForNodes = array();
	public $preserveEncoding = false;
	
	public function __construct($html = '', $css = '')
	{
		$this->setHtml($html);
		$this->setCss($css);
	}
	
	
	public function __destruct()
	{
		$this->purgeVisitedNodes();
	}
	
	
	public function setHtml($html)
	{
		$this->html = $html;
	}
	
	
	public function setCss($css)
	{
		$this->css = $css;
	}
	
	
	public function emogrify()
	{
		if ($this->html === '')
		{
			throw new \BadMethodCallException('Please set some HTML first before calling emogrify.', 1390393096);
		}
		
		$xmlDocument = $this->createXmlDocument();
		$xpath = new \DOMXPath($xmlDocument);
		$this->clearAllCaches();
		$this->purgeVisitedNodes();
		$nodesWithStyleAttributes = $xpath->query('//*[@style]');
		if ($nodesWithStyleAttributes !== false)
		{
			foreach ($nodesWithStyleAttributes as $node)
			{
				$normalizedOriginalStyle = preg_replace_callback('/[A-z\\-]+(?=\\:)/S', function ($m)
				{
					return strtolower($m[0]);
				}, $node->getAttribute('style'));
				$nodePath = $node->getNodePath();
				if (!isset($this->styleAttributesForNodes[$nodePath]))
				{
					$this->styleAttributesForNodes[$nodePath] = $this->parseCssDeclarationBlock($normalizedOriginalStyle);
					$this->visitedNodes[$nodePath] = $node;
				}
				
				$node->setAttribute('style', $normalizedOriginalStyle);
			}
		
		}
		
		$allCss = $this->css;
		$allCss .= $this->getCssFromAllStyleNodes($xpath);
		$cssParts = $this->splitCssAndMediaQuery($allCss);
		$cssKey = md5($cssParts['css']);
		if (!isset($this->caches[self::CACHE_KEY_CSS][$cssKey]))
		{
			preg_match_all('/(?:^|[\\s^{}]*)([^{]+){([^}]*)}/mis', $cssParts['css'], $matches, PREG_SET_ORDER);
			$allSelectors = array();
			foreach ($matches as $key => $selectorString)
			{
				if (!strlen(trim($selectorString[2])))
				{
					continue;
				}
				
				$selectors = explode(',', $selectorString[1]);
				foreach ($selectors as $selector)
				{
					if (strpos($selector, ':') !== false && !preg_match('/:\\S+\\-(child|type)\\(/i', $selector))
					{
						continue;
					}
					
					$allSelectors[] = array('selector' => trim($selector), 'attributes' => trim($selectorString[2]), 'line' => $key);
				}
			
			}
			
			usort($allSelectors, array($this, 'sortBySelectorPrecedence'));
			$this->caches[self::CACHE_KEY_CSS][$cssKey] = $allSelectors;
		}
		
		foreach ($this->caches[self::CACHE_KEY_CSS][$cssKey] as $value)
		{
			$nodesMatchingCssSelectors = $xpath->query($this->translateCssToXpath($value['selector']));
			foreach ($nodesMatchingCssSelectors as $node)
			{
				if ($node->hasAttribute('style'))
				{
					$oldStyleDeclarations = $this->parseCssDeclarationBlock($node->getAttribute('style'));
				}
				else
				{
					$oldStyleDeclarations = array();
				}
				
				$newStyleDeclarations = $this->parseCssDeclarationBlock($value['attributes']);
				$node->setAttribute('style', $this->generateStyleStringFromDeclarationsArrays($oldStyleDeclarations, $newStyleDeclarations));
			}
		
		}
		
		foreach ($this->styleAttributesForNodes as $nodePath => $styleAttributesForNode)
		{
			$node = $this->visitedNodes[$nodePath];
			$currentStyleAttributes = $this->parseCssDeclarationBlock($node->getAttribute('style'));
			$node->setAttribute('style', $this->generateStyleStringFromDeclarationsArrays($currentStyleAttributes, $styleAttributesForNode));
		}
		
		$nodesWithStyleDisplayNone = $xpath->query('//*[contains(translate(translate(@style," ",""),"NOE","noe"),"display:none")]');
		if ($nodesWithStyleDisplayNone->length > 0)
		{
			foreach ($nodesWithStyleDisplayNone as $node)
			{
				if ($node->parentNode && is_callable(array($node->parentNode, 'removeChild')))
				{
					$node->parentNode->removeChild($node);
				}
			
			}
		
		}
		
		$this->copyCssWithMediaToStyleNode($cssParts, $xmlDocument);
		if ($this->preserveEncoding)
		{
			return mb_convert_encoding($xmlDocument->saveHTML(), self::ENCODING, 'HTML-ENTITIES');
		}
		else
		{
			return $xmlDocument->saveHTML();
		}
	
	}
	
	
	private function clearAllCaches()
	{
		$this->clearCache(self::CACHE_KEY_CSS);
		$this->clearCache(self::CACHE_KEY_SELECTOR);
		$this->clearCache(self::CACHE_KEY_XPATH);
		$this->clearCache(self::CACHE_KEY_CSS_DECLARATION_BLOCK);
	}
	
	
	private function clearCache($key)
	{
		$allowedCacheKeys = array(self::CACHE_KEY_CSS, self::CACHE_KEY_SELECTOR, self::CACHE_KEY_XPATH, self::CACHE_KEY_CSS_DECLARATION_BLOCK);
		if (!in_array($key, $allowedCacheKeys, true))
		{
			throw new \InvalidArgumentException('Invalid cache key: ' . $key, 1391822035);
		}
		
		$this->caches[$key] = array();
	}
	
	
	private function purgeVisitedNodes()
	{
		$this->visitedNodes = array();
		$this->styleAttributesForNodes = array();
	}
	
	
	public function addUnprocessableHtmlTag($tagName)
	{
		$this->unprocessableHtmlTags[] = $tagName;
	}
	
	
	public function removeUnprocessableHtmlTag($tagName)
	{
		$key = array_search($tagName, $this->unprocessableHtmlTags, true);
		if ($key !== false)
		{
			unset($this->unprocessableHtmlTags[$key]);
		}
	
	}
	
	
	private function generateStyleStringFromDeclarationsArrays($oldStyles, $newStyles)
	{
		$combinedStyles = array_merge($oldStyles, $newStyles);
		$style = '';
		foreach ($combinedStyles as $attributeName => $attributeValue)
		{
			$style .= strtolower(trim($attributeName)) . ': ' . trim($attributeValue) . '; ';
		}
		
		return trim($style);
	}
	
	
	public function copyCssWithMediaToStyleNode($cssParts, $xmlDocument)
	{
		if (isset($cssParts['media']) && $cssParts['media'] !== '')
		{
			$this->addStyleElementToDocument($xmlDocument, $cssParts['media']);
		}
	
	}
	
	
	private function getCssFromAllStyleNodes($xpath)
	{
		$styleNodes = $xpath->query('//style');
		if ($styleNodes === false)
		{
			return '';
		}
		
		$css = '';
		foreach ($styleNodes as $styleNode)
		{
			$css .= "\n\n" . $styleNode->nodeValue;
			$styleNode->parentNode->removeChild($styleNode);
		}
		
		return $css;
	}
	
	
	protected function addStyleElementToDocument($document, $css)
	{
		$styleElement = $document->createElement('style', $css);
		$styleAttribute = $document->createAttribute('type');
		$styleAttribute->value = 'text/css';
		$styleElement->appendChild($styleAttribute);
		$head = $this->getOrCreateHeadElement($document);
		$head->appendChild($styleElement);
	}
	
	
	private function getOrCreateHeadElement($document)
	{
		$head = $document->getElementsByTagName('head')->item(0);
		if ($head === null)
		{
			$head = $document->createElement('head');
			$html = $document->getElementsByTagName('html')->item(0);
			$html->insertBefore($head, $document->getElementsByTagName('body')->item(0));
		}
		
		return $head;
	}
	
	
	private function splitCssAndMediaQuery($css)
	{
		$media = '';
		$css = preg_replace_callback('#@media\\s+(?:only\\s)?(?:[\\s{\\(]|screen|all)\\s?[^{]+{.*}\\s*}\\s*#misU', function ($matches) use(&$media)
		{
			$media .= $matches[0];
		}, $css);
		$search = array('/\\/\\*.*\\*\\//sU', '/^\\s*@import\\s[^;]+;/misU', '/^\\s*@media\\s[^{]+{(.*)}\\s*}\\s/misU');
		$replace = array('', '', '');
		$css = preg_replace($search, $replace, $css);
		return array('css' => $css, 'media' => $media);
	}
	
	
	private function createXmlDocument()
	{
		$xmlDocument = new \DOMDocument();
		$xmlDocument->encoding = self::ENCODING;
		$xmlDocument->strictErrorChecking = false;
		$xmlDocument->formatOutput = true;
		$libXmlState = libxml_use_internal_errors(true);
		$xmlDocument->loadHTML($this->getUnifiedHtml());
		libxml_clear_errors();
		libxml_use_internal_errors($libXmlState);
		$xmlDocument->normalizeDocument();
		return $xmlDocument;
	}
	
	
	private function getUnifiedHtml()
	{
		if (!empty($this->unprocessableHtmlTags))
		{
			$unprocessableHtmlTags = implode('|', $this->unprocessableHtmlTags);
			$bodyWithoutUnprocessableTags = preg_replace('/<\\/?(' . $unprocessableHtmlTags . ')[^>]*>/i', '', $this->html);
		}
		else
		{
			$bodyWithoutUnprocessableTags = $this->html;
		}
		
		return mb_convert_encoding($bodyWithoutUnprocessableTags, 'HTML-ENTITIES', self::ENCODING);
	}
	
	
	private function sortBySelectorPrecedence($a, $b)
	{
		$precedenceA = $this->getCssSelectorPrecedence($a['selector']);
		$precedenceB = $this->getCssSelectorPrecedence($b['selector']);
		$precedenceForEquals = $a['line'] < $b['line'] ? -1 : 1;
		$precedenceForNotEquals = $precedenceA < $precedenceB ? -1 : 1;
		return $precedenceA === $precedenceB ? $precedenceForEquals : $precedenceForNotEquals;
	}
	
	
	private function getCssSelectorPrecedence($selector)
	{
		$selectorKey = md5($selector);
		if (!isset($this->caches[self::CACHE_KEY_SELECTOR][$selectorKey]))
		{
			$precedence = 0;
			$value = 100;
			$search = array('\\#', '\\.', '');
			foreach ($search as $s)
			{
				if (trim($selector) === '')
				{
					break;
				}
				
				$number = 0;
				$selector = preg_replace('/' . $s . '\\w+/', '', $selector, -1, $number);
				$precedence += $value * $number;
				$value /= 10;
			}
			
			$this->caches[self::CACHE_KEY_SELECTOR][$selectorKey] = $precedence;
		}
		
		return $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey];
	}
	
	
	private function translateCssToXpath($paramCssSelector)
	{
		$cssSelector = ' ' . $paramCssSelector . ' ';
		$cssSelector = preg_replace_callback('/\\s+\\w+\\s+/', function ($matches)
		{
			return strtolower($matches[0]);
		}, $cssSelector);
		$cssSelector = trim($cssSelector);
		$xpathKey = md5($cssSelector);
		if (!isset($this->caches[self::CACHE_KEY_XPATH][$xpathKey]))
		{
			$search = array('/\\s+>\\s+/', '/\\s+\\+\\s+/', '/\\s+/', '/([^\\/]+):first-child/i', '/([^\\/]+):last-child/i', '/^\\[(\\w+)\\]/', '/(\\w)\\[(\\w+)\\]/', '/(\\w)\\[(\\w+)\\=[\'"]?(\\w+)[\'"]?\\]/');
			$replace = array('/', '/following-sibling::*[1]/self::', '//', '*[1]/self::\\1', '*[last()]/self::\\1', '*[@\\1]', '\\1[@\\2]', '\\1[@\\2="\\3"]');
			$cssSelector = '//' . preg_replace($search, $replace, $cssSelector);
			$cssSelector = preg_replace_callback(self::ID_ATTRIBUTE_MATCHER, array($this, 'matchIdAttributes'), $cssSelector);
			$cssSelector = preg_replace_callback(self::CLASS_ATTRIBUTE_MATCHER, array($this, 'matchClassAttributes'), $cssSelector);
			$cssSelector = preg_replace_callback('/([^\\/]+):nth-child\\(\\s*(odd|even|[+\\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i', array($this, 'translateNthChild'), $cssSelector);
			$cssSelector = preg_replace_callback('/([^\\/]+):nth-of-type\\(\\s*(odd|even|[+\\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i', array($this, 'translateNthOfType'), $cssSelector);
			$this->caches[self::CACHE_KEY_SELECTOR][$xpathKey] = $cssSelector;
		}
		
		return $this->caches[self::CACHE_KEY_SELECTOR][$xpathKey];
	}
	
	
	private function matchIdAttributes($match)
	{
		return (strlen($match[1]) ? $match[1] : '*') . '[@id="' . $match[2] . '"]';
	}
	
	
	private function matchClassAttributes($match)
	{
		return (strlen($match[1]) ? $match[1] : '*') . '[contains(concat(" ",@class," "),concat(" ","' . implode('"," "))][contains(concat(" ",@class," "),concat(" ","', explode('.', substr($match[2], 1))) . '"," "))]';
	}
	
	
	private function translateNthChild($match)
	{
		$result = $this->parseNth($match);
		if (isset($result[self::MULTIPLIER]))
		{
			if ($result[self::MULTIPLIER] < 0)
			{
				$result[self::MULTIPLIER] = abs($result[self::MULTIPLIER]);
				return sprintf('*[(last() - position()) mod %u = %u]/self::%s', $result[self::MULTIPLIER], $result[self::INDEX], $match[1]);
			}
			else
			{
				return sprintf('*[position() mod %u = %u]/self::%s', $result[self::MULTIPLIER], $result[self::INDEX], $match[1]);
			}
		
		}
		else
		{
			return sprintf('*[%u]/self::%s', $result[self::INDEX], $match[1]);
		}
	
	}
	
	
	private function translateNthOfType($match)
	{
		$result = $this->parseNth($match);
		if (isset($result[self::MULTIPLIER]))
		{
			if ($result[self::MULTIPLIER] < 0)
			{
				$result[self::MULTIPLIER] = abs($result[self::MULTIPLIER]);
				return sprintf('%s[(last() - position()) mod %u = %u]', $match[1], $result[self::MULTIPLIER], $result[self::INDEX]);
			}
			else
			{
				return sprintf('%s[position() mod %u = %u]', $match[1], $result[self::MULTIPLIER], $result[self::INDEX]);
			}
		
		}
		else
		{
			return sprintf('%s[%u]', $match[1], $result[self::INDEX]);
		}
	
	}
	
	
	private function parseNth($match)
	{
		if (in_array(strtolower($match[2]), array('even', 'odd'), true))
		{
			$index = strtolower($match[2]) === 'even' ? 0 : 1;
			return array(self::MULTIPLIER => 2, self::INDEX => $index);
		}
		elseif (stripos($match[2], 'n') === false)
		{
			$index = (int) str_replace(' ', '', $match[2]);
			return array(self::INDEX => $index);
		}
		else
		{
			if (isset($match[3]))
			{
				$multipleTerm = str_replace($match[3], '', $match[2]);
				$index = (int) str_replace(' ', '', $match[3]);
			}
			else
			{
				$multipleTerm = $match[2];
				$index = 0;
			}
			
			$multiplier = (int) str_ireplace('n', '', $multipleTerm);
			if (!strlen($multiplier))
			{
				$multiplier = 1;
			}
			elseif ($multiplier === 0)
			{
				return array(self::INDEX => $index);
			}
			else
			{
				$multiplier = (int) $multiplier;
			}
			
			while ($index < 0)
			{
				$index += abs($multiplier);
			}
			
			return array(self::MULTIPLIER => $multiplier, self::INDEX => $index);
		}
	
	}
	
	
	private function parseCssDeclarationBlock($cssDeclarationBlock)
	{
		if (isset($this->caches[self::CACHE_KEY_CSS_DECLARATION_BLOCK][$cssDeclarationBlock]))
		{
			return $this->caches[self::CACHE_KEY_CSS_DECLARATION_BLOCK][$cssDeclarationBlock];
		}
		
		$properties = array();
		$declarations = explode(';', $cssDeclarationBlock);
		foreach ($declarations as $declaration)
		{
			$matches = array();
			if (!preg_match('/ *([A-Za-z\\-]+) *: *([^;]+) */', $declaration, $matches))
			{
				continue;
			}
			
			$propertyName = strtolower($matches[1]);
			$propertyValue = $matches[2];
			$properties[$propertyName] = $propertyValue;
		}
		
		$this->caches[self::CACHE_KEY_CSS_DECLARATION_BLOCK][$cssDeclarationBlock] = $properties;
		return $properties;
	}

}