muddle 1.0.0rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Changes.md +5 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +220 -0
- data/Rakefile +2 -0
- data/lib/muddle.rb +37 -0
- data/lib/muddle/configuration.rb +43 -0
- data/lib/muddle/filter.rb +46 -0
- data/lib/muddle/filter/boilerplate_attributes.rb +24 -0
- data/lib/muddle/filter/boilerplate_css.rb +42 -0
- data/lib/muddle/filter/boilerplate_style_element.rb +31 -0
- data/lib/muddle/filter/premailer.rb +15 -0
- data/lib/muddle/filter/schema_validation.rb +13 -0
- data/lib/muddle/logger.rb +12 -0
- data/lib/muddle/parser.rb +45 -0
- data/lib/muddle/resources/boilerplate_inline.css +16 -0
- data/lib/muddle/resources/boilerplate_style.css +41 -0
- data/lib/muddle/version.rb +7 -0
- metadata +196 -0
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Ben Hamill
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
# Muddle
|
2
|
+
|
3
|
+
Email clients are not web browsers. They render html all funny, to put it
|
4
|
+
politely. In general, the best practices for writing HTML that will look good
|
5
|
+
in an email are the exact inverse from those that you should use for a web
|
6
|
+
page. Remembering all those differences sucks.
|
7
|
+
|
8
|
+
With muddle, we're trying to make it so that the only thing you have to know is
|
9
|
+
to **use tables in your emails**. Muddle will take care of the rest. It uses
|
10
|
+
ideas from [HTML Email Boilerplate](http://htmlemailboilerplate.com/) to help
|
11
|
+
you get your emails in line without having to know tons about how clients
|
12
|
+
render it.
|
13
|
+
|
14
|
+
* CSS will be inlined using premailer, so you can use external style sheets as
|
15
|
+
you normally would.
|
16
|
+
* HTML elements will be augmented with all the attributes they need for email,
|
17
|
+
so you don't need to worry about ensuring all your anchor tags have `_target`
|
18
|
+
set, etc.
|
19
|
+
* The resulting html document will be checked for tags that don't play well in
|
20
|
+
email (like `div`).
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Add this line to your application's `Gemfile`:
|
25
|
+
|
26
|
+
gem 'muddle'
|
27
|
+
|
28
|
+
And then execute:
|
29
|
+
|
30
|
+
$ bundle
|
31
|
+
|
32
|
+
Or install it yourself with:
|
33
|
+
|
34
|
+
$ gem install muddle
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
### The Basics
|
39
|
+
|
40
|
+
However you're sending email, you'll want to get what you intend to be the html
|
41
|
+
body of your email into a variable. How you do that is up to you. Say you have
|
42
|
+
a [SLIM](http://slim-lang.com) template and you're using
|
43
|
+
[Mail](https://github.com/mikel/mail) to build and send your emails:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
require 'muddle'
|
47
|
+
require 'slim'
|
48
|
+
require 'mail'
|
49
|
+
|
50
|
+
body = Slim::Template('path/to/welcome_email.html.slim').render
|
51
|
+
|
52
|
+
# This is really the only thing Muddle is involved in.
|
53
|
+
muddled_body = Muddle.parse(body)
|
54
|
+
|
55
|
+
email = Mail.new do
|
56
|
+
to 'some_new_customer@gmail.com'
|
57
|
+
from 'welcome@awesome_web_service.com'
|
58
|
+
subject 'Welcome!!!!!!!!!!1!!!!one!!!'
|
59
|
+
|
60
|
+
html_part do
|
61
|
+
body muddled_body
|
62
|
+
content_type 'text/html; charset=UTF-8'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
If you're using `ActionMailer`, you could do like this:
|
68
|
+
|
69
|
+
``` ruby
|
70
|
+
class UserMailer < ActionMailer::Base
|
71
|
+
def welcome_email
|
72
|
+
mail(
|
73
|
+
to: 'some_new_customer@gmail.com',
|
74
|
+
from: 'welcome@awesome_web_service.com',
|
75
|
+
subject: 'Welcome!!!!!!!!!!1!!!!one!!!'
|
76
|
+
) do |format|
|
77
|
+
format.html { Muddle.parse(render) }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
### Configuration
|
84
|
+
|
85
|
+
You can configure Muddle with a block. Maybe throw this in an
|
86
|
+
initializer of some sort. Here are all the defaults:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
Muddle.configure do |config|
|
90
|
+
config.parse_with_premailer = true
|
91
|
+
config.insert_boilerplate_styles = true
|
92
|
+
config.insert_boilerplate_css = true
|
93
|
+
config.insert_boilerplate_attributes = true
|
94
|
+
config.validate_html = true
|
95
|
+
config.generate_plain_text = false
|
96
|
+
config.logger = nil
|
97
|
+
|
98
|
+
config.premailer_options = {
|
99
|
+
:remove_comments => true,
|
100
|
+
:with_html_string => true,
|
101
|
+
:adapter => :hpricot
|
102
|
+
}
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
### Writing An Email
|
107
|
+
|
108
|
+
For best results, just start writing your email with a table tag and move in
|
109
|
+
from there. Muddler will handle putting the `xmlns` and `DOCTYPE` and a bunch
|
110
|
+
of stuff into the `<head>`, then open the `<body>` for you. It will also close
|
111
|
+
these tags at the end.
|
112
|
+
|
113
|
+
For example, if you have a template the ends up like this:
|
114
|
+
|
115
|
+
```html
|
116
|
+
<html>
|
117
|
+
<body>
|
118
|
+
<table>
|
119
|
+
<tbody>
|
120
|
+
<tr>
|
121
|
+
<td><h1>Welcome to our AWESOME NEW WEB SERVICE!</h1></td>
|
122
|
+
</tr>
|
123
|
+
<tr>
|
124
|
+
<td><p>You should come <a href="http://awesome_web_service.com">check us out</a>.</p></td>
|
125
|
+
</tr>
|
126
|
+
</tbody>
|
127
|
+
</table>
|
128
|
+
</body>
|
129
|
+
</html>
|
130
|
+
```
|
131
|
+
|
132
|
+
Muddle will spit out this:
|
133
|
+
|
134
|
+
```html
|
135
|
+
<html xmlns="http://www.w3.org/1999/xhtml">
|
136
|
+
<head>
|
137
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
138
|
+
</head>
|
139
|
+
<body style="width: 100% !important; margin: 0; padding: 0; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%;">
|
140
|
+
<table cellpadding="0" cellspacing="0" border="0" align="center">
|
141
|
+
<tbody>
|
142
|
+
<tr>
|
143
|
+
<td valign="top"><h1 style="color: black !important;">Welcome to our AWESOME NEW WEB SERVICE!</h1></td>
|
144
|
+
</tr>
|
145
|
+
<tr>
|
146
|
+
<td valign="top"><p style="margin: 1em 0;">You should <a href="http://awesome_web_service.com" style="color: blue;" target="_blank">check us out</a>.</p></td>
|
147
|
+
</tr>
|
148
|
+
</tbody>
|
149
|
+
</table>
|
150
|
+
|
151
|
+
<style type="text/css">
|
152
|
+
/* Boilerplate CSS for BODY */
|
153
|
+
|
154
|
+
#outlook a {padding:0;}
|
155
|
+
#backgroundTable {margin:0; padding:0; width:100% !important; line-height: 100% !important;}
|
156
|
+
|
157
|
+
.ExternalClass {width:100%;}
|
158
|
+
.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
|
159
|
+
.image_fix {display:block;}
|
160
|
+
|
161
|
+
h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {color: red !important;}
|
162
|
+
h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {color: purple !important;}
|
163
|
+
|
164
|
+
@media only screen and (max-device-width: 480px) {
|
165
|
+
a[href^="tel"], a[href^="sms"] {
|
166
|
+
text-decoration: none;
|
167
|
+
color: blue;
|
168
|
+
pointer-events: none;
|
169
|
+
cursor: default;
|
170
|
+
}
|
171
|
+
.mobile_link a[href^="tel"], .mobile_link a[href^="sms"] {
|
172
|
+
text-decoration: default;
|
173
|
+
color: orange !important;
|
174
|
+
pointer-events: auto;
|
175
|
+
cursor: default;
|
176
|
+
}
|
177
|
+
}
|
178
|
+
@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) {
|
179
|
+
a[href^="tel"], a[href^="sms"] {
|
180
|
+
text-decoration: none;
|
181
|
+
color: blue;
|
182
|
+
pointer-events: none;
|
183
|
+
cursor: default;
|
184
|
+
}
|
185
|
+
.mobile_link a[href^="tel"], .mobile_link a[href^="sms"] {
|
186
|
+
text-decoration: default;
|
187
|
+
color: orange !important;
|
188
|
+
pointer-events: auto;
|
189
|
+
cursor: default;
|
190
|
+
}
|
191
|
+
}
|
192
|
+
</style>
|
193
|
+
<style type="text/css">
|
194
|
+
body { width: 100% !important; -webkit-text-size-adjust: 100% !important; -ms-text-size-adjust: 100% !important; margin: 0 !important; padding: 0 !important; }
|
195
|
+
img { outline: none !important; text-decoration: none !important; -ms-interpolation-mode: bicubic !important; }
|
196
|
+
</style>
|
197
|
+
</body>
|
198
|
+
</html>
|
199
|
+
```
|
200
|
+
|
201
|
+
|
202
|
+
## To Do
|
203
|
+
|
204
|
+
* naughty tag warnings
|
205
|
+
* performance tests
|
206
|
+
* test external CSS resource handling
|
207
|
+
* test if premailer is making image URI's absolute where possible
|
208
|
+
* complain about images with relative urls
|
209
|
+
* complain about image not having alt, height, width
|
210
|
+
* create background attribute from css where relevant
|
211
|
+
* check for lines starting with a period
|
212
|
+
|
213
|
+
|
214
|
+
## Contributing
|
215
|
+
|
216
|
+
1. Fork it
|
217
|
+
2. Create your feature branch (`$ git checkout -b my-new-feature`)
|
218
|
+
3. Commit your changes (`$ git commit -am 'Added some feature'`)
|
219
|
+
4. Push to the branch (`$ git push origin my-new-feature`)
|
220
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/lib/muddle.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
|
3
|
+
|
4
|
+
require "muddle/version"
|
5
|
+
|
6
|
+
require "muddle/filter"
|
7
|
+
require "muddle/parser"
|
8
|
+
require "muddle/configuration"
|
9
|
+
require 'muddle/logger'
|
10
|
+
|
11
|
+
module Muddle
|
12
|
+
# Top-level configuration function
|
13
|
+
#
|
14
|
+
# Pass it a block and the configuration object will yield 'self'
|
15
|
+
#
|
16
|
+
def self.configure(&block)
|
17
|
+
config.configure(&block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.config
|
21
|
+
@config ||= Muddle::Configuration.new
|
22
|
+
end
|
23
|
+
|
24
|
+
# Top-level parser function
|
25
|
+
#
|
26
|
+
# body_string should be an email body in string form
|
27
|
+
#
|
28
|
+
# returns body_string after passing it through the filters defined in Parser.filters
|
29
|
+
#
|
30
|
+
def self.parse(body_string)
|
31
|
+
parser.parse body_string
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.parser
|
35
|
+
@parser ||= Muddle::Parser.new
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Muddle::Configuration
|
2
|
+
attr_accessor :parse_with_premailer
|
3
|
+
attr_accessor :insert_boilerplate_styles
|
4
|
+
attr_accessor :insert_boilerplate_css
|
5
|
+
attr_accessor :insert_boilerplate_attributes
|
6
|
+
attr_accessor :validate_html
|
7
|
+
attr_accessor :generate_plain_text
|
8
|
+
attr_accessor :logger
|
9
|
+
|
10
|
+
attr_accessor :premailer_options
|
11
|
+
|
12
|
+
# Initialize the configuration object with default values
|
13
|
+
#
|
14
|
+
# if a block is passed, we'll yield 'this' to it so you can set config values
|
15
|
+
#
|
16
|
+
def initialize(options = {})
|
17
|
+
@parse_with_premailer = options[:parse_with_premailer] || true
|
18
|
+
@insert_boilerplate_styles = options[:insert_boilerplate_styles] || true
|
19
|
+
@insert_boilerplate_css = options[:insert_boilerplate_css] || true
|
20
|
+
@insert_boilerplate_attributes = options[:insert_boilerplate_attributes] || true
|
21
|
+
@validate_html = options[:validate_html] || true
|
22
|
+
@generate_plain_text = options[:generate_plain_text] || false
|
23
|
+
@logger = options[:logger]
|
24
|
+
|
25
|
+
# NOTE: when this tries to inline CSS, all it sees is a stylesheet URL
|
26
|
+
# This may require that we download css from the interwebs @ each render
|
27
|
+
# pass of a mailer ?!?!?
|
28
|
+
@premailer_options = {
|
29
|
+
:remove_comments => true, # Env-dependent?
|
30
|
+
:with_html_string => true,
|
31
|
+
:adapter => :hpricot
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
# Set config vars
|
36
|
+
#
|
37
|
+
# Pass it a block, will yield the config object
|
38
|
+
#
|
39
|
+
def configure
|
40
|
+
yield self
|
41
|
+
self
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Muddle::Filter
|
2
|
+
# Prepend the content if child nodes exist, otherwise insert it
|
3
|
+
#
|
4
|
+
def prepend_or_insert(doc, content)
|
5
|
+
unless doc.empty?
|
6
|
+
doc.children.first.before(content)
|
7
|
+
else
|
8
|
+
doc.inner_html(content)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Append the content if child nodes exist, otherwise insert it
|
13
|
+
#
|
14
|
+
def append_or_insert(doc, content)
|
15
|
+
unless doc.empty?
|
16
|
+
doc.children.last.after(content)
|
17
|
+
else
|
18
|
+
doc.inner_html(content)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
# Find `selector` within `doc`. If not found, create using `with` as the
|
24
|
+
# last child of `doc`
|
25
|
+
#
|
26
|
+
def find_or_append(doc, selector, opts, &block)
|
27
|
+
append_or_insert(doc, opts[:with]) if doc.search(selector).empty?
|
28
|
+
yield doc.search(selector)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Find `selector` within `doc` if not found, create using `with` as the
|
32
|
+
# first child of `doc`
|
33
|
+
#
|
34
|
+
# yields to `block` and passes the found/created element
|
35
|
+
#
|
36
|
+
def find_or_prepend(doc, selector, opts, &block)
|
37
|
+
prepend_or_insert(doc, opts[:with]) if doc.search(selector).empty?
|
38
|
+
yield doc.search(selector)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
require 'muddle/filter/boilerplate_attributes'
|
43
|
+
require 'muddle/filter/boilerplate_css'
|
44
|
+
require 'muddle/filter/boilerplate_style_element'
|
45
|
+
require 'muddle/filter/premailer'
|
46
|
+
require 'muddle/filter/schema_validation'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'hpricot'
|
2
|
+
|
3
|
+
module Muddle::Filter::BoilerplateAttributes
|
4
|
+
def self.filter(body_string)
|
5
|
+
doc = Hpricot(body_string)
|
6
|
+
|
7
|
+
ensure_node_includes(doc, 'table', 'cellpadding', '0')
|
8
|
+
ensure_node_includes(doc, 'table', 'cellspacing', '0')
|
9
|
+
ensure_node_includes(doc, 'table', 'border', '0')
|
10
|
+
ensure_node_includes(doc, 'table', 'align', 'center')
|
11
|
+
|
12
|
+
ensure_node_includes(doc, 'td', 'valign', 'top')
|
13
|
+
|
14
|
+
ensure_node_includes(doc, 'a', 'target', '_blank')
|
15
|
+
|
16
|
+
doc.to_html
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.ensure_node_includes(doc, element_selector, attribute, default_value)
|
20
|
+
doc.search("#{element_selector}:not([@#{attribute}])").each do |node|
|
21
|
+
node.attributes[attribute] = default_value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'hpricot'
|
2
|
+
|
3
|
+
module Muddle::Filter::BoilerplateCSS
|
4
|
+
extend Muddle::Filter
|
5
|
+
|
6
|
+
# Boilerplate CSS Filter
|
7
|
+
#
|
8
|
+
# Inserts a style tag containing the boilerplate CSS - we assume that this will
|
9
|
+
# later be filtered with the Premailer filter and inlined at that time.
|
10
|
+
#
|
11
|
+
# If the body_string doesn't look like an HTML file, we'll build as much structure
|
12
|
+
# as makes sense in the context of a style tag (ie at least enclosing <html> and <head> tags)
|
13
|
+
#
|
14
|
+
# The style tag will be inserted before any existing style tags so that any user-supplied
|
15
|
+
# CSS will over-write our boilerplate stuff
|
16
|
+
#
|
17
|
+
def self.filter(body_string)
|
18
|
+
doc = Hpricot(body_string)
|
19
|
+
|
20
|
+
insert_styles_to_inline(doc)
|
21
|
+
|
22
|
+
doc.to_html
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.insert_styles_to_inline(doc)
|
26
|
+
find_or_append(doc, 'html', :with => '<html></html>') do |html|
|
27
|
+
find_or_prepend(html.first, 'head', :with => '<head></head>') do |head|
|
28
|
+
if node = head.search('style:first-of-type()').first
|
29
|
+
node.before('<style type="text/css"></style>')
|
30
|
+
else
|
31
|
+
prepend_or_insert(head.first, '<style type="text/css"></style>')
|
32
|
+
end
|
33
|
+
|
34
|
+
head.search('style:first-of-type()').inner_html(boilerplate_css)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.boilerplate_css
|
40
|
+
@boilerplate_css ||= File.read(File.join(File.dirname(__FILE__), '..', 'resources', 'boilerplate_inline.css'))
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'hpricot'
|
2
|
+
|
3
|
+
module Muddle::Filter::BoilerplateStyleElement
|
4
|
+
extend Muddle::Filter
|
5
|
+
|
6
|
+
def self.filter(body_string)
|
7
|
+
doc = Hpricot(body_string)
|
8
|
+
|
9
|
+
insert_style_block(doc)
|
10
|
+
|
11
|
+
doc.to_html
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.insert_style_block(doc)
|
15
|
+
find_or_append(doc, 'html', :with => '<html></html>') do |html|
|
16
|
+
find_or_prepend(html.first, 'body', :with => '<body></body>') do |body|
|
17
|
+
if node = body.search('style:first-of-type()').first
|
18
|
+
node.before('<style type="text/css"></style>')
|
19
|
+
else
|
20
|
+
append_or_insert(body.first, '<style type="text/css"></style>')
|
21
|
+
end
|
22
|
+
|
23
|
+
body.search('style:first-of-type()').inner_html(boilerplate_css)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.boilerplate_css
|
29
|
+
@boilerplate_css ||= File.read(File.join(File.dirname(__FILE__), '..', 'resources', 'boilerplate_style.css'))
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'premailer'
|
2
|
+
|
3
|
+
module Muddle::Filter::Premailer
|
4
|
+
def self.filter(body_string)
|
5
|
+
premailer = Premailer.new(body_string, Muddle.config.premailer_options)
|
6
|
+
|
7
|
+
warn "Premailer generated #{premailer.warnings.length.to_s} warnings:" unless premailer.warnings.empty?
|
8
|
+
|
9
|
+
premailer.warnings.each do |w|
|
10
|
+
warn "#{w[:message]} (#{w[:level]}) may not render properly in #{w[:clients]}"
|
11
|
+
end
|
12
|
+
|
13
|
+
premailer.to_inline_css
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module Muddle::Filter::SchemaValidation
|
4
|
+
def self.filter(body_string)
|
5
|
+
doc = Nokogiri::XML(body_string)
|
6
|
+
|
7
|
+
if doc.internal_subset.nil?
|
8
|
+
doc.create_internal_subset("html", "-//W3C//DTD XHTML 1.0 Strict//EN", "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd")
|
9
|
+
end
|
10
|
+
|
11
|
+
doc.to_xhtml
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
class Muddle::Parser
|
2
|
+
# Set up the parser
|
3
|
+
#
|
4
|
+
# The default filters do the following:
|
5
|
+
#
|
6
|
+
# CSS adds a style block containing boilerplate CSS attributes to be
|
7
|
+
# inlined. This is based on Email Boilerplate's 'Inline: YES' portions
|
8
|
+
#
|
9
|
+
# Premailer passes the email through the Premailer gem, which inlines the CSS
|
10
|
+
# it can, then appends a style block to the body containing the rest (since
|
11
|
+
# some email clients strip out the <head> tag).
|
12
|
+
#
|
13
|
+
# Style Element adds another style block, but this one is intended to be left
|
14
|
+
# as a style declaration (rather than being inlined). This is based on
|
15
|
+
# EMail Boilerplate's 'Inline: NO' portions
|
16
|
+
#
|
17
|
+
# Attributes adds attributes to HTML elements where helpful, such as table cellpadding
|
18
|
+
# and such. Based on Email Boilerplate's example element declarations
|
19
|
+
#
|
20
|
+
# Schema Validation currently just adds the XHTML Strict DTD and outputs to a string,
|
21
|
+
# however it's intended that this will eventually validate against an XSD and emit
|
22
|
+
# warnings about potentially troublesome tags (like <div>)
|
23
|
+
#
|
24
|
+
def initialize
|
25
|
+
@filters = []
|
26
|
+
|
27
|
+
@filters << Muddle::Filter::BoilerplateCSS if Muddle.config.insert_boilerplate_css
|
28
|
+
@filters << Muddle::Filter::Premailer if Muddle.config.parse_with_premailer
|
29
|
+
@filters << Muddle::Filter::BoilerplateStyleElement if Muddle.config.insert_boilerplate_styles
|
30
|
+
@filters << Muddle::Filter::BoilerplateAttributes if Muddle.config.insert_boilerplate_attributes
|
31
|
+
@filters << Muddle::Filter::SchemaValidation if Muddle.config.validate_html
|
32
|
+
end
|
33
|
+
|
34
|
+
# Parse an email body
|
35
|
+
#
|
36
|
+
# body_string is the email body to be parsed in string form
|
37
|
+
#
|
38
|
+
# Returns the parsed body string
|
39
|
+
#
|
40
|
+
def parse(body_string)
|
41
|
+
@filters.inject(body_string) do |filtered_string, filter|
|
42
|
+
s = filter.filter(filtered_string)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
/* Boilerplate CSS for Inlining */
|
3
|
+
|
4
|
+
body{width:100%; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0;}
|
5
|
+
|
6
|
+
img {outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;}
|
7
|
+
a img {border:none;}
|
8
|
+
|
9
|
+
p {margin: 1em 0;}
|
10
|
+
|
11
|
+
h1, h2, h3, h4, h5, h6 {color: black;}
|
12
|
+
|
13
|
+
h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {color: blue;}
|
14
|
+
|
15
|
+
a {color: blue;}
|
16
|
+
|
@@ -0,0 +1,41 @@
|
|
1
|
+
|
2
|
+
/* Boilerplate CSS for HEAD */
|
3
|
+
|
4
|
+
#outlook a {padding:0;}
|
5
|
+
#backgroundTable {margin:0; padding:0; width:100%; line-height: 100%;}
|
6
|
+
|
7
|
+
.ExternalClass {width:100%;}
|
8
|
+
.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
|
9
|
+
.image_fix {display:block;}
|
10
|
+
|
11
|
+
h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {color: red;}
|
12
|
+
h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {color: purple;}
|
13
|
+
|
14
|
+
@media only screen and (max-device-width: 480px) {
|
15
|
+
a[href^="tel"], a[href^="sms"] {
|
16
|
+
text-decoration: none;
|
17
|
+
color: blue;
|
18
|
+
pointer-events: none;
|
19
|
+
cursor: default;
|
20
|
+
}
|
21
|
+
.mobile_link a[href^="tel"], .mobile_link a[href^="sms"] {
|
22
|
+
text-decoration: default;
|
23
|
+
color: orange;
|
24
|
+
pointer-events: auto;
|
25
|
+
cursor: default;
|
26
|
+
}
|
27
|
+
}
|
28
|
+
@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) {
|
29
|
+
a[href^="tel"], a[href^="sms"] {
|
30
|
+
text-decoration: none;
|
31
|
+
color: blue;
|
32
|
+
pointer-events: none;
|
33
|
+
cursor: default;
|
34
|
+
}
|
35
|
+
.mobile_link a[href^="tel"], .mobile_link a[href^="sms"] {
|
36
|
+
text-decoration: default;
|
37
|
+
color: orange;
|
38
|
+
pointer-events: auto;
|
39
|
+
cursor: default;
|
40
|
+
}
|
41
|
+
}
|
metadata
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: muddle
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0rc1
|
5
|
+
prerelease: 5
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ryan Michael
|
9
|
+
- Ben Hamill
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-07-10 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: premailer
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ~>
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.7.3
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ~>
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: 1.7.3
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: nokogiri
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ~>
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: 1.5.0
|
39
|
+
type: :runtime
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ~>
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 1.5.0
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: hpricot
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.6'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ~>
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0.6'
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: css_parser
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ~>
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 1.2.6
|
71
|
+
type: :runtime
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ~>
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: 1.2.6
|
79
|
+
- !ruby/object:Gem::Dependency
|
80
|
+
name: rspec
|
81
|
+
requirement: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
type: :development
|
88
|
+
prerelease: false
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: email_spec
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ! '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: mail
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ! '>='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
123
|
+
requirements:
|
124
|
+
- - ! '>='
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
- !ruby/object:Gem::Dependency
|
128
|
+
name: pry
|
129
|
+
requirement: !ruby/object:Gem::Requirement
|
130
|
+
none: false
|
131
|
+
requirements:
|
132
|
+
- - ! '>='
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
type: :development
|
136
|
+
prerelease: false
|
137
|
+
version_requirements: !ruby/object:Gem::Requirement
|
138
|
+
none: false
|
139
|
+
requirements:
|
140
|
+
- - ! '>='
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
description: Email clients are not web browsers. They render html all funny, to put
|
144
|
+
it politely. In general, the best practices for writing HTML that will look good
|
145
|
+
in an email are the exact inverse from those that you should use for a web page.
|
146
|
+
Remembering all those differences sucks.
|
147
|
+
email:
|
148
|
+
- benhamill@otherinbox.com
|
149
|
+
executables: []
|
150
|
+
extensions: []
|
151
|
+
extra_rdoc_files: []
|
152
|
+
files:
|
153
|
+
- lib/muddle.rb
|
154
|
+
- lib/muddle/configuration.rb
|
155
|
+
- lib/muddle/filter.rb
|
156
|
+
- lib/muddle/filter/boilerplate_attributes.rb
|
157
|
+
- lib/muddle/filter/boilerplate_css.rb
|
158
|
+
- lib/muddle/filter/boilerplate_style_element.rb
|
159
|
+
- lib/muddle/filter/premailer.rb
|
160
|
+
- lib/muddle/filter/schema_validation.rb
|
161
|
+
- lib/muddle/logger.rb
|
162
|
+
- lib/muddle/parser.rb
|
163
|
+
- lib/muddle/resources/boilerplate_inline.css
|
164
|
+
- lib/muddle/resources/boilerplate_style.css
|
165
|
+
- lib/muddle/version.rb
|
166
|
+
- Gemfile
|
167
|
+
- LICENSE
|
168
|
+
- Rakefile
|
169
|
+
- README.md
|
170
|
+
- Changes.md
|
171
|
+
homepage: http://github.com/otherinbox/muddle
|
172
|
+
licenses: []
|
173
|
+
post_install_message:
|
174
|
+
rdoc_options: []
|
175
|
+
require_paths:
|
176
|
+
- lib
|
177
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
178
|
+
none: false
|
179
|
+
requirements:
|
180
|
+
- - ! '>='
|
181
|
+
- !ruby/object:Gem::Version
|
182
|
+
version: '0'
|
183
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
184
|
+
none: false
|
185
|
+
requirements:
|
186
|
+
- - ! '>'
|
187
|
+
- !ruby/object:Gem::Version
|
188
|
+
version: 1.3.1
|
189
|
+
requirements: []
|
190
|
+
rubyforge_project:
|
191
|
+
rubygems_version: 1.8.24
|
192
|
+
signing_key:
|
193
|
+
specification_version: 3
|
194
|
+
summary: Never type all the annoying markup that emails demand again.
|
195
|
+
test_files: []
|
196
|
+
has_rdoc:
|