Skip to content

Commit

Permalink
Merge pull request #156 from classyllama/task/disable-style-parsing
Browse files Browse the repository at this point in the history
[FEATURE] Allow disabling of "inline style" and "style block" parsing
  • Loading branch information
oliverklee committed May 15, 2015
2 parents 9acfd01 + 36d0dc7 commit ed72bcd
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 27 deletions.
124 changes: 97 additions & 27 deletions Classes/Emogrifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,26 @@ class Emogrifier
*/
private $styleAttributesForNodes = array();

/**
* Determines whether the "style" attributes of tags in the the HTML passed to this class should be preserved.
* If set to false, the value of the style attributes will be discarded.
*
* @var bool
*/
private $isInlineStyleAttributesParsingEnabled = true;

/**
* Determines whether the <style> blocks in the HTML passed to this class should be parsed.
*
* If set to true, the <style> blocks will be removed from the HTML and their contents will be applied to the HTML
* via inline styles.
*
* If set to false, the <style> blocks will be left as they are in the HTML.
*
* @var bool
*/
private $isStyleBlocksParsingEnabled = true;

/**
* This attribute applies to the case where you want to preserve your original text encoding.
*
Expand Down Expand Up @@ -190,33 +210,21 @@ public function emogrify()
if ($nodesWithStyleAttributes !== false) {
/** @var \DOMElement $node */
foreach ($nodesWithStyleAttributes as $node) {
$normalizedOriginalStyle = preg_replace_callback(
'/[A-z\\-]+(?=\\:)/S',
function (array $m) {
return strtolower($m[0]);
},
$node->getAttribute('style')
);

// in order to not overwrite existing style attributes in the HTML, we have to save
// the original HTML styles
$nodePath = $node->getNodePath();
if (!isset($this->styleAttributesForNodes[$nodePath])) {
$this->styleAttributesForNodes[$nodePath] = $this->parseCssDeclarationBlock(
$normalizedOriginalStyle
);
$this->visitedNodes[$nodePath] = $node;
if ($this->isInlineStyleAttributesParsingEnabled) {
$this->normalizeStyleAttributes($node);
} else {
$node->removeAttribute('style');
}

$node->setAttribute('style', $normalizedOriginalStyle);
}
}

// grab any existing style blocks from the html and append them to the existing CSS
// (these blocks should be appended so as to have precedence over conflicting styles in the existing CSS)
$allCss = $this->css;

$allCss .= $this->getCssFromAllStyleNodes($xpath);
if ($this->isStyleBlocksParsingEnabled) {
$allCss .= $this->getCssFromAllStyleNodes($xpath);
}

$cssParts = $this->splitCssAndMediaQuery($allCss);

Expand Down Expand Up @@ -277,14 +285,8 @@ function (array $m) {
}
}

// now iterate through the nodes that contained inline styles in the original HTML
foreach ($this->styleAttributesForNodes as $nodePath => $styleAttributesForNode) {
$node = $this->visitedNodes[$nodePath];
$currentStyleAttributes = $this->parseCssDeclarationBlock($node->getAttribute('style'));
$node->setAttribute(
'style',
$this->generateStyleStringFromDeclarationsArrays($currentStyleAttributes, $styleAttributesForNode)
);
if ($this->isInlineStyleAttributesParsingEnabled) {
$this->fillStyleAttributesWithMergedStyles();
}

// This removes styles from your email that contain display:none.
Expand Down Expand Up @@ -315,6 +317,26 @@ function (array $m) {
}
}

/**
* Disables the parsing of inline styles.
*
* @return void
*/
public function disableInlineStyleAttributesParsing()
{
$this->isInlineStyleAttributesParsingEnabled = false;
}

/**
* Disables the parsing of <style> blocks.
*
* @return void
*/
public function disableStyleBlocksParsing()
{
$this->isStyleBlocksParsingEnabled = false;
}

/**
* Clears all caches.
*
Expand Down Expand Up @@ -396,6 +418,54 @@ public function removeUnprocessableHtmlTag($tagName)
}
}

/**
* Normalizes the value of the "style" attribute and saves it.
*
* @param \DOMElement $node
*
* @return void
*/
private function normalizeStyleAttributes(\DOMElement $node)
{
$normalizedOriginalStyle = preg_replace_callback(
'/[A-z\\-]+(?=\\:)/S',
function (array $m) {
return strtolower($m[0]);
},
$node->getAttribute('style')
);

// in order to not overwrite existing style attributes in the HTML, we
// have to save the original HTML styles
$nodePath = $node->getNodePath();
if (!isset($this->styleAttributesForNodes[$nodePath])) {
$this->styleAttributesForNodes[$nodePath] = $this->parseCssDeclarationBlock($normalizedOriginalStyle);
$this->visitedNodes[$nodePath] = $node;
}

$node->setAttribute('style', $normalizedOriginalStyle);
}

/**
* Merges styles from styles attributes and style nodes and applies them to the attribute nodes
*
* return @void
*/
private function fillStyleAttributesWithMergedStyles()
{
foreach ($this->styleAttributesForNodes as $nodePath => $styleAttributesForNode) {
$node = $this->visitedNodes[$nodePath];
$currentStyleAttributes = $this->parseCssDeclarationBlock($node->getAttribute('style'));
$node->setAttribute(
'style',
$this->generateStyleStringFromDeclarationsArrays(
$currentStyleAttributes,
$styleAttributesForNode
)
);
}
}

/**
* This method merges old or existing name/value array with new name/value array
* and then generates a string of the combined style suitable for placing inline.
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@ After you have set the HTML and CSS, you can call the `emogrify` method to merge

$mergedHtml = $emogrifier->emogrify();

## Options

There are several options that you can set on the Emogrifier object before
calling the `emogrify` method:

* `$emogrifier->disableStyleBlocksParsing()` - By default, Emogrifier will grab
all `<style>` blocks in the HTML and will apply the CSS styles as inline
"style" attributes to the HTML. The `<style>` blocks will then be removed
from the HTML. If you want to disable this functionality so that Emogrifier
leaves these `<style>` blocks in the HTML and does not parse them, you should
use this option.
* `$emogrifier->disableInlineStylesParsing()` - By default, Emogrifier
preserves all of the "style" attributes on tags in the HTML you pass to it.
However if you want to discard all existing inline styles in the HTML before
the CSS is applied, you should use this option.


## Installing with Composer

Expand Down
73 changes: 73 additions & 0 deletions Tests/Unit/EmogrifierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,79 @@ public function emogrifyAppliesCssFromStyleNodes()
);
}

/**
* @test
*/
public function emogrifyWhenDisabledNotAppliesCssFromStyleBlocks()
{
$styleAttributeValue = 'color: #ccc;';
$html = $this->html5DocumentType . self::LF .
'<html><style type="text/css">html {' . $styleAttributeValue . '}</style></html>';
$this->subject->setHtml($html);
$this->subject->disableStyleBlocksParsing();

$this->assertNotContains(
'<html style="' . $styleAttributeValue . '">',
$this->subject->emogrify()
);
}

/**
* @test
*/
public function emogrifyWhenStyleBlocksParsingDisabledKeepInlineStyles()
{
$styleAttributeValue = 'text-align: center;';
$html = $this->html5DocumentType . self::LF .
'<html><head><style type="text/css">p { color: #ccc; }</style></head>'
. '<body><p style="' . $styleAttributeValue . '">paragraph</p></body></html>';
$expected = '<p style="' . $styleAttributeValue . '">';
$this->subject->setHtml($html);
$this->subject->disableStyleBlocksParsing();

$this->assertContains(
$expected,
$this->subject->emogrify()
);
}

/**
* @test
*/
public function emogrifyWhenDisabledNotAppliesCssFromInlineStyles()
{
$styleAttributeValue = 'color: #ccc;';
$html = $this->html5DocumentType . self::LF .
'<html style="' . $styleAttributeValue . '"></html>';
$expected = '<html></html>';
$this->subject->setHtml($html);
$this->subject->disableInlineStyleAttributesParsing();

$this->assertContains(
$expected,
$this->subject->emogrify()
);
}

/**
* @test
*/
public function emogrifyWhenInlineStyleAttributesParsingDisabledKeepStyleBlockStyles()
{
$styleAttributeValue = 'color: #ccc;';
$html = $this->html5DocumentType . self::LF .
'<html><head><style type="text/css">p { ' . $styleAttributeValue . ' }</style></head>'
. '<body><p style="text-align: center;">paragraph</p></body></html>';
$expected = '<p style="' . $styleAttributeValue . '">';
$this->subject->setHtml($html);
$this->subject->disableInlineStyleAttributesParsing();

$this->assertContains(
$expected,
$this->subject->emogrify()
);
}

/**
* @test
*/
Expand Down

0 comments on commit ed72bcd

Please sign in to comment.