Giter Club home page Giter Club logo

emogrifier's Introduction

Emogrifier

Build Status Latest Stable Version Total Downloads License

n. e•mog•ri•fi•er [\ē-'mä-grƏ-,fī-Ər] - a utility for changing completely the nature or appearance of HTML email, esp. in a particularly fantastic or bizarre manner

Emogrifier converts CSS styles into inline style attributes in your HTML code. This ensures proper display on email and mobile device readers that lack stylesheet support.

This utility was developed as part of Intervals to deal with the problems posed by certain email clients (namely Outlook 2007 and GoogleMail) when it comes to the way they handle styling contained in HTML emails. As many web developers and designers already know, certain email clients are notorious for their lack of CSS support. While attempts are being made to develop common email standards, implementation is still a ways off.

The primary problem with uncooperative email clients is that most tend to only regard inline CSS, discarding all <style> elements and links to stylesheets in <link> elements. Emogrifier solves this problem by converting CSS styles into inline style attributes in your HTML code.

How it Works

Emogrifier automagically transmogrifies your HTML by parsing your CSS and inserting your CSS definitions into tags within your HTML based on your CSS selectors.

Installation

For installing emogrifier, either add pelago/emogrifier to the require section in your project's composer.json, or you can use composer as below:

composer require pelago/emogrifier

See https://getcomposer.org/ for more information and documentation.

Usage

Inlining Css

The most basic way to use the CssInliner class is to create an instance with the original HTML, inline the external CSS, and then get back the resulting HTML:

use Pelago\Emogrifier\CssInliner;

…

$visualHtml = CssInliner::fromHtml($html)->inlineCss($css)->render();

If there is no external CSS file and all CSS is located within <style> elements in the HTML, you can omit the $css parameter:

$visualHtml = CssInliner::fromHtml($html)->inlineCss()->render();

If you would like to get back only the content of the <body> element instead of the complete HTML document, you can use the renderBodyContent method instead:

$bodyContent = $visualHtml = CssInliner::fromHtml($html)->inlineCss()
  ->renderBodyContent();

If you would like to modify the inlining process with any of the available options, you will need to call the corresponding methods before inlining the CSS. The code then would look like this:

$visualHtml = CssInliner::fromHtml($html)->disableStyleBlocksParsing()
  ->inlineCss($css)->render();

There are also some other HTML-processing classes available (all of which are subclasses of AbstractHtmlProcessor) which you can use to further change the HTML after inlining the CSS. (For more details on the classes, please have a look at the sections below.) CssInliner and all HTML-processing classes can share the same DOMDocument instance to work on:

use Pelago\Emogrifier\CssInliner;
use Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter;
use Pelago\Emogrifier\HtmlProcessor\HtmlPruner;

…

$cssInliner = CssInliner::fromHtml($html)->inlineCss($css);
$domDocument = $cssInliner->getDomDocument();
HtmlPruner::fromDomDocument($domDocument)->removeElementsWithDisplayNone()
  ->removeRedundantClassesAfterCssInlined($cssInliner);
$finalHtml = CssToAttributeConverter::fromDomDocument($domDocument)
  ->convertCssToVisualAttributes()->render();

Normalizing and cleaning up HTML

The HtmlNormalizer class normalizes the given HTML in the following ways:

  • add a document type (HTML5) if missing
  • disentangle incorrectly nested tags
  • add HEAD and BODY elements (if they are missing)

The class can be used like this:

use Pelago\Emogrifier\HtmlProcessor\HtmlNormalizer;

…

$cleanHtml = HtmlNormalizer::fromHtml($rawHtml)->render();

Converting CSS styles to visual HTML attributes

The CssToAttributeConverter converts a few style attributes values to visual HTML attributes. This allows to get at least a bit of visual styling for email clients that do not support CSS well. For example, style="width: 100px" will be converted to width="100".

The class can be used like this:

use Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter;

…

$visualHtml = CssToAttributeConverter::fromHtml($rawHtml)
  ->convertCssToVisualAttributes()->render();

You can also have the CssToAttributeConverter work on a DOMDocument:

$visualHtml = CssToAttributeConverter::fromDomDocument($domDocument)
  ->convertCssToVisualAttributes()->render();

Removing redundant content and attributes from the HTML

The HtmlPruner class can reduce the size of the HTML by removing elements with a display: none style declaration, and/or removing classes from class attributes that are not required.

It can be used like this:

use Pelago\Emogrifier\HtmlProcessor\HtmlPruner;

…

$prunedHtml = HtmlPruner::fromHtml($html)->removeElementsWithDisplayNone()
  ->removeRedundantClasses($classesToKeep)->render();

The removeRedundantClasses method accepts a whitelist of names of classes that should be retained. If this is a post-processing step after inlining CSS, you can alternatively use removeRedundantClassesAfterCssInlined, passing it the CssInliner instance that has inlined the CSS (and having the HtmlPruner work on the DOMDocument). This will use information from the CssInliner to determine which classes are still required (namely, those used in uninlinable rules that have been copied to a <style> element):

$prunedHtml = HtmlPruner::fromDomDocument($cssInliner->getDomDocument())
  ->removeElementsWithDisplayNone()
  ->removeRedundantClassesAfterCssInlined($cssInliner)->render();

The removeElementsWithDisplayNone method will not remove any elements which have the class -emogrifier-keep. So if, for example, there are elements which by default have display: none but are revealed by an @media rule, or which are intended as a preheader, you can add that class to those elements. The paragraph in this HTML snippet will not be removed even though it has display: none (which has presumably been applied by CssInliner::inlineCss() from a CSS rule .preheader { display: none; }):

<p class="preheader -emogrifier-keep" style="display: none;">
  Hello World!
</p>

The removeRedundantClassesAfterCssInlined (or removeRedundantClasses) method, if invoked after removeElementsWithDisplayNone, will remove the -emogrifier-keep class.

Options

There are several options that you can set on the CssInliner instance before calling the inlineCss method:

  • ->disableStyleBlocksParsing() - By default, CssInliner 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 CssInliner leaves these <style> blocks in the HTML and does not parse them, you should use this option. If you use this option, the contents of the <style> blocks will not be applied as inline styles and any CSS you want CssInliner to use must be passed in as described in the Usage section above.
  • ->disableInlineStyleAttributesParsing() - By default, CssInliner 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.
  • ->addAllowedMediaType(string $mediaName) - By default, CssInliner will keep only media types all, screen and print. If you want to keep some others, you can use this method to define them.
  • ->removeAllowedMediaType(string $mediaName) - You can use this method to remove media types that Emogrifier keeps.
  • ->addExcludedSelector(string $selector) - Keeps elements from being affected by CSS inlining. Note that only elements matching the supplied selector(s) will be excluded from CSS inlining, not necessarily their descendants. If you wish to exclude an entire subtree, you should provide selector(s) which will match all elements in the subtree, for example by using the universal selector:
    $cssInliner->addExcludedSelector('.message-preview');
    $cssInliner->addExcludedSelector('.message-preview *');
  • ->addExcludedCssSelector(string $selector) - Contrary to addExcludedSelector which excludes HTML nodes, this method excludes CSS selectors from being inlined. This is for example useful if you don't want your CSS reset rules to be inlined on each HTML node (e.g. * { margin: 0; padding: 0; font-size: 100% }). Note that these selectors must precisely match the selectors you wish to exclude. Meaning that excluding .example will not exclude p .example.
    $cssInliner->addExcludedCssSelector('*');
    $cssInliner->addExcludedCssSelector('form');
  • ->removeExcludedCssSelector(string $selector) - Removes previously added excluded selectors, if any.
    $cssInliner->removeExcludedCssSelector('form');

Migrating from the dropped Emogrifier class to the CssInliner class

Minimal example

Old code using Emogrifier:

$emogrifier = new Emogrifier($html);
$html = $emogrifier->emogrify();

New code using CssInliner:

$html = CssInliner::fromHtml($html)->inlineCss()->render();

NB: In this example, the old code removes elements with display: none; while the new code does not, as the default behaviors of the old and the new class differ in this regard.

More complex example

Old code using Emogrifier:

$emogrifier = new Emogrifier($html, $css);
$emogrifier->enableCssToHtmlMapping();

$html = $emogrifier->emogrify();

New code using CssInliner and family:

$domDocument = CssInliner::fromHtml($html)->inlineCss($css)->getDomDocument();

HtmlPruner::fromDomDocument($domDocument)->removeElementsWithDisplayNone();
$html = CssToAttributeConverter::fromDomDocument($domDocument)
  ->convertCssToVisualAttributes()->render();

Supported CSS selectors

Emogrifier currently supports the following CSS selectors:

The following selectors are not implemented yet:

Rules involving the following selectors cannot be applied as inline styles. They will, however, be preserved and copied to a <style> element in the HTML:

Caveats

  • Emogrifier requires the HTML and the CSS to be UTF-8. Encodings like ISO8859-1 or ISO8859-15 are not supported.
  • Emogrifier preserves all valuable @media rules. Media queries can be very useful in responsive email design. See media query support. However, in order for them to be effective, you may need to add !important to some of the declarations within them so that they will override CSS styles that have been inlined. For example, with the following CSS, the font-size declaration in the @media rule would not override the font size for p elements from the preceding rule after that has been inlined as <p style="font-size: 16px;"> in the HTML, without the !important directive (even though !important would not be necessary if the CSS were not inlined):
    p {
      font-size: 16px;
    }
    @media (max-width: 640px) {
      p {
        font-size: 14px !important;
      }
    }
  • Emogrifier cannot inline CSS rules involving selectors with pseudo-elements (such as ::after) or dynamic pseudo-classes (such as :hover) – it is impossible. However, such rules will be preserved and copied to a <style> element, as for @media rules. The same caveat about the possible need for the !important directive also applies with pseudo-classes.
  • Emogrifier will grab existing inline style attributes and will grab <style> blocks from your HTML, but it will not grab CSS files referenced in <link> elements or @import rules (though it will leave them intact for email clients that support them).
  • Even with styles inline, certain CSS properties are ignored by certain email clients. For more information, refer to these resources:
  • All CSS attributes that apply to an element will be applied, even if they are redundant. For example, if you define a font attribute and a font-size attribute, both attributes will be applied to that element (in other words, the more specific attribute will not be combined into the more general attribute).
  • There's a good chance you might encounter problems if your HTML is not well-formed and valid (DOMDocument might complain). If you get problems like this, consider running your HTML through Tidy before you pass it to Emogrifier.
  • Emogrifier automatically converts the provided (X)HTML into HTML5, i.e., self-closing tags will lose their slash. To keep your HTML valid, it is recommended to use HTML5 instead of one of the XHTML variants.

Steps to release a new version

  1. In the composer.json, update the branch-alias entry to point to the release after the upcoming release.
  2. In the CHANGELOG.md, create a new section with subheadings for changes after the upcoming release, set the version number for the upcoming release, and remove any empty sections.
  3. Create a pull request "Prepare release of version x.y.z" with those changes.
  4. Have the pull request reviewed and merged.
  5. Tag the new release.
  6. In the Releases tab, create a new release and copy the change log entries to the new release.
  7. Post about the new release on social media.

Maintainers

emogrifier's People

Contributors

acorncom avatar adduc avatar bramley avatar demis-palma avatar dependabot[bot] avatar erikhansen avatar finwe avatar ionelscutelnicu avatar jakeqz avatar jjriv avatar jorrit avatar joubertredrat avatar jriboux avatar kozaktomas avatar markhalliwell avatar mistic100 avatar nlemoine avatar oliverklee avatar osbre avatar pmclain avatar renkas avatar sanderkruger avatar seldaek avatar signpostmarv avatar synchro avatar tams avatar timothyasp avatar timwolla avatar vanderlee avatar zoliszabo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

emogrifier's Issues

:first-child and :last-child selectors do not add CSS

There were 2 failures:

  1. Pelago\Tests\Unit\EmogrifierTest::emogrifierMatchesSelectors with data set "BODY:first-child matches first child" ('body:first-child {color: red} ', '#

    #')
    Failed asserting that '

some text

some text

some more text

' matches PCRE pattern "#

#".

/home/klee/eclipse/emogrifier/Tests/Unit/EmogrifierTest.php:370

  1. Pelago\Tests\Unit\EmogrifierTest::emogrifierMatchesSelectors with data set "BODY:last-child matches last child" ('body:last-child {color: red} ', '#

    #')
    Failed asserting that '

some text

some text

some more text

' matches PCRE pattern "#

#".

/home/klee/eclipse/emogrifier/Tests/Unit/EmogrifierTest.php:370

FAILURES!
Tests: 57, Assertions: 57, Failures: 2.

This is for the following lines in selectorDataProvider:

        'BODY:first-child matches first child'
            => array('body:first-child {' . $styleRule . '} ', '#<p class="p-1" style="' . $styleRule . '">#'),
        'BODY:first-child not matches middle child' => array('body:first-child {' . $styleRule . '} ', '#<p class="p-2">#'),
        'BODY:first-child not matches last child' => array('body:first-child {' . $styleRule . '} ', '#<p class="p-3">#'),
        'BODY:last-child not matches first child' => array('body:last-child {' . $styleRule . '} ', '#<p class="p-1">#'),
        'BODY:last-child not matches middle child' => array('body:last-child {' . $styleRule . '} ', '#<p class="p-2">#'),
        'BODY:last-child matches last child' => array('body:last-child {' . $styleRule . '} ', '#<p class="p-3" style="' . $styleRule . '">#'),

inconsistent handling of css case

Unit test:
emogrifyLowercasesAttributeNamesFromStyleAttributes
verifies that inline css in uppercase is translated to lowercase.

Unit test:
emogrifyKeepsAttributeNamesFromCssInOriginalCasing
seems to verify that passed in css does not become lowercase.

I have separately verified that uppercase css in a <style> maintains its uppercase without translation. Is this by design or would you like a pull request to ensure all css is lowercase?

empty <tr></tr>

Hi,

We have a problem in Outlook 2007 and 2010 with empty (the make a big line height with empty space), we can reproduce this issue here http://www.pelagodesign.com/sidecar/emogrifier/

with this css:

.zusatzleistung_desc_tr{
 display: none;
visibility: hidden;
}

and this html:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
    </head>
    <body>
        <table class="angebotdetails">
            <tr class="zusatzleistung_desc_tr">
                <td class="zusatzleistung_desc">Cane</td>
                <td class="zusatzleistung_amount">5,00 Euro</td>
            </tr>
        </table>
    </body>
</html>

gives after emogrify:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head></head>
<body>
        <table><tr></tr></table>
</body>
</html>

it should be:

<table>
        <tr style=" display: none;visibility: hidden;">
            <td class="zusatzleistung_desc">Cane</td>
            <td class="zusatzleistung_amount">5,00 Euro</td>
        </tr>
</table>

or remove the tr as well.

thank you!
lenny

Semantic versioned releases

Would it be possible to use github's release feature and semantic versioning? It would make dependencies much easier/safer in composer.

Bug: UTF8 issues

It looks like this lib has the same issues as InlineCss, it cannot work with some UTF8 chars:

<div id="container">
    <h1>チェック</h1>
    <p>降です。アーリーチェックインはリクエ</p>
    <p class="small">チェック'foo'</p>
</div>

Gets scrambled into

<div id="container">
    <h1 style="font-weight: bold; font-size: 2em;">&#12481;&#12455;&#12483;&#12463;</h1>
    <p style="margin-bottom: 1em; font-family: sans-serif; text-align: justify;">&#38477;&#12391;&#12377;&#12290;&#12450;&#12540;&#12522;&#12540;&#12481;&#12455;&#12483;&#12463;&#12452;&#12531;&#12399;&#12522;&#12463;&#12456;</p>
    <p class="small" style="margin-bottom:1em;font-family:sans-serif;text-align:justify;font-size:70%;">&#12481;&#12455;&#12483;&#12463;'foo'</p>
</div>

For the other lib I got i partially working again using htmlentitiesdecode.

Reorder the methods

The following order should make the class more logical to read:

  1. constructor
  2. destructor
  3. setHtml
  4. setCss
  5. emogriy
  6. other public methods

Version number in documentation on the composer configuration needs to be "@dev"

I had to use "pelago/emogrifier": "@dev" in my composer.json. Otherwise I got

Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - The requested package pelago/emogrifier could not be found in any version, there may be a typo in the package name.

Potential causes:
 - A typo in the package name
 - The package is not available in a stable-enough version according to your minimum-stability setting
   see <https://groups.google.com/d/topic/composer-dev/_g3ASeIFlrc/discussion> for more details.

Read <http://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.

Add composer.json

It would be cool if you can add a composer.json to this repo so that we can install it using packagist and use composer's autoloader 😄

Release pre-1.0 stable version

Because the only available version of this package is 1.0-alpha1, the only way one can install this package with Composer as part of another component is by reducing the strength of minimum-stability.

Could you please release a 0.1 version of the current state of the component, so that other libraries which depend on this component do not require the root project to reduce minimum-stability, without having to wait for 1.0?

refactor of parseCssDeclarationBlock

Creating this ticket per request to not leave comments in the code:

if (!preg_match('/ *([A-Za-z-]+) *: *([^;]+) */', $declaration, $matches)) {
continue;
}
// can we remove this regex entirely and replace with an explode on :, and conditional on resulting array being 2 strings?

It seems like this regex is only verifying that we have a single css declaration, and then using the regex match for the propertyName and propertyValue. I added the note because in the back of my head I thought an explode on the colon, and then verifying the resultant array has exactly 2 elements, and then using the 1st as propertyName and second as propertyValue would be quicker than a regex.

Add media print to the preserved media types

Email tracking tools like https://litmus.com are using follow technique for tracking email prints:

<style>
    @media print {
        #track { 
            background-image: url('http://www.letmeknow.com/print.jpg');
        }
    }
</style>
<div id="track"></div>

Current version from #62 PR strip this media query.

Drop support for PHP 5.3

We should do this once PHP 5.3 has reached its end of life, i.e., probably some time around June 2014.

Emogrifier turns $ into %24 unexpectedly

I use merge tags in my email bulletins to customize their content, for example to show the name of the recipient, I use the string $[USER:NAME]$ and then I replace it with the name.
If I place the merge tag wrapped in a span or div, there is no inconvenience, but if the merge tag is inside an attribute, let's say an href, emogrifier turns the '$' symbol to '%24' and '[' to '%5B' respectively.
If my original html is <a href="$[LINK:EVENT]$">LINK</a> emogrifier turns it into <a href="%24%5BLINK:EVENT%5D%24">LINK</a>

Any advice to avoid this behavior?

style tags in HTML cause an Xpath invalid query error

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

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

PHPUnit 3.7.29 by Sebastian Bergmann.

.........................................................E

Time: 59 ms, Memory: 3.00Mb

There was 1 error:

1) Pelago\Tests\Unit\EmogrifierTest::emogrifyAppliesCssFromStyleNodes
DOMXPath::query(): Invalid expression

Installing as dependency of a package fails

Hi

I'm writing a package that requires the emogrifier as a dependency of the package, I get an error when trying to install my package, saying that there is no matching package for the emogrifier require.

  • oligriffiths/emails-component dev-master requires pelago/emogrifier dev-master-> no matching package found.

However if installing as a require in the root composer.json the package installs fine.

Any ideas?

oli

Supporting id attributes

Any plans on supporting IDs?
There might be some templates that actually use that

css: div#container { margin: 1em auto; }

html: <div id="container">

They can - just as classes - also be stripped then afterwards.

Clean up early returns

  • only allow early returns for guard clauses
  • check where guard clauses would make sense

PHP 5.5 compatibility

preg_replace(): The /e modifier is deprecated, use preg_replace_callback instead

Location: emogrifier.php on line 315

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.