premailer 1.19.0 → 1.30.0
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.
- checksums.yaml +4 -4
- data/README.md +118 -29
- data/bin/premailer +1 -1
- data/lib/premailer/adapter/nokogiri.rb +143 -81
- data/lib/premailer/adapter/nokogiri_fast.rb +46 -40
- data/lib/premailer/adapter/nokogumbo.rb +37 -31
- data/lib/premailer/adapter/rgb_to_hex.rb +11 -9
- data/lib/premailer/adapter.rb +10 -13
- data/lib/premailer/cached_rule_set.rb +13 -0
- data/lib/premailer/executor.rb +12 -11
- data/lib/premailer/html_to_plain_text.rb +20 -17
- data/lib/premailer/premailer.rb +162 -125
- data/lib/premailer/version.rb +2 -1
- data/lib/premailer.rb +2 -0
- metadata +14 -133
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9d0463a94da370db30e1e80fa7851c90f9b24743518137a2d04b2c21ed7246cd
|
|
4
|
+
data.tar.gz: 73bf8f6e3f7cade9389ff65a6fef58112e8cf2a756f0271adc8b3278bd998a5b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d98924c3ff4088566f7952d8b56c2ae8747fc99e6ba3f286dcce5e9dd788b3b5c80d1faf60c1a8ff3bb77dc4be136477b1f862cd00b4f1f55b100c9d6ae1289
|
|
7
|
+
data.tar.gz: aff5c151b261c9f35eb58a9a87407cb5c6ee67804ae2ec9585ad4b59043eb6132e443223ad650565dfda7a9fdfe8c528315b5946c5b2d4ea1281ecb8e65bb373
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
For the best HTML e-mail delivery results, CSS should be inline. This is a
|
|
6
6
|
huge pain and a simple newsletter becomes un-managable very quickly. This
|
|
7
|
-
|
|
7
|
+
gem is a solution.
|
|
8
8
|
|
|
9
9
|
* CSS styles are converted to inline style attributes
|
|
10
10
|
- Checks `style` and `link[rel=stylesheet]` tags and preserves existing inline attributes
|
|
@@ -12,35 +12,26 @@ script is my solution.
|
|
|
12
12
|
- Checks links in `href`, `src` and CSS `url('')`
|
|
13
13
|
* CSS properties are checked against e-mail client capabilities
|
|
14
14
|
- Based on the Email Standards Project's guides
|
|
15
|
-
* A [plain text version](
|
|
15
|
+
* A [plain text version](#plain-text-version) is created (optional)
|
|
16
16
|
|
|
17
17
|
## Installation
|
|
18
18
|
|
|
19
|
-
Install the Premailer gem from RubyGems.
|
|
20
|
-
|
|
21
19
|
```bash
|
|
22
20
|
gem install premailer
|
|
23
21
|
```
|
|
24
22
|
|
|
25
|
-
or add it to your `Gemfile` and run `bundle`.
|
|
26
|
-
|
|
27
23
|
## Example
|
|
28
24
|
|
|
29
25
|
```ruby
|
|
30
26
|
require 'premailer'
|
|
31
27
|
|
|
32
|
-
premailer = Premailer.new('http://example.com/myfile.html', :
|
|
28
|
+
premailer = Premailer.new('http://example.com/myfile.html', warn_level: Premailer::Warnings::SAFE)
|
|
33
29
|
|
|
34
|
-
# Write the plain-text output
|
|
35
|
-
|
|
36
|
-
File.open("output.txt", "w") do |fout|
|
|
37
|
-
fout.puts premailer.to_plain_text
|
|
38
|
-
end
|
|
30
|
+
# Write the plain-text output (must come before to_inline_css)
|
|
31
|
+
File.write "output.txt", premailer.to_plain_text
|
|
39
32
|
|
|
40
33
|
# Write the HTML output
|
|
41
|
-
File.
|
|
42
|
-
fout.puts premailer.to_inline_css
|
|
43
|
-
end
|
|
34
|
+
File.write "output.html", premailer.to_inline_css
|
|
44
35
|
|
|
45
36
|
# Output any CSS warnings
|
|
46
37
|
premailer.warnings.each do |w|
|
|
@@ -50,17 +41,13 @@ end
|
|
|
50
41
|
|
|
51
42
|
## Adapters
|
|
52
43
|
|
|
53
|
-
Premailer's default adapter is nokogiri if both nokogiri and nokogumbo are included in the Gemfile list. However, if you want to use a different adapter, you can choose to.
|
|
54
|
-
|
|
55
|
-
There are three adapters in total (as of premailer 1.10.0)
|
|
56
|
-
|
|
57
44
|
1. nokogiri (default)
|
|
58
|
-
2. nokogiri_fast
|
|
45
|
+
2. nokogiri_fast (20x speed, more memory)
|
|
59
46
|
3. nokogumbo
|
|
60
47
|
|
|
61
|
-
hpricot adapter removed
|
|
48
|
+
(hpricot adapter removed, use `~>1.9.0` version if you need it)
|
|
62
49
|
|
|
63
|
-
|
|
50
|
+
Picking an adapter:
|
|
64
51
|
|
|
65
52
|
```ruby
|
|
66
53
|
Premailer::Adapter.use = :nokogiri_fast
|
|
@@ -97,22 +84,125 @@ will result in
|
|
|
97
84
|
<table cellspacing='5' width='500'>
|
|
98
85
|
```
|
|
99
86
|
|
|
100
|
-
##
|
|
87
|
+
## Plain text version
|
|
88
|
+
|
|
89
|
+
Premailer can generate a plain text version of your HTML. Links and images will be inlined.
|
|
90
|
+
|
|
91
|
+
For example
|
|
92
|
+
|
|
93
|
+
```html
|
|
94
|
+
<a href="https://example.com" >
|
|
95
|
+
<img src="https://github.com/premailer.png" alt="Premailer Logo" />
|
|
96
|
+
</a>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
will become
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
Premailer Logo ( https://example.com )
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
To ignore/omit a section of HTML content from the plain text version, wrap it with the following comments.
|
|
101
106
|
|
|
102
|
-
|
|
107
|
+
```html
|
|
108
|
+
<!-- start text/html -->
|
|
109
|
+
<p>This will be omitted from the plain text version.</p>
|
|
110
|
+
<p>
|
|
111
|
+
This is extremely helpful for <strong>removing email headers and footers</strong>
|
|
112
|
+
that aren't needed in the text version.
|
|
113
|
+
</p>
|
|
114
|
+
<!-- end text/html -->
|
|
115
|
+
```
|
|
103
116
|
|
|
104
|
-
|
|
117
|
+
## Configuration options
|
|
105
118
|
|
|
119
|
+
For example:
|
|
106
120
|
```ruby
|
|
107
|
-
|
|
121
|
+
Premailer.new(
|
|
122
|
+
html, # html as string
|
|
123
|
+
with_html_string: true,
|
|
124
|
+
drop_unmergeable_css_rules: true
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
[available options](https://premailer.github.io/premailer/Premailer.html#initialize-instance_method)
|
|
129
|
+
|
|
130
|
+
## Support for CSS variables
|
|
131
|
+
|
|
132
|
+
The gem does not automatically replace CSS variables with their static values.
|
|
133
|
+
|
|
134
|
+
For example, if a variable is used to set the `font-weight` of an `h1` element, the result will be
|
|
135
|
+
```html
|
|
136
|
+
<h1 style="
|
|
137
|
+
font-size:3rem;
|
|
138
|
+
font-weight:var(--bulma-content-heading-weight);">
|
|
139
|
+
Title</h1>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
This causes the `font-weight` value to be the CSS variable call `var(--bulma-content-heading-weight);` instead of its static value.
|
|
143
|
+
|
|
144
|
+
### Replace CSS variable calls with their static values
|
|
145
|
+
|
|
146
|
+
The following section instructs how to replace CSS variables with their static value in the context of a Ruby on Rails application.
|
|
147
|
+
|
|
148
|
+
Install the `postcss-css-variables` plugin for PostCSS to process the CSS variables.
|
|
149
|
+
|
|
150
|
+
```shell
|
|
151
|
+
yarn add postcss postcss-cli postcss-css-variables
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
To configure the plugin, create the file `postcss.config.js` in the root directory with the content:
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
module.exports = {
|
|
158
|
+
plugins: [
|
|
159
|
+
// https://github.com/MadLittleMods/postcss-css-variables to transform the css
|
|
160
|
+
require("postcss-css-variables")({
|
|
161
|
+
preserve: false, // Set to false to replace variables with static values
|
|
162
|
+
}),
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
In the `package.json` file, add the new "build:emails" to the scripts.<br>Replace `./app/assets/stylesheets/emails.css` with your file path:
|
|
168
|
+
```json
|
|
169
|
+
"scripts": {
|
|
170
|
+
"build:emails": "postcss ./app/assets/stylesheets/emails.css -o ./app/assets/builds/emails.css"
|
|
171
|
+
}
|
|
108
172
|
```
|
|
109
173
|
|
|
110
|
-
|
|
174
|
+
The previous script processes and overwrites the file at `./app/assets/stylesheets/emails.css` with PostCSS using its `postcss-css-variables` plugin, replacing the CSS variables with their static value.
|
|
111
175
|
|
|
176
|
+
If the file to be processed is not `.css`, but `.scss`, it needs to be converted first to `.css`, then have its variables replaced. The script would then be
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
"scripts": {
|
|
180
|
+
"build:emails": "sass ./app/assets/stylesheets/emails.scss:./app/assets/builds/emails.css --no-source-map --load-path=node_modules && postcss ./app/assets/builds/emails.css -o ./app/assets/builds/emails.css"
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Next, to execute the script when running `bin/dev`, add the following line in the file `Procfile.dev`
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
emails_css: yarn build:emails --watch
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The srcipt can also be executed separately with the command
|
|
191
|
+
|
|
192
|
+
```shell
|
|
193
|
+
yarn build:emails
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Caveat
|
|
197
|
+
|
|
198
|
+
The variables must be declared before use. Otherwise, their values when called will be set to `undefined`.
|
|
112
199
|
|
|
113
200
|
## Contributions
|
|
114
201
|
|
|
115
|
-
Contributions are most welcome.
|
|
202
|
+
Contributions are most welcome.
|
|
203
|
+
Premailer was rotting away in a private SVN repository for too long and could use some TLC.
|
|
204
|
+
Fork and patch to your heart's content.
|
|
205
|
+
Please don't increment the version numbers.
|
|
116
206
|
|
|
117
207
|
A few areas that are particularly in need of love:
|
|
118
208
|
|
|
@@ -129,4 +219,3 @@ and to [Campaign Monitor](https://www.campaignmonitor.com/) for supporting the w
|
|
|
129
219
|
The source code can be found on [GitHub](https://github.com/premailer/premailer).
|
|
130
220
|
|
|
131
221
|
Copyright by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2007-2017. See [LICENSE.md](https://github.com/premailer/premailer/blob/master/LICENSE.md) for license details.
|
|
132
|
-
|
data/bin/premailer
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require 'nokogiri'
|
|
2
3
|
|
|
3
4
|
class Premailer
|
|
4
5
|
module Adapter
|
|
5
6
|
# Nokogiri adapter
|
|
6
7
|
module Nokogiri
|
|
8
|
+
WIDTH_AND_HIGHT = ['width', 'height'].freeze
|
|
7
9
|
|
|
8
10
|
include AdapterHelper::RgbToHex
|
|
9
11
|
# Merge CSS into the HTML document.
|
|
@@ -13,42 +15,67 @@ class Premailer
|
|
|
13
15
|
doc = @processed_doc
|
|
14
16
|
@unmergable_rules = CssParser::Parser.new
|
|
15
17
|
|
|
18
|
+
# Accumulate matched rule sets per node in memory instead of
|
|
19
|
+
# round-tripping them through style attributes as [SPEC=n[...]]
|
|
20
|
+
# string markers (which costs two libxml2 attribute round-trips per
|
|
21
|
+
# element plus a regex re-scan, and dominates allocation churn).
|
|
22
|
+
rules_by_node = {}
|
|
23
|
+
|
|
16
24
|
# Give all styles already in style attributes a specificity of 1000
|
|
17
25
|
# per http://www.w3.org/TR/CSS21/cascade.html#specificity
|
|
26
|
+
# Identical style strings (repeated components) share one rule set so
|
|
27
|
+
# the fold cache below can hit on them.
|
|
28
|
+
rule_set_cache = {}
|
|
18
29
|
doc.search("*[@style]").each do |el|
|
|
19
|
-
|
|
30
|
+
style = el.attributes['style'].to_s
|
|
31
|
+
rs = rule_set_cache.fetch(style) { rule_set_cache[style] = build_rule_set(style, 1000) }
|
|
32
|
+
rules_by_node[el] = rs ? [rs] : []
|
|
20
33
|
end
|
|
21
34
|
# Iterate through the rules and merge them into the HTML
|
|
22
35
|
@css_parser.each_selector(:all) do |selector, declaration, specificity, media_types|
|
|
23
36
|
# Save un-mergable rules separately
|
|
24
|
-
selector.gsub!(/:link([\s]*)+/i) { |
|
|
37
|
+
selector.gsub!(/:link([\s]*)+/i) { |_m| $1 }
|
|
25
38
|
|
|
26
39
|
# Convert element names to lower case
|
|
27
|
-
selector.gsub!(/([\s]|^)([\w]+)/) { |
|
|
40
|
+
selector.gsub!(/([\s]|^)([\w]+)/) { |_m| $1.to_s + $2.to_s.downcase }
|
|
28
41
|
|
|
29
|
-
if Premailer.
|
|
30
|
-
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration), media_types) unless @options[:preserve_styles]
|
|
42
|
+
if Premailer.media_query?(media_types) || selector =~ Premailer::RE_UNMERGABLE_SELECTORS
|
|
43
|
+
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selectors: selector, block: declaration), media_types) unless @options[:preserve_styles]
|
|
31
44
|
else
|
|
32
45
|
begin
|
|
33
|
-
if selector
|
|
46
|
+
if Premailer::RE_RESET_SELECTORS.match?(selector) && !!@options[:preserve_reset]
|
|
34
47
|
# this is in place to preserve the MailChimp CSS reset: http://github.com/mailchimp/Email-Blueprints/
|
|
35
48
|
# however, this doesn't mean for testing pur
|
|
36
|
-
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration))
|
|
49
|
+
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selectors: selector, block: declaration))
|
|
37
50
|
end
|
|
38
51
|
|
|
39
52
|
# Change single ID CSS selectors into xpath so that we can match more
|
|
40
53
|
# than one element. Added to work around dodgy generated code.
|
|
41
54
|
selector.gsub!(/\A\#([\w_\-]+)\Z/, '*[@id=\1]')
|
|
42
55
|
|
|
56
|
+
rule_set = nil
|
|
57
|
+
rule_set_missing = false
|
|
43
58
|
doc.search(selector).each do |el|
|
|
44
|
-
if el.elem?
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
if el.elem? && ((el.name != 'head') && (el.parent.name != 'head'))
|
|
60
|
+
# Enroll the element even when the rule below fails to build:
|
|
61
|
+
# the marker implementation wrote the style attribute before
|
|
62
|
+
# parsing, so matched elements end up with style="" when
|
|
63
|
+
# their only rule is invalid and exceptions are disabled.
|
|
64
|
+
rules = (rules_by_node[el] ||= [])
|
|
65
|
+
next if rule_set_missing
|
|
66
|
+
|
|
67
|
+
# One shared rule set per CSS rule; merge never mutates inputs
|
|
68
|
+
# (CachedRuleSet#expand_shorthand! is idempotent by design).
|
|
69
|
+
rule_set ||= build_rule_set(declaration, specificity)
|
|
70
|
+
if rule_set.nil?
|
|
71
|
+
rule_set_missing = true
|
|
72
|
+
next
|
|
73
|
+
end
|
|
74
|
+
rules << rule_set
|
|
48
75
|
end
|
|
49
76
|
end
|
|
50
77
|
rescue ::Nokogiri::SyntaxError, RuntimeError, ArgumentError
|
|
51
|
-
|
|
78
|
+
warn "CSS syntax error with selector: #{selector}" if @options[:verbose]
|
|
52
79
|
next
|
|
53
80
|
end
|
|
54
81
|
end
|
|
@@ -57,65 +84,35 @@ class Premailer
|
|
|
57
84
|
# Remove script tags
|
|
58
85
|
doc.search("script").remove if @options[:remove_scripts]
|
|
59
86
|
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if el[html_attr].nil? and not merged[css_attr].empty?
|
|
82
|
-
new_val = merged[css_attr].dup
|
|
83
|
-
|
|
84
|
-
# Remove url() function wrapper
|
|
85
|
-
new_val.gsub!(/url\((['"])(.*?)\1\)/, '\2')
|
|
86
|
-
|
|
87
|
-
# Remove !important, trailing semi-colon, and leading/trailing whitespace
|
|
88
|
-
new_val.gsub!(/;$|\s*!important/, '').strip!
|
|
89
|
-
|
|
90
|
-
# For width and height tags, remove px units
|
|
91
|
-
new_val.gsub!(/(\d+)px/, '\1') if %w[width height].include?(html_attr)
|
|
92
|
-
|
|
93
|
-
# For color-related tags, convert RGB to hex if specified by options
|
|
94
|
-
new_val = ensure_hex(new_val) if css_attr.end_with?('color') && @options[:rgb_to_hex_attributes]
|
|
95
|
-
|
|
96
|
-
el[html_attr] = new_val
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
unless @options[:preserve_style_attribute]
|
|
100
|
-
merged.instance_variable_get("@declarations").tap do |declarations|
|
|
101
|
-
declarations.delete(css_attr)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|
|
87
|
+
# Perform style folding. Elements sharing the same matched rule sets
|
|
88
|
+
# (ubiquitous in table-based email markup) fold to the same result, so
|
|
89
|
+
# the merge/expand/collapse/serialize pipeline runs once per unique
|
|
90
|
+
# (element name, rule list) pair instead of once per element.
|
|
91
|
+
fold_cache = {}
|
|
92
|
+
rules_by_node.each do |el, declarations|
|
|
93
|
+
related = Premailer::RELATED_ATTRIBUTES.key?(el.name) && @options[:css_to_attributes]
|
|
94
|
+
# Rule sets are interned above, so default identity hashing makes
|
|
95
|
+
# them usable directly as cache key elements.
|
|
96
|
+
cache_key = [related ? el.name : nil, *declarations]
|
|
97
|
+
|
|
98
|
+
folded = fold_cache[cache_key] ||= fold_declarations(declarations, related ? el.name : nil)
|
|
99
|
+
style, attributes = folded
|
|
100
|
+
|
|
101
|
+
# Write the inline STYLE attribute first so the attribute order for
|
|
102
|
+
# elements that had no style attribute matches the marker-based
|
|
103
|
+
# implementation (style attr was created during selector matching).
|
|
104
|
+
el['style'] = style
|
|
105
|
+
|
|
106
|
+
attributes&.each do |html_attr, value|
|
|
107
|
+
el[html_attr] = value if el[html_attr].nil?
|
|
105
108
|
end
|
|
106
|
-
|
|
107
|
-
# Collapse multiple rules into one as much as possible.
|
|
108
|
-
merged.create_shorthand! if @options[:create_shorthands]
|
|
109
|
-
|
|
110
|
-
# write the inline STYLE attribute
|
|
111
|
-
el['style'] = merged.declarations_to_s
|
|
112
109
|
end
|
|
113
110
|
|
|
114
111
|
doc = write_unmergable_css_rules(doc, @unmergable_rules) unless @options[:drop_unmergeable_css_rules]
|
|
115
112
|
|
|
116
|
-
if @options[:remove_classes]
|
|
113
|
+
if @options[:remove_classes] || @options[:remove_comments]
|
|
117
114
|
doc.traverse do |el|
|
|
118
|
-
if el.comment?
|
|
115
|
+
if el.comment? && @options[:remove_comments]
|
|
119
116
|
el.remove
|
|
120
117
|
elsif el.element?
|
|
121
118
|
el.remove_attribute('class') if @options[:remove_classes]
|
|
@@ -127,7 +124,7 @@ class Premailer
|
|
|
127
124
|
# find all anchor's targets and hash them
|
|
128
125
|
targets = []
|
|
129
126
|
doc.search("a[@href^='#']").each do |el|
|
|
130
|
-
target = el.get_attribute('href')[1
|
|
127
|
+
target = el.get_attribute('href')[1..]
|
|
131
128
|
targets << target
|
|
132
129
|
el.set_attribute('href', "#" + Digest::SHA256.hexdigest(target))
|
|
133
130
|
end
|
|
@@ -149,7 +146,7 @@ class Premailer
|
|
|
149
146
|
end
|
|
150
147
|
|
|
151
148
|
@processed_doc = doc
|
|
152
|
-
if
|
|
149
|
+
if xhtml?
|
|
153
150
|
# we don't want to encode carriage returns
|
|
154
151
|
@processed_doc.to_xhtml(:encoding => @options[:output_encoding]).gsub(/&\#(xD|13);/i, "\r")
|
|
155
152
|
else
|
|
@@ -157,6 +154,72 @@ class Premailer
|
|
|
157
154
|
end
|
|
158
155
|
end
|
|
159
156
|
|
|
157
|
+
# Merge a list of rule sets into a final style string plus the HTML
|
|
158
|
+
# attribute duplications (bgcolor/align/...) for RELATED_ATTRIBUTES
|
|
159
|
+
# elements. Element-independent: the per-element "attribute already
|
|
160
|
+
# set" guard stays with the caller.
|
|
161
|
+
def fold_declarations(declarations, related_el_name) # :nodoc:
|
|
162
|
+
merged = CssParser.merge(declarations)
|
|
163
|
+
if merged.equal?(declarations[0])
|
|
164
|
+
# CssParser.merge returns its input untouched when given a single
|
|
165
|
+
# rule set; copy it before the destructive fold pipeline below so
|
|
166
|
+
# rule sets shared across elements are not corrupted.
|
|
167
|
+
merged = CssParser::RuleSet.new(block: merged.declarations_to_s, specificity: merged.specificity)
|
|
168
|
+
end
|
|
169
|
+
begin
|
|
170
|
+
merged.expand_shorthand!
|
|
171
|
+
rescue ArgumentError => e
|
|
172
|
+
raise e if @options[:rule_set_exceptions]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
attributes = nil
|
|
176
|
+
# Duplicate CSS attributes as HTML attributes
|
|
177
|
+
if related_el_name
|
|
178
|
+
Premailer::RELATED_ATTRIBUTES[related_el_name].each do |css_attr, html_attr|
|
|
179
|
+
unless merged[css_attr].empty?
|
|
180
|
+
new_val = merged[css_attr].dup
|
|
181
|
+
|
|
182
|
+
# Remove url() function wrapper
|
|
183
|
+
new_val.gsub!(/url\((['"])(.*?)\1\)/, '\2')
|
|
184
|
+
|
|
185
|
+
# Remove !important, trailing semi-colon, and leading/trailing whitespace
|
|
186
|
+
new_val.gsub!(/;$|\s*!important/, '').strip!
|
|
187
|
+
|
|
188
|
+
# For width and height tags, remove px units
|
|
189
|
+
new_val.gsub!(/(\d+)px/, '\1') if WIDTH_AND_HIGHT.include?(html_attr)
|
|
190
|
+
|
|
191
|
+
# For color-related tags, convert RGB to hex if specified by options
|
|
192
|
+
new_val = ensure_hex(new_val) if css_attr.end_with?('color') && @options[:rgb_to_hex_attributes]
|
|
193
|
+
|
|
194
|
+
(attributes ||= []) << [html_attr, new_val]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
unless @options[:preserve_style_attribute]
|
|
198
|
+
merged.instance_variable_get(:@declarations).tap do |declarations|
|
|
199
|
+
declarations.delete(css_attr)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Collapse multiple rules into one as much as possible.
|
|
206
|
+
merged.create_shorthand! if @options[:create_shorthands]
|
|
207
|
+
|
|
208
|
+
# write the inline STYLE attribute
|
|
209
|
+
[merged.declarations_to_s, attributes]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Build an expanded rule set for folding. Returns nil (and optionally
|
|
213
|
+
# swallows the error, mirroring the old fold-time rescue) on bad CSS.
|
|
214
|
+
def build_rule_set(block, specificity) # :nodoc:
|
|
215
|
+
rs = Premailer::CachedRuleSet.new(block: block, specificity: specificity)
|
|
216
|
+
rs.expand_shorthand!
|
|
217
|
+
rs
|
|
218
|
+
rescue ArgumentError => e
|
|
219
|
+
raise e if @options[:rule_set_exceptions]
|
|
220
|
+
nil
|
|
221
|
+
end
|
|
222
|
+
|
|
160
223
|
# Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
|
|
161
224
|
# and write it into the <tt>head</tt>.
|
|
162
225
|
#
|
|
@@ -171,17 +234,16 @@ class Premailer
|
|
|
171
234
|
style_tag.content = styles
|
|
172
235
|
doc.add_child(style_tag)
|
|
173
236
|
else
|
|
174
|
-
style_tag = doc.create_element "style",
|
|
237
|
+
style_tag = doc.create_element "style", styles.to_s
|
|
175
238
|
head = doc.at_css('head')
|
|
176
|
-
head ||= doc.root.first_element_child.add_previous_sibling(doc.create_element
|
|
177
|
-
head ||= doc.add_child(doc.create_element
|
|
239
|
+
head ||= doc.root.first_element_child.add_previous_sibling(doc.create_element("head")) if doc.root&.first_element_child
|
|
240
|
+
head ||= doc.add_child(doc.create_element("head"))
|
|
178
241
|
head << style_tag
|
|
179
242
|
end
|
|
180
243
|
end
|
|
181
244
|
doc
|
|
182
245
|
end
|
|
183
246
|
|
|
184
|
-
|
|
185
247
|
# Converts the HTML document to a format suitable for plain-text e-mail.
|
|
186
248
|
#
|
|
187
249
|
# If present, uses the <body> element as its base; otherwise uses the whole document.
|
|
@@ -191,17 +253,17 @@ class Premailer
|
|
|
191
253
|
html_src = ''
|
|
192
254
|
begin
|
|
193
255
|
html_src = @doc.at("body").inner_html
|
|
194
|
-
rescue
|
|
256
|
+
rescue StandardError
|
|
195
257
|
end
|
|
196
258
|
|
|
197
|
-
html_src = @doc.to_html unless html_src
|
|
259
|
+
html_src = @doc.to_html unless html_src && !html_src.empty?
|
|
198
260
|
convert_to_text(html_src, @options[:line_length], @html_encoding)
|
|
199
261
|
end
|
|
200
262
|
|
|
201
263
|
# Gets the original HTML as a string.
|
|
202
264
|
# @return [String] HTML.
|
|
203
265
|
def to_s
|
|
204
|
-
if
|
|
266
|
+
if xhtml?
|
|
205
267
|
@doc.to_xhtml(:encoding => nil)
|
|
206
268
|
else
|
|
207
269
|
@doc.to_html(:encoding => nil)
|
|
@@ -215,13 +277,13 @@ class Premailer
|
|
|
215
277
|
thing = nil
|
|
216
278
|
|
|
217
279
|
# TODO: duplicate options
|
|
218
|
-
if @options[:with_html_string]
|
|
280
|
+
if @options[:with_html_string] || @options[:inline] || input.respond_to?(:read)
|
|
219
281
|
thing = input
|
|
220
282
|
elsif @is_local_file
|
|
221
283
|
@base_dir = File.dirname(input)
|
|
222
284
|
thing = File.open(input, 'r')
|
|
223
285
|
else
|
|
224
|
-
thing = URI.
|
|
286
|
+
thing = URI.parse(input).open
|
|
225
287
|
end
|
|
226
288
|
|
|
227
289
|
if thing.respond_to?(:read)
|
|
@@ -232,7 +294,8 @@ class Premailer
|
|
|
232
294
|
doc = nil
|
|
233
295
|
|
|
234
296
|
# Handle HTML entities
|
|
235
|
-
if @options[:replace_html_entities] == true
|
|
297
|
+
if (@options[:replace_html_entities] == true) && thing.is_a?(String)
|
|
298
|
+
thing = +thing
|
|
236
299
|
HTML_ENTITIES.map do |entity, replacement|
|
|
237
300
|
thing.gsub! entity, replacement
|
|
238
301
|
end
|
|
@@ -241,21 +304,20 @@ class Premailer
|
|
|
241
304
|
doc = if @options[:html_fragment]
|
|
242
305
|
::Nokogiri::HTML.fragment(thing, encoding)
|
|
243
306
|
else
|
|
244
|
-
::Nokogiri::HTML(thing, nil, encoding
|
|
307
|
+
::Nokogiri::HTML(thing, nil, encoding, &:recover)
|
|
245
308
|
end
|
|
246
309
|
|
|
247
310
|
# Fix for removing any CDATA tags from both style and script tags inserted per
|
|
248
311
|
# https://github.com/sparklemotion/nokogiri/issues/311 and
|
|
249
312
|
# https://github.com/premailer/premailer/issues/199
|
|
250
|
-
|
|
313
|
+
['style', 'script'].each do |tag|
|
|
251
314
|
doc.search(tag).children.each do |child|
|
|
252
|
-
child.swap(child.text
|
|
315
|
+
child.swap(child.text) if child.cdata?
|
|
253
316
|
end
|
|
254
317
|
end
|
|
255
318
|
|
|
256
319
|
doc
|
|
257
320
|
end
|
|
258
|
-
|
|
259
321
|
end
|
|
260
322
|
end
|
|
261
323
|
end
|