Make CSS inline in HTML mails
authorChristian Weiske <cweiske@cweiske.de>
Fri, 17 Jun 2016 19:36:03 +0000 (21:36 +0200)
committerChristian Weiske <cweiske@cweiske.de>
Fri, 17 Jun 2016 19:36:03 +0000 (21:36 +0200)
src/bdrem/Renderer/Html.php
src/bdrem/Renderer/Mail.php

index e802faf..10059ab 100644 (file)
@@ -76,6 +76,7 @@ class Renderer_Html extends Renderer
 
         $tr = new Renderer_HtmlTable();
         $table = $tr->render($arEvents);
+        $css = static::getCss();
         $s = <<<HTM
 <?xml version="1.0" encoding="utf-8"?>
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
@@ -84,7 +85,22 @@ class Renderer_Html extends Renderer
  <head>
   <title>bdrem</title>
   <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-$links  <style type="text/css">
+$links  <style type="text/css">$css</style>
+ </head>
+ <body>
+$table
+ </body>
+</html>
+HTM;
+        return $s;
+    }
+
+    /**
+     * Get the CSS for the HTML table
+     */
+    public static function getCss()
+    {
+        return <<<CSS
 table {
     border: 1px solid black;
     border-collapse: collapse;
@@ -150,14 +166,7 @@ tr.d2 td.icon:before {
 tr.d3 td.icon:before {
     content: "\342\227\224"
 }
-  </style>
- </head>
- <body>
-$table
- </body>
-</html>
-HTM;
-        return $s;
+CSS;
     }
 }
 ?>
index d7aef3e..0efa3af 100644 (file)
@@ -29,6 +29,18 @@ require_once 'Mail/mime.php';
 class Renderer_Mail extends Renderer
 {
     /**
+     * Add HTML part to email
+     * @var bool
+     */
+    public $html = true;
+
+    /**
+     * CSS "inline" in tags, or "separate" in a style block
+     * @var string
+     */
+    public $css = 'inline';
+
+    /**
      * Render the events - send out mails.
      *
      * Uses the config's "mail_to" array as recipients.
@@ -52,8 +64,8 @@ class Renderer_Mail extends Renderer
             $subject .= ': ' . implode(', ', $todays);
         }
 
-        $rc = new Renderer_Console();
-        $rh = new Renderer_Html();
+        $rc  = new Renderer_Console();
+        $rht = new Renderer_HtmlTable();
 
         $hdrs = array(
             'From'    => $this->config->get('mail_from', 'birthday@example.org'),
@@ -69,7 +81,20 @@ class Renderer_Mail extends Renderer
         );
 
         $mime->setTXTBody($rc->render($arEvents));
-        $mime->setHTMLBody($rh->render($arEvents));
+        if ($this->html) {
+            if ($this->css == 'inline') {
+                $html = $this->inlineCss(
+                    $rht->render($arEvents),
+                    Renderer_Html::getCss()
+                );
+            } else {
+                $html = '<style type="text/css">'
+                    . Renderer_Html::getCss()
+                    . '</style>'
+                    . $rht->render($arEvents);
+            }
+            $mime->setHTMLBody($this->minifyHtml($html));
+        }
 
         $body = $mime->get();
         $hdrs = $mime->headers($hdrs);
@@ -107,5 +132,113 @@ class Renderer_Mail extends Renderer
 
         return mb_substr($str, 0, $len - 1) . '…';
     }
+
+    /**
+     * Takes the HTML and CSS code and inlines CSS into HTML.
+     *
+     * This is important for some e-mail clients which do
+     * not interpret <style> tags but only support inline styles.
+     *
+     * Works nicely with bdrem's CSS. If you need more CSS selector
+     * support, have a look at https://github.com/jjriv/emogrifier
+     *
+     * @param string $html HTML code
+     * @param string $css  CSS code
+     *
+     * @return string HTML with inlined CSS
+     */
+    protected function inlineCss($html, $css)
+    {
+        preg_match_all(
+            '#([^{]+) {([^}]+)}#m',
+            $css,
+            $parts
+        );
+        $rules = array();
+        foreach ($parts[1] as $key => $rule) {
+            $mrules = explode(',', $rule);
+            foreach ($mrules as $rule) {
+                $rule  = trim($rule);
+                $style = trim($parts[2][$key]);
+                $rules[$rule] = preg_replace(
+                    '#([:;]) +#', '\1',
+                    str_replace(
+                        ["\r", "\n", '    '],
+                        ['', '', ' '],
+                        $style
+                    )
+                );
+            }
+        }
+        $sx = simplexml_load_string($html);
+        foreach ($rules as $rule => $style) {
+            $mode = null;
+            $parts = explode(' ', $rule);
+            $xp = '';
+            foreach ($parts as $part) {
+                $part = trim($part);
+                if (strpos($part, ':') !== false) {
+                    //.foo:before
+                    list($part, $mode) = explode(':', $part);
+                    if ($mode == 'hover') {
+                        continue 2;
+                    }
+                }
+                if (strpos($part, '.') === false) {
+                    //tag only
+                    if ($part == '') {
+                        $xp = '//*';
+                    } else {
+                        $xp .= '//' . $part;
+                    }
+                } else {
+                    //tag.class
+                    list($tag, $class) = explode('.', $part);
+                    if ($tag == '') {
+                        $tag = '*';
+                    }
+                    $xp .= '//' . $tag
+                        . '[contains('
+                        . 'concat(" ", normalize-space(@class), " "), '
+                        . '" ' . $class . ' "'
+                        . ')]';
+                }
+            }
+            $res = $sx->xpath($xp);
+            //var_dump($res);die();
+            //var_dump($xp, $style);
+            foreach ($res as $xelem) {
+                if ($mode === null) {
+                    $xelem['style'] .= $style;
+                } else if ($mode == 'before') {
+                    $xelem[0] = preg_replace(
+                        '#content:\s*"(.+)"#', '\1', $style
+                    );
+                }
+            }
+        }
+
+        $html = $sx->asXML();
+        //strip xml header
+        $lines = explode("\n", $html);
+        unset($lines[0]);
+        $html = implode("\n", $lines);
+
+        //echo $html . "\n";die();
+        return $html;
+    }
+
+    /**
+     * Remove whitespace between tags
+     *
+     * @param string $html HTML code
+     *
+     * @return string Smaller HTML code
+     */
+    protected function minifyHtml($html)
+    {
+        $html = trim(preg_replace("#[\n\r ]+<#", '<', $html));
+        return $html;
+    }
 }
 ?>
\ No newline at end of file