pakyow-mailer 0.10.2 → 0.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dd01a34dce235ce0d202baa48d429678dad59dd4
4
- data.tar.gz: b503b8a7f8b607e5fe8a73774dcaf217fd99587d
3
+ metadata.gz: d0b74faa0bb8adc9f4f7016b3b5fc9973c3450a8
4
+ data.tar.gz: 0ed46bc6a8dc4ac35434b6deb381aea87e7dd3d3
5
5
  SHA512:
6
- metadata.gz: 4740d3b49e9af8d7358f6091a547179ff208f2698bb5f080c722ae03175d4fb3d04d55a1e7104837d5413ace94541a04edba4ced83e79315a6143a1c0f54ae7c
7
- data.tar.gz: 58d6509129fe30e199d51660407fb2525f7841cc0f81db601ea976ec5de4acea95ca8ae73782a6361d714fae1e6cd661d2fe8b4d82ec9cf1a91606f71c04c0d8
6
+ metadata.gz: 16c2a60c86fd4f10ce099359b7667412fc691e9c3c234e948619912cc06a6a3de5c58f4df9b65113c1875284dccd624eb2b86d48c9cdeab40832c52c364a65e5
7
+ data.tar.gz: 505ed535070e22021d6aad5dc4e8a6660a7fde7c2da6015d8871cb202c509c0f45cc0bc1d446284ca43b545a9c4c98759f1aa256e6814a3fdfbd60b7a629b1af
@@ -1,3 +1,8 @@
1
+ # 0.11.0
2
+
3
+ * Adds support to use mutators on a mailer view
4
+ * Moves everything into the Pakyow namespace
5
+
1
6
  # 0.10.0 / 2015-10-19
2
7
 
3
8
  * Prevent styles from being mixed into plaintext emails
@@ -1,11 +1 @@
1
- libdir = File.dirname(__FILE__)
2
- $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
3
-
4
- # Gems
5
- require 'mail'
6
- require 'premailer'
7
-
8
- # Base
9
- require 'mailer/mailer'
10
- require 'mailer/config/mailer'
11
- require 'mailer/helpers'
1
+ require 'pakyow/mailer'
@@ -0,0 +1,9 @@
1
+ require 'mail'
2
+ require 'premailer'
3
+
4
+ require 'pakyow/mailer/mailer'
5
+ require 'pakyow/mailer/config/mailer'
6
+ require 'pakyow/mailer/helpers'
7
+ require 'pakyow/mailer/ext/premailer/adapter/oga'
8
+
9
+ Premailer::Adapter.use = :oga
@@ -1,9 +1,9 @@
1
- Pakyow::Config.register(:mailer) { |config|
1
+ Pakyow::Config.register :mailer do |config|
2
2
  # the default sender name
3
3
  config.opt :default_sender, 'Pakyow'
4
4
 
5
5
  # the default mimetype to use
6
- config.opt :default_content_type, lambda { 'text/html; charset=' + Pakyow::Config.mailer.encoding }
6
+ config.opt :default_content_type, -> { 'text/html; charset=' + Pakyow::Config.mailer.encoding }
7
7
 
8
8
  # the delivery method to use for sending mail
9
9
  config.opt :delivery_method, :sendmail
@@ -13,4 +13,4 @@ Pakyow::Config.register(:mailer) { |config|
13
13
 
14
14
  # the default encoding to use
15
15
  config.opt :encoding, 'UTF-8'
16
- }
16
+ end
@@ -0,0 +1,303 @@
1
+ require 'oga'
2
+
3
+ module Pakyow
4
+ class OgaAttributes
5
+ def initialize(attributes)
6
+ @attributes = attributes
7
+ end
8
+
9
+ def [](key)
10
+ if key.is_a?(Integer)
11
+ super
12
+ else
13
+ @attributes.find { |a| a.name.to_sym == key.to_sym }
14
+ end
15
+ end
16
+
17
+ def method_missing(*args, &block)
18
+ @attributes.send(*args, &block)
19
+ end
20
+ end
21
+ end
22
+
23
+ module Oga
24
+ module XML
25
+ class Document
26
+ # Premailer builds its css searches with `@` prepended to an attribute,
27
+ # which breaks Oga. Since Premailer also expects `search` to be defined
28
+ # on a document, we define it and just strip the `@` out before calling
29
+ # Oga's build-in #css method.
30
+ def search(query)
31
+ css(query.gsub('@', ''))
32
+ end
33
+ end
34
+
35
+ class Element
36
+ def attributes
37
+ Pakyow::OgaAttributes.new(@attributes)
38
+ end
39
+
40
+ def []=(name, value)
41
+ set(name, value)
42
+ end
43
+
44
+ def [](name)
45
+ get(name)
46
+ end
47
+ end
48
+
49
+ class Attribute
50
+ def to_str
51
+ value
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ class Premailer
58
+ # This method was tied to a specific type of document rather than being
59
+ # adapter-agnostic (honestly it's a stretch to call them adapters). So,
60
+ # we just pulled this method in and made it work w/ Oga.
61
+ def load_css_from_html! # :nodoc:
62
+ tags = @doc.css("link[rel='stylesheet']").reject { |tag|
63
+ tag.attribute('data-premailer') == 'ignore'
64
+ }
65
+
66
+ if tags
67
+ tags.each do |tag|
68
+ if tag.to_s.strip =~ /^\<link/i && tag.attribute('href') && media_type_ok?(tag.attribute('media')) && @options[:include_link_tags]
69
+ # A user might want to <link /> to a local css file that is also mirrored on the site
70
+ # but the local one is different (e.g. newer) than the live file, premailer will now choose the local file
71
+
72
+ if tag.attribute('href').to_s.include? @base_url.to_s and @html_file.kind_of?(String)
73
+ if @options[:with_html_string]
74
+ link_uri = tag.attribute('href').to_s.sub(@base_url.to_s, '')
75
+ else
76
+ link_uri = File.join(File.dirname(@html_file), tag.attribute('href').to_s.sub!(@base_url.to_s, ''))
77
+ # if the file does not exist locally, try to grab the remote reference
78
+ unless File.exists?(link_uri)
79
+ link_uri = Premailer.resolve_link(tag.attribute('href').to_s, @html_file)
80
+ end
81
+ end
82
+ else
83
+ link_uri = tag.attribute('href').to_s
84
+ end
85
+
86
+ if Premailer.local_data?(link_uri)
87
+ $stderr.puts "Loading css from local file: " + link_uri if @options[:verbose]
88
+ load_css_from_local_file!(link_uri)
89
+ else
90
+ $stderr.puts "Loading css from uri: " + link_uri if @options[:verbose]
91
+ @css_parser.load_uri!(link_uri, {:only_media_types => [:screen, :handheld]})
92
+ end
93
+
94
+ elsif tag.to_s.strip =~ /^\<style/i && @options[:include_style_tags]
95
+ @css_parser.add_block!(tag.inner_html, :base_uri => @base_url, :base_dir => @base_dir, :only_media_types => [:screen, :handheld])
96
+ end
97
+ end
98
+ unless @options[:preserve_styles]
99
+ tags.each do |tag|
100
+ tag.remove
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ module Adapter
107
+ # Oga adapter
108
+ module Oga
109
+
110
+ # Merge CSS into the HTML document.
111
+ #
112
+ # @return [String] an HTML.
113
+ def to_inline_css
114
+ doc = @processed_doc
115
+ @unmergable_rules = CssParser::Parser.new
116
+
117
+ # Give all styles already in style attributes a specificity of 1000
118
+ # per http://www.w3.org/TR/CSS21/cascade.html#specificity
119
+ doc.css("*[style]").each do |el|
120
+ el['style'] = '[SPEC=1000[' + el.attribute('style') + ']]'
121
+ end
122
+ # Iterate through the rules and merge them into the HTML
123
+ @css_parser.each_selector(:all) do |selector, declaration, specificity, media_types|
124
+ # Save un-mergable rules separately
125
+ selector.gsub!(/:link([\s]*)+/i) { |m| $1 }
126
+
127
+ # Convert element names to lower case
128
+ selector.gsub!(/([\s]|^)([\w]+)/) { |m| $1.to_s + $2.to_s.downcase }
129
+
130
+ if Premailer.is_media_query?(media_types) || selector =~ Premailer::RE_UNMERGABLE_SELECTORS
131
+ @unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration), media_types) unless @options[:preserve_styles]
132
+ else
133
+ begin
134
+ if selector =~ Premailer::RE_RESET_SELECTORS
135
+ # this is in place to preserve the MailChimp CSS reset: http://github.com/mailchimp/Email-Blueprints/
136
+ # however, this doesn't mean for testing pur
137
+ @unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration)) unless !@options[:preserve_reset]
138
+ end
139
+
140
+ # Change single ID CSS selectors into xpath so that we can match more
141
+ # than one element. Added to work around dodgy generated code.
142
+ selector.gsub!(/\A\#([\w_\-]+)\Z/, '*[@id=\1]')
143
+
144
+ doc.css(selector).each do |el|
145
+ if el.elem? and (el.name != 'head' and el.parent.name != 'head')
146
+ # Add a style attribute or append to the existing one
147
+ block = "[SPEC=#{specificity}[#{declaration}]]"
148
+ el['style'] = (el.attribute('style').to_s ||= '') + ' ' + block
149
+ end
150
+ end
151
+ rescue RuntimeError, ArgumentError
152
+ $stderr.puts "CSS syntax error with selector: #{selector}" if @options[:verbose]
153
+ next
154
+ end
155
+ end
156
+ end
157
+
158
+ # Remove script tags
159
+ if @options[:remove_scripts]
160
+ doc.css("script").remove
161
+ end
162
+
163
+ # Read STYLE attributes and perform folding
164
+ doc.css("*[style]").each do |el|
165
+ style = el.attribute('style').to_s
166
+
167
+ declarations = []
168
+ style.scan(/\[SPEC\=([\d]+)\[(.[^\]\]]*)\]\]/).each do |declaration|
169
+ rs = CssParser::RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
170
+ declarations << rs
171
+ end
172
+
173
+ # Perform style folding
174
+ merged = CssParser.merge(declarations)
175
+ merged.expand_shorthand!
176
+
177
+ # Duplicate CSS attributes as HTML attributes
178
+ if Premailer::RELATED_ATTRIBUTES.has_key?(el.name) && @options[:css_to_attributes]
179
+ Premailer::RELATED_ATTRIBUTES[el.name].each do |css_att, html_att|
180
+ el[html_att] = merged[css_att].gsub(/url\(['|"](.*)['|"]\)/, '\1').gsub(/;$|\s*!important/, '').strip if el[html_att].nil? and not merged[css_att].empty?
181
+ merged.instance_variable_get("@declarations").tap do |declarations|
182
+ declarations.delete(css_att)
183
+ end
184
+ end
185
+ end
186
+ # Collapse multiple rules into one as much as possible.
187
+ merged.create_shorthand! if @options[:create_shorthands]
188
+
189
+ # write the inline STYLE attribute
190
+ attributes = Premailer.escape_string(merged.declarations_to_s).split(';').map(&:strip)
191
+ attributes = attributes.map { |attr| [attr.split(':').first, attr] }.sort_by { |pair| pair.first }.map { |pair| pair[1] }
192
+ el['style'] = attributes.join('; ')
193
+ end
194
+
195
+ doc = write_unmergable_css_rules(doc, @unmergable_rules)
196
+
197
+ if @options[:remove_classes] or @options[:remove_comments]
198
+ doc.traverse do |el|
199
+ if el.comment? and @options[:remove_comments]
200
+ el.remove
201
+ elsif el.element?
202
+ el.unset('class') if @options[:remove_classes]
203
+ end
204
+ end
205
+ end
206
+
207
+ if @options[:remove_ids]
208
+ # find all anchor's targets and hash them
209
+ targets = []
210
+ doc.css("a[@href^='#']").each do |el|
211
+ target = el.get_attribute('href')[1..-1]
212
+ targets << target
213
+ el.set('href', "#" + Digest::MD5.hexdigest(target))
214
+ end
215
+ # hash ids that are links target, delete others
216
+ doc.css("*[@id]").each do |el|
217
+ id = el.get_attribute('id')
218
+ if targets.include?(id)
219
+ el.set('id', Digest::MD5.hexdigest(id))
220
+ else
221
+ el.unset('id')
222
+ end
223
+ end
224
+ end
225
+
226
+ if @options[:reset_contenteditable]
227
+ doc.css('*[contenteditable]').each do |el|
228
+ el.unset('contenteditable')
229
+ end
230
+ end
231
+
232
+ @processed_doc = doc
233
+ @processed_doc.to_xml
234
+ end
235
+
236
+ # Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
237
+ # and write it into the <tt>body</tt>.
238
+ #
239
+ # <tt>doc</tt> is an Oga document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
240
+ #
241
+ # @return [::Oga::XML::Document] a document.
242
+ def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
243
+ styles = unmergable_rules.to_s
244
+
245
+ unless styles.empty?
246
+ style_tag = "<style type=\"text/css\">\n#{styles}</style>"
247
+ unless (body = doc.css('body')).empty?
248
+ if doc.at_css('body').children && !doc.at_css('body').children.empty?
249
+ doc.at_css('body').children.before(::Oga.parse_html(style_tag))
250
+ else
251
+ doc.at_css('body').add_child(::Oga.parse_html(style_tag))
252
+ end
253
+ else
254
+ doc.inner_html = style_tag += doc.inner_html
255
+ end
256
+ end
257
+ doc
258
+ end
259
+
260
+
261
+ # Converts the HTML document to a format suitable for plain-text e-mail.
262
+ #
263
+ # If present, uses the <body> element as its base; otherwise uses the whole document.
264
+ #
265
+ # @return [String] a plain text.
266
+ def to_plain_text
267
+ html_src = ''
268
+ begin
269
+ html_src = @doc.at("body").inner_html
270
+ rescue;
271
+ end
272
+
273
+ html_src = @doc.to_xml unless html_src and not html_src.empty?
274
+ convert_to_text(html_src, @options[:line_length], @html_encoding)
275
+ end
276
+
277
+ # Gets the original HTML as a string.
278
+ # @return [String] HTML.
279
+ def to_s
280
+ @doc.to_xml
281
+ end
282
+
283
+ # Load the HTML file and convert it into an Oga document.
284
+ #
285
+ # @return [::Oga::XML::Document] a document.
286
+ def load_html(input) # :nodoc:
287
+ thing = nil
288
+
289
+ # TODO: duplicate options
290
+ if @options[:with_html_string] or @options[:inline] or input.respond_to?(:read)
291
+ thing = input
292
+ elsif @is_local_file
293
+ @base_dir = File.dirname(input)
294
+ thing = File.open(input, 'r')
295
+ else
296
+ thing = open(input)
297
+ end
298
+
299
+ ::Oga.parse_html(thing)
300
+ end
301
+ end
302
+ end
303
+ end
@@ -0,0 +1,7 @@
1
+ module Pakyow
2
+ module Helpers
3
+ def mailer(view_path)
4
+ Mailer.from_store(view_path, @presenter.store, self)
5
+ end
6
+ end
7
+ end
@@ -2,8 +2,9 @@ module Pakyow
2
2
  class Mailer
3
3
  attr_accessor :view, :message, :processed
4
4
 
5
- def self.from_store(view_path, view_store)
6
- new(view: view_store.view(view_path))
5
+ def self.from_store(view_path, view_store, context = nil)
6
+ view = view_store.view(view_path)
7
+ new(view: Pakyow::Presenter::ViewContext.new(view, context))
7
8
  end
8
9
 
9
10
  def initialize(view: nil, content: nil)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pakyow-mailer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.2
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bryan Powell
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-11-16 00:00:00.000000000 Z
12
+ date: 2016-04-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: pakyow-support
@@ -17,42 +17,42 @@ dependencies:
17
17
  requirements:
18
18
  - - '='
19
19
  - !ruby/object:Gem::Version
20
- version: 0.10.2
20
+ version: 0.11.0
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - '='
26
26
  - !ruby/object:Gem::Version
27
- version: 0.10.2
27
+ version: 0.11.0
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: pakyow-core
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
32
  - - '='
33
33
  - !ruby/object:Gem::Version
34
- version: 0.10.2
34
+ version: 0.11.0
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - '='
40
40
  - !ruby/object:Gem::Version
41
- version: 0.10.2
41
+ version: 0.11.0
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: pakyow-presenter
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
46
  - - '='
47
47
  - !ruby/object:Gem::Version
48
- version: 0.10.2
48
+ version: 0.11.0
49
49
  type: :runtime
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
53
  - - '='
54
54
  - !ruby/object:Gem::Version
55
- version: 0.10.2
55
+ version: 0.11.0
56
56
  - !ruby/object:Gem::Dependency
57
57
  name: mail
58
58
  requirement: !ruby/object:Gem::Requirement
@@ -104,10 +104,12 @@ files:
104
104
  - pakyow-mailer/CHANGELOG.md
105
105
  - pakyow-mailer/LICENSE
106
106
  - pakyow-mailer/README.md
107
- - pakyow-mailer/lib/mailer/config/mailer.rb
108
- - pakyow-mailer/lib/mailer/helpers.rb
109
- - pakyow-mailer/lib/mailer/mailer.rb
110
107
  - pakyow-mailer/lib/pakyow-mailer.rb
108
+ - pakyow-mailer/lib/pakyow/mailer.rb
109
+ - pakyow-mailer/lib/pakyow/mailer/config/mailer.rb
110
+ - pakyow-mailer/lib/pakyow/mailer/ext/premailer/adapter/oga.rb
111
+ - pakyow-mailer/lib/pakyow/mailer/helpers.rb
112
+ - pakyow-mailer/lib/pakyow/mailer/mailer.rb
111
113
  homepage: http://pakyow.org
112
114
  licenses:
113
115
  - MIT
@@ -128,9 +130,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
128
130
  version: '0'
129
131
  requirements: []
130
132
  rubyforge_project:
131
- rubygems_version: 2.4.5
133
+ rubygems_version: 2.5.1
132
134
  signing_key:
133
135
  specification_version: 4
134
136
  summary: Pakyow Mailer
135
137
  test_files: []
136
- has_rdoc:
@@ -1,7 +0,0 @@
1
- module Pakyow
2
- module AppHelpers
3
- def mailer(view_path)
4
- Mailer.from_store(view_path, @presenter.store)
5
- end
6
- end
7
- end