roadie 3.2.2 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5817fde135bcf056bf3f9ef1d5f28b99dd6362ca
4
- data.tar.gz: 663c031f765c9a3aff26b247040ce184cd37f234
3
+ metadata.gz: 2a5d2f3eb66ab40ecf551f53e5b7df59321c2805
4
+ data.tar.gz: 82c0b5241461df56c8810dca075245a75a0acf99
5
5
  SHA512:
6
- metadata.gz: 38cf12788dc10e050a87bee1c8ba68a5a21254ff4a049369b22735f39bf9bee08253f4045b7db78c224a425fd3ca7ed430caf7a43a17a84c5142e01d8c6724e5
7
- data.tar.gz: 2b5b336462dc8c9a53d39137c563925764dead15242c3f8cc09313d8f5487e242d4ea231fe591a433f0b30b4033d9f96fdc469ef2ca03cc1adc4f52387fad9b3
6
+ metadata.gz: e8c1a2ed3e74d586b95e341472b4c80e18e58ae01a094955b7dc102cd2cfe7c76ec1172159e1a1138a09adea465ac17ca1b0fcbde1f42680c46252cffee977ec
7
+ data.tar.gz: 777552240c002247354dad9c1bcc6ac6fb78ab62774d7e4fb3fe2e1a7aa49f855a67f3586b0178d0b99455c5d0e9547d72d3a30f460e3f8ecec6cabd9f437125
@@ -5,8 +5,12 @@ rvm:
5
5
  - 2.2
6
6
  - 2.3
7
7
  - 2.4
8
+ - 2.5
8
9
  - jruby
9
10
  - rbx
11
+ before_install:
12
+ - gem update --system # Need for Ruby 2.5.0. https://github.com/travis-ci/travis-ci/issues/8978
13
+ - gem install bundler
10
14
  matrix:
11
15
  allow_failures:
12
16
  # Rubinius and JRuby have a lot of trouble and no large following, so I'm going to
@@ -1,11 +1,25 @@
1
1
  ### dev
2
2
 
3
- [full changelog](https://github.com/Mange/roadie/compare/v3.2.2...master)
3
+ [full changelog](https://github.com/Mange/roadie/compare/v3.3.0...master)
4
4
 
5
5
  * Nothing yet.
6
6
 
7
7
  ### 3.2.2
8
8
 
9
+ [full changelog](https://github.com/Mange/roadie/compare/v3.2.2...v3.3.0)
10
+
11
+ * Enhancements
12
+ * Allow transforming to XHTML instead of HTML - [Zhivko Draganov](https://github.com/zdraganov) (#144).
13
+ * Support partial HTML documents (fragments) - #147
14
+ * With the help of [andfx](https://github.com/andfx) - #115
15
+ * With the help of [Frida Sjoholm](https://github.com/andf://github.com/FridaSjoholm) - #146
16
+ * Skip URL rewriting on elements with `data-roadie-ignore` - #154.
17
+ * With the help of [Hamed Asghari](https://github.com/hasghari) - #138.
18
+ * Bug fixes:
19
+ * Apply correct string encoding / charset in `NetHttpProvider` - [Jeremy Nagel](https://github.com/jeznag) (#152).
20
+
21
+ ### 3.2.2
22
+
9
23
  [full changelog](https://github.com/Mange/roadie/compare/v3.2.1...v3.2.2)
10
24
 
11
25
  * Enhancements
data/README.md CHANGED
@@ -5,7 +5,6 @@ Roadie
5
5
  [![Code Climate](https://codeclimate.com/github/Mange/roadie.png)](https://codeclimate.com/github/Mange/roadie)
6
6
  [![Code coverage status](https://codecov.io/github/Mange/roadie/coverage.svg?branch=master)](https://codecov.io/github/Mange/roadie?branch=master)
7
7
  [![Gem Version](https://badge.fury.io/rb/roadie.png)](http://badge.fury.io/rb/roadie)
8
- [![Dependency Status](https://gemnasium.com/Mange/roadie.png)](https://gemnasium.com/Mange/roadie)
9
8
 
10
9
  **Note: This README details the 3.x version of Roadie. You might be using 2.x, which is much older and only for Rails.**
11
10
 
@@ -23,7 +22,11 @@ This gem makes this easier by automatically inlining stylesheets into the docume
23
22
 
24
23
  "Dynamic" selectors (`:hover`, `:visited`, `:focus`, etc.), or selectors not understood by Nokogiri will be inlined into a single `<style>` element for those email clients that support it. This changes specificity a great deal for these rules, so it might not work 100% out of the box. (See more about this below)
25
24
 
26
- Roadie also rewrites all relative URLs in the email to an absolute counterpart, making images you insert and those referenced in your stylesheets work. No more headaches about how to write the stylesheets while still having them work with emails from your acceptance environments.
25
+ Roadie also rewrites all relative URLs in the email to an absolute counterpart,
26
+ making images you insert and those referenced in your stylesheets work. No more
27
+ headaches about how to write the stylesheets while still having them work with
28
+ emails from your acceptance environments. You can disable this on specific
29
+ elements using a `data-roadie-ignore` marker.
27
30
 
28
31
  Features
29
32
  --------
@@ -35,9 +38,12 @@ Features
35
38
  * Keeps `:hover` and friends around in a separate `<style>` element.
36
39
  * Makes image urls absolute.
37
40
  * Hostname and port configurable on a per-environment basis.
41
+ * Can be disabled on individual elements.
38
42
  * Makes link `href`s and `img` `src`s absolute.
39
43
  * Automatically adds proper HTML skeleton when missing; you don't have to create a layout for emails.
44
+ * Also supports HTML fragments / partial documents, where layout is not added.
40
45
  * Allows you to inject stylesheets in a number of ways, at runtime.
46
+ * Removes `data-roadie-ignore` markers before finishing the HTML.
41
47
 
42
48
  Install & Usage
43
49
  ---------------
@@ -51,10 +57,17 @@ gem 'roadie', '~> 3.2'
51
57
  You can then create a new instance of a Roadie document:
52
58
 
53
59
  ```ruby
60
+ # Transform full documents with the #transform method.
54
61
  document = Roadie::Document.new "<html><body></body></html>"
55
62
  document.add_css "body { color: green; }"
56
63
  document.transform
57
64
  # => "<html><body style=\"color:green;\"></body></html>"
65
+
66
+ # Transform partial documents with #transform_partial.
67
+ document = Roadie::Document.new "<div>Hello world!</div>"
68
+ document.add_css "div { color: green; }"
69
+ document.transform_partial
70
+ # => "<div style=\"color:green;\">Hello world!</div>"
58
71
  ```
59
72
 
60
73
  Your document instance can be configured with several options:
@@ -83,6 +96,14 @@ The following URLs will be rewritten for you:
83
96
  * `img[src]` (HTML)
84
97
  * `url()` (CSS)
85
98
 
99
+ You can disable individual elements by adding an `data-roadie-ignore` marker on
100
+ them. CSS will still be inlined on those elements, but URLs will not be
101
+ rewritten.
102
+
103
+ ```html
104
+ <a href="|UNSUBSCRIBE_URL|" data-roadie-ignore>Unsubscribe</a>
105
+ ```
106
+
86
107
  ### Referenced stylesheets ###
87
108
 
88
109
  By default, `style` and `link` elements in the email document's `head` are processed along with the stylesheets and removed from the `head`.
@@ -177,7 +198,9 @@ The `NetHttpProvider` will download the URLs that is is given using Ruby's stand
177
198
  You can give it a whitelist of hosts that downloads are allowed from:
178
199
 
179
200
  ```ruby
180
- document.external_asset_providers << Roadie::NetHttpProvider.new(whitelist: ["myapp.com", "assets.myapp.com", "cdn.cdnnetwork.co.jp"])
201
+ document.external_asset_providers << Roadie::NetHttpProvider.new(
202
+ whitelist: ["myapp.com", "assets.myapp.com", "cdn.cdnnetwork.co.jp"],
203
+ )
181
204
  document.external_asset_providers << Roadie::NetHttpProvider.new # Allows every host
182
205
  ```
183
206
 
@@ -331,11 +354,18 @@ end
331
354
 
332
355
  ### Keeping CSS that is impossible to inline
333
356
 
334
- Some CSS is impossible to inline properly. `:hover` and `::after` comes to mind. Roadie tries its best to keep these around by injecting them inside a new `<style>` element in the `<head>`.
357
+ Some CSS is impossible to inline properly. `:hover` and `::after` comes to
358
+ mind. Roadie tries its best to keep these around by injecting them inside a new
359
+ `<style>` element in the `<head>` (or at the beginning of the partial if
360
+ transforming a partial document).
335
361
 
336
- The problem here is that Roadie cannot possible adjust the specificity for you, so they will not apply the same way as they did before the styles were inlined.
362
+ The problem here is that Roadie cannot possible adjust the specificity for you,
363
+ so they will not apply the same way as they did before the styles were inlined.
337
364
 
338
- Another caveat is that a lot of email clients does not support this (which is the entire point of inlining in the first place), so don't put anything important in here. Always handle the case of these selectors not being part of the email.
365
+ Another caveat is that a lot of email clients does not support this (which is
366
+ the entire point of inlining in the first place), so don't put anything
367
+ important in here. Always handle the case of these selectors not being part of
368
+ the email.
339
369
 
340
370
  #### Specificity problems ####
341
371
 
@@ -374,10 +404,26 @@ class TrackNewsletterLinks
374
404
  end
375
405
  end
376
406
 
377
- document.before_transformation = proc { |dom, document| logger.debug "Inlining document with title #{dom.at_css('head > title').try(:text)}" }
407
+ document.before_transformation = ->(dom, document) {
408
+ logger.debug "Inlining document with title #{dom.at_css('head > title').try(:text)}"
409
+ }
378
410
  document.after_transformation = TrackNewsletterLinks.new
379
411
  ```
380
412
 
413
+ ### XHTML vs HTML ###
414
+
415
+ You can configure the underlying HTML/XML engine to output XHTML or HTML (which
416
+ is the default). One usecase for this is that `{` tokens usually gets escaped
417
+ to `&#123;`, which would be a problem if you then pass the resulting HTML on to
418
+ some other templating engine that uses those tokens (like Handlebars or Mustache).
419
+
420
+ ```ruby
421
+ document.mode = :xhtml
422
+ ```
423
+
424
+ This will also affect the emitted `<!DOCTYPE>` if transforming a full document.
425
+ Partial documents does not have a `<!DOCTYPE>`.
426
+
381
427
  Build Status
382
428
  ------------
383
429
 
@@ -418,6 +464,40 @@ Instructions on how to do this on most platforms, see [Nokogiri's official insta
418
464
 
419
465
  The CSS Parser used in Roadie does not handle keyframes. I don't think any email clients do either, but if you want to keep on trying you can add them manually to a `<style>` element (or a separate referenced stylesheet) and [tell Roadie not to touch them](#referenced-stylesheets).
420
466
 
467
+ ### How do I get rid of the `<body>` elements that are added?
468
+
469
+ It sounds like you want to transform a partial document. Maybe you are building
470
+ partials or template fragments to later place in other documents. Use
471
+ `Document#transform_partial` instead of `Document#transform` in order to treat
472
+ the HTML as a partial document.
473
+
474
+ ### Can I skip URL rewriting on a specific element?
475
+
476
+ If you add the `data-roadie-ignore` attribute on an element, URL rewriting will
477
+ not be performed on that element. This could be really useful for you if you
478
+ intend to send the email through some other rendering pipeline that replaces
479
+ some placeholders/variables.
480
+
481
+ ```html
482
+ <a href="/about-us">About us</a>
483
+ <a href="|UNSUBSCRIBE_URL|" data-roadie-ignore>Unsubscribe</a>
484
+ ```
485
+
486
+ Note that this will not skip CSS inlining on the element; it will still get the
487
+ correct styles applied.
488
+
489
+ ### What should I do about "Invalid URL" errors?
490
+
491
+ If the URL is invalid on purpose, see _Can I skip URL rewriting on a specific
492
+ element?_ above. Otherwise, you can try to parse it yourself using Ruby's `URI`
493
+ class and see if you can figure it out.
494
+
495
+ ```ruby
496
+ require "uri"
497
+ URI.parse("https://example.com/best image.jpg") # raises
498
+ URI.parse("https://example.com/best%20image.jpg") # Works!
499
+ ```
500
+
421
501
  Documentation
422
502
  -------------
423
503
 
@@ -462,7 +542,7 @@ License
462
542
 
463
543
  (The MIT License)
464
544
 
465
- Copyright (c) 2009-2017 Magnus Bergmark, Jim Neath / Purify, and contributors.
545
+ Copyright (c) 2009-2018 Magnus Bergmark, Jim Neath / Purify, and contributors.
466
546
 
467
547
  * [Magnus Bergmark](https://github.com/Mange) <magnus.bergmark@gmail.com>
468
548
 
@@ -46,7 +46,6 @@ module Roadie
46
46
  element.remove if stylesheet
47
47
  stylesheet
48
48
  }.compact
49
- remove_ignore_markers
50
49
  stylesheets
51
50
  end
52
51
 
@@ -102,11 +101,5 @@ module Roadie
102
101
 
103
102
  true
104
103
  end
105
-
106
- def remove_ignore_markers
107
- @dom.css("[data-roadie-ignore]").each do |node|
108
- node.remove_attribute "data-roadie-ignore"
109
- end
110
- end
111
104
  end
112
105
  end
@@ -13,6 +13,8 @@ module Roadie
13
13
  # The internal stylesheet is used last and gets the highest priority. The
14
14
  # rest is used in the same order as browsers are supposed to use them.
15
15
  #
16
+ # The execution methods are {#transform} and {#transform_partial}.
17
+ #
16
18
  # @attr [#call] before_transformation Callback to call just before {#transform}ation begins. Will be called with the parsed DOM tree and the {Document} instance.
17
19
  # @attr [#call] after_transformation Callback to call just before {#transform}ation is completed. Will be called with the current DOM tree and the {Document} instance.
18
20
  class Document
@@ -27,6 +29,9 @@ module Roadie
27
29
  # Should CSS that cannot be inlined be kept in a new `<style>` element in `<head>`?
28
30
  attr_accessor :keep_uninlinable_css
29
31
 
32
+ # The mode to generate markup in. Valid values are `:html` (default) and `:xhtml`.
33
+ attr_reader :mode
34
+
30
35
  # @param [String] html the input HTML
31
36
  def initialize(html)
32
37
  @keep_uninlinable_css = true
@@ -34,6 +39,7 @@ module Roadie
34
39
  @asset_providers = ProviderList.wrap(FilesystemProvider.new)
35
40
  @external_asset_providers = ProviderList.empty
36
41
  @css = ""
42
+ @mode = :html
37
43
  end
38
44
 
39
45
  # Append additional CSS to the document's internal stylesheet.
@@ -42,18 +48,21 @@ module Roadie
42
48
  @css << "\n\n" << new_css
43
49
  end
44
50
 
45
- # Transform the input HTML and returns the processed HTML.
51
+ # Transform the input HTML as a full document and returns the processed
52
+ # HTML.
46
53
  #
47
54
  # Before the transformation begins, the {#before_transformation} callback
48
55
  # will be called with the parsed HTML tree and the {Document} instance, and
49
56
  # after all work is complete the {#after_transformation} callback will be
50
57
  # invoked in the same way.
51
58
  #
52
- # Most of the work is delegated to other classes. A list of them can be seen below.
59
+ # Most of the work is delegated to other classes. A list of them can be
60
+ # seen below.
53
61
  #
54
62
  # @see MarkupImprover MarkupImprover (improves the markup of the DOM)
55
63
  # @see Inliner Inliner (inlines the stylesheets)
56
64
  # @see UrlRewriter UrlRewriter (rewrites URLs and makes them absolute)
65
+ # @see #transform_partial Transforms partial documents (fragments)
57
66
  #
58
67
  # @return [String] the transformed HTML
59
68
  def transform
@@ -62,7 +71,41 @@ module Roadie
62
71
  callback before_transformation, dom
63
72
 
64
73
  improve dom
65
- inline dom
74
+ inline dom, keep_uninlinable_in: :head
75
+ rewrite_urls dom
76
+
77
+ callback after_transformation, dom
78
+
79
+ remove_ignore_markers dom
80
+ serialize_document dom
81
+ end
82
+
83
+ # Transform the input HTML as a HTML fragment/partial and returns the
84
+ # processed HTML.
85
+ #
86
+ # Before the transformation begins, the {#before_transformation} callback
87
+ # will be called with the parsed HTML tree and the {Document} instance, and
88
+ # after all work is complete the {#after_transformation} callback will be
89
+ # invoked in the same way.
90
+ #
91
+ # The main difference between this and {#transform} is that this does not
92
+ # treat the HTML as a full document and does not try to fix it by adding
93
+ # doctypes, {<head>} elements, etc.
94
+ #
95
+ # Most of the work is delegated to other classes. A list of them can be
96
+ # seen below.
97
+ #
98
+ # @see Inliner Inliner (inlines the stylesheets)
99
+ # @see UrlRewriter UrlRewriter (rewrites URLs and makes them absolute)
100
+ # @see #transform Transforms full documents
101
+ #
102
+ # @return [String] the transformed HTML
103
+ def transform_partial
104
+ dom = Nokogiri::HTML.fragment html
105
+
106
+ callback before_transformation, dom
107
+
108
+ inline dom, keep_uninlinable_in: :root
66
109
  rewrite_urls dom
67
110
 
68
111
  callback after_transformation, dom
@@ -80,7 +123,23 @@ module Roadie
80
123
  @external_asset_providers = ProviderList.wrap(list)
81
124
  end
82
125
 
126
+ # Change the mode. The mode affects how the resulting markup is generated.
127
+ #
128
+ # Valid modes:
129
+ # `:html` (default)
130
+ # `:xhtml`
131
+ def mode=(mode)
132
+ if VALID_MODES.include?(mode)
133
+ @mode = mode
134
+ else
135
+ raise ArgumentError, "Invalid mode #{mode.inspect}. Valid modes are: #{VALID_MODES.inspect}"
136
+ end
137
+ end
138
+
83
139
  private
140
+ VALID_MODES = %i[html xhtml].freeze
141
+ private_constant :VALID_MODES
142
+
84
143
  def stylesheet
85
144
  Stylesheet.new "(Document styles)", @css
86
145
  end
@@ -89,9 +148,13 @@ module Roadie
89
148
  MarkupImprover.new(dom, html).improve
90
149
  end
91
150
 
92
- def inline(dom)
151
+ def inline(dom, options = {})
152
+ keep_uninlinable_in = options.fetch(:keep_uninlinable_in)
93
153
  dom_stylesheets = AssetScanner.new(dom, asset_providers, external_asset_providers).extract_css
94
- Inliner.new(dom_stylesheets + [stylesheet], dom).inline(keep_uninlinable_css)
154
+ Inliner.new(dom_stylesheets + [stylesheet], dom).inline(
155
+ keep_uninlinable_css: keep_uninlinable_css,
156
+ keep_uninlinable_in: keep_uninlinable_in,
157
+ )
95
158
  end
96
159
 
97
160
  def rewrite_urls(dom)
@@ -101,10 +164,17 @@ module Roadie
101
164
  def serialize_document(dom)
102
165
  # #dup is called since it fixed a few segfaults in certain versions of Nokogiri
103
166
  save_options = Nokogiri::XML::Node::SaveOptions
167
+ format = {
168
+ html: save_options::AS_HTML,
169
+ xhtml: save_options::AS_XHTML,
170
+ }.fetch(mode)
171
+
104
172
  dom.dup.to_html(
105
173
  save_with: (
106
- save_options::NO_DECLARATION | save_options::NO_EMPTY_TAGS | save_options::AS_HTML
107
- )
174
+ save_options::NO_DECLARATION |
175
+ save_options::NO_EMPTY_TAGS |
176
+ format
177
+ ),
108
178
  )
109
179
  end
110
180
 
@@ -127,5 +197,11 @@ module Roadie
127
197
  end
128
198
  end
129
199
  end
200
+
201
+ def remove_ignore_markers(dom)
202
+ dom.css("[data-roadie-ignore]").each do |node|
203
+ node.remove_attribute "data-roadie-ignore"
204
+ end
205
+ end
130
206
  end
131
207
  end
@@ -24,13 +24,20 @@ module Roadie
24
24
 
25
25
  # Start the inlining, mutating the DOM tree.
26
26
  #
27
- # @param [true, false] keep_extra_blocks
27
+ # @option options [true, false] :keep_uninlinable_css
28
+ # @option options [:root, :head] :keep_uninlinable_in
28
29
  # @return [nil]
29
- def inline(keep_extra_blocks = true)
30
+ def inline(options = {})
31
+ keep_uninlinable_css = options.fetch(:keep_uninlinable_css, true)
32
+ keep_uninlinable_in = options.fetch(:keep_uninlinable_in, :head)
33
+
30
34
  style_map, extra_blocks = consume_stylesheets
31
35
 
32
36
  apply_style_map(style_map)
33
- add_styles_to_head(extra_blocks) if keep_extra_blocks
37
+
38
+ if keep_uninlinable_css
39
+ add_uninlinable_styles(keep_uninlinable_in, extra_blocks)
40
+ end
34
41
 
35
42
  nil
36
43
  end
@@ -89,21 +96,31 @@ module Roadie
89
96
  nil
90
97
  end
91
98
 
92
- def add_styles_to_head(blocks)
93
- unless blocks.empty?
94
- create_style_element(blocks, find_head)
95
- end
99
+ def add_uninlinable_styles(parent, blocks)
100
+ return if blocks.empty?
101
+
102
+ parent_node =
103
+ case parent
104
+ when :head
105
+ find_head
106
+ when :root
107
+ dom
108
+ else
109
+ raise ArgumentError, "Parent must be either :head or :root. Was #{parent.inspect}"
110
+ end
111
+
112
+ create_style_element(blocks, parent_node)
96
113
  end
97
114
 
98
115
  def find_head
99
116
  dom.at_xpath('html/head')
100
117
  end
101
118
 
102
- def create_style_element(style_blocks, head)
103
- return unless head
104
- element = Nokogiri::XML::Node.new("style", head.document)
119
+ def create_style_element(style_blocks, parent)
120
+ return unless parent
121
+ element = Nokogiri::XML::Node.new("style", parent.document)
105
122
  element.content = style_blocks.join("\n")
106
- head.add_child(element)
123
+ parent.add_child(element)
107
124
  end
108
125
 
109
126
  # @api private