inkcite 1.11.0 → 1.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +2 -2
  3. data/assets/init/helpers.tsv +7 -0
  4. data/assets/social/facebook.png +0 -0
  5. data/assets/social/pintrest.png +0 -0
  6. data/assets/social/twitter.png +0 -0
  7. data/inkcite.gemspec +5 -4
  8. data/lib/inkcite.rb +17 -6
  9. data/lib/inkcite/animation.rb +135 -0
  10. data/lib/inkcite/cli/base.rb +5 -14
  11. data/lib/inkcite/cli/build.rb +3 -3
  12. data/lib/inkcite/cli/init.rb +3 -3
  13. data/lib/inkcite/cli/preview.rb +25 -1
  14. data/lib/inkcite/cli/server.rb +1 -1
  15. data/lib/inkcite/cli/validate.rb +2 -11
  16. data/lib/inkcite/email.rb +1 -1
  17. data/lib/inkcite/mailer.rb +5 -0
  18. data/lib/inkcite/minifier.rb +58 -11
  19. data/lib/inkcite/renderer.rb +10 -2
  20. data/lib/inkcite/renderer/base.rb +45 -3
  21. data/lib/inkcite/renderer/button.rb +16 -12
  22. data/lib/inkcite/renderer/container_base.rb +14 -4
  23. data/lib/inkcite/renderer/element.rb +13 -3
  24. data/lib/inkcite/renderer/image.rb +46 -19
  25. data/lib/inkcite/renderer/image_base.rb +3 -3
  26. data/lib/inkcite/renderer/in_browser.rb +1 -1
  27. data/lib/inkcite/renderer/link.rb +91 -59
  28. data/lib/inkcite/renderer/lorem.rb +9 -1
  29. data/lib/inkcite/renderer/mobile_image.rb +3 -11
  30. data/lib/inkcite/renderer/mobile_only.rb +48 -0
  31. data/lib/inkcite/renderer/outlook_background.rb +61 -13
  32. data/lib/inkcite/renderer/property.rb +1 -1
  33. data/lib/inkcite/renderer/responsive.rb +10 -8
  34. data/lib/inkcite/renderer/snow.rb +20 -10
  35. data/lib/inkcite/renderer/social.rb +128 -0
  36. data/lib/inkcite/renderer/table.rb +13 -1
  37. data/lib/inkcite/renderer/table_base.rb +3 -3
  38. data/lib/inkcite/renderer/td.rb +32 -7
  39. data/lib/inkcite/renderer/video_preview.rb +257 -0
  40. data/lib/inkcite/uploader.rb +3 -3
  41. data/lib/inkcite/util.rb +19 -5
  42. data/lib/inkcite/version.rb +1 -1
  43. data/lib/inkcite/view.rb +29 -18
  44. data/lib/inkcite/view/context.rb +30 -0
  45. data/test/animation_spec.rb +38 -0
  46. data/test/email_spec.rb +0 -4
  47. data/test/minifier_spec.rb +243 -4
  48. data/test/parser_spec.rb +0 -4
  49. data/test/project/helpers.tsv +3 -0
  50. data/test/renderer/button_spec.rb +15 -13
  51. data/test/renderer/div_spec.rb +13 -4
  52. data/test/renderer/element_spec.rb +1 -5
  53. data/test/renderer/footnote_spec.rb +0 -4
  54. data/test/renderer/image_spec.rb +27 -11
  55. data/test/renderer/link_spec.rb +14 -4
  56. data/test/renderer/lorem_spec.rb +0 -4
  57. data/test/renderer/mobile_image_spec.rb +3 -11
  58. data/test/renderer/mobile_only_spec.rb +21 -0
  59. data/test/renderer/mobile_style_spec.rb +1 -5
  60. data/test/renderer/outlook_background_spec.rb +61 -0
  61. data/test/renderer/redacted_spec.rb +0 -4
  62. data/test/renderer/social_spec.rb +53 -0
  63. data/test/renderer/span_spec.rb +0 -4
  64. data/test/renderer/table_spec.rb +8 -4
  65. data/test/renderer/td_spec.rb +0 -4
  66. data/test/renderer/video_preview_spec.rb +19 -0
  67. data/test/renderer_spec.rb +0 -4
  68. data/test/test_helper.rb +7 -0
  69. data/test/view_spec.rb +12 -4
  70. metadata +89 -56
@@ -20,7 +20,7 @@ module Inkcite
20
20
  color = opt[:color]
21
21
 
22
22
  # Optional call-to-action override - otherwise defaults to view in browser.
23
- cta = opt[:cta] || 'View in Browser'
23
+ cta = opt[:cta] || ctx.production?? 'View in Browser' : 'Preview in Browser'
24
24
 
25
25
  id = opt[:id] || 'in-browser'
26
26
 
@@ -18,7 +18,7 @@ module Inkcite
18
18
 
19
19
  # Check to see if the declaration has been marked as a block
20
20
  # element and if so, close the div.
21
- html << '</div>' if opening[:block]
21
+ html << '</div>' if opening[:div]
22
22
 
23
23
  return html
24
24
  end
@@ -41,17 +41,82 @@ module Inkcite
41
41
  # including font, background color, border, etc.
42
42
  mix_all a, opt, ctx
43
43
 
44
- id = opt[:id]
45
- href = opt[:href]
44
+ id, href, target_blank = Link.process(opt[:id], opt[:href], opt[:force], ctx)
45
+
46
+ a[:target] = BLANK if target_blank
47
+
48
+ # Make sure that these types of links have quotes.
49
+ href = quote(href) unless ctx.text?
50
+
51
+ # Set the href attribute to the resolved href.
52
+ a[:href] = href
53
+
54
+ # Links never get any text decoration.
55
+ a.style[TEXT_DECORATION] = NONE
56
+
57
+ # Force the display: block attribute if the boolean block parameter has
58
+ # been specified.
59
+ a.style[:display] = :block if opt[:block]
60
+
61
+ if ctx.browser?
62
+
63
+ # Programmatically we can install onclick listeners for hosted versions.
64
+ # Check to see if one is specified and the Javascript is permitted in
65
+ # this version.
66
+ onclick = opt[:onclick]
67
+ a[:onclick] = quote(onclick) unless onclick.blank?
68
+
69
+ end
70
+
71
+ html = ''
72
+
73
+ if ctx.text?
74
+ html << a[:href]
75
+
76
+ else
77
+
78
+ klass = opt[:class]
79
+ a.classes << klass unless klass.blank?
80
+
81
+ mix_responsive a, opt, ctx
82
+
83
+ # Some responsive modes (e.g. button) change the display type from in-line
84
+ # to block. This change can cause unexpected whitespace or other unexpected
85
+ # layout changes. Outlook doesn't support block display on link elements
86
+ # so the best workaround is simply to wrap the element in <div> tags.
87
+ if a.responsive_styles.any?(&:block?)
88
+ html << '<div>'
89
+
90
+ # Remember that we made this element block-display so that we can append
91
+ # the extra div when we close the tag.
92
+ opt[:div] = true
93
+
94
+ end
95
+
96
+ html << a.to_s
97
+
98
+ end
99
+
100
+ html
101
+ end
102
+
103
+ def self.process id, href, force, ctx
104
+
105
+ # Initially assume a blank target isn't needed
106
+ target_blank = false
46
107
 
47
108
  # If a URL wasn't provided in the HTML, then check to see if there is
48
109
  # a link declared in the project's links_tsv file. If so, we need to
49
110
  # duplicate it so that tagging doesn't get applied multiple times.
50
111
  if href.blank?
51
112
  links_tsv_href = ctx.links_tsv[id]
52
- href = links_tsv_href.dup unless links_tsv_href.blank?
113
+ href = links_tsv_href unless links_tsv_href.blank?
53
114
  end
54
115
 
116
+ # Always duplicate the string provided just to make sure we're not
117
+ # modifying a frozen link or adding tagging to a previously tagged HREF.
118
+ href = href.dup
119
+
55
120
  # True if the href is missing. If so, we may try to look it up by it's ID
56
121
  # or we'll insert a default TBD link.
57
122
  missing = href.blank?
@@ -89,10 +154,14 @@ module Inkcite
89
154
  # If we don't have a URL, check to see if we've encountered this
90
155
  href = last_href || ctx[MISSING_LINK_HREF]
91
156
 
92
- ctx.error "Link missing href", { :id => id } unless last_href
157
+ ctx.error 'Link missing href', { :id => id } unless last_href
93
158
 
94
159
  else
95
160
 
161
+ # Ensure the validity of the URL in the link to prevent problems -
162
+ # e.g. unexpected carriage return in the href.
163
+ ctx.error('Link href appears to be invalid', { :id => id, :href => href }) unless force || valid?(href)
164
+
96
165
  # Optionally tag the link's query string for post-send log analytics.
97
166
  href = add_tagging(id, href, ctx)
98
167
 
@@ -105,7 +174,7 @@ module Inkcite
105
174
 
106
175
  # It saves everyone a lot of time if you alert them that an ID appears multiple times
107
176
  # in the email and with mismatched URLs.
108
- ctx.error "Link href mismatch", { :id => id, :expected => last_href, :found => href }
177
+ ctx.error 'Link href mismatch', { :id => id, :expected => last_href, :found => href }
109
178
 
110
179
  end
111
180
 
@@ -116,59 +185,11 @@ module Inkcite
116
185
  # URLs interfering with the links file.
117
186
  href = add_tracking(id, href, ctx)
118
187
 
119
- a[:target] = BLANK
188
+ target_blank = true
120
189
 
121
190
  end
122
191
 
123
- # Make sure that these types of links have quotes.
124
- href = quote(href) unless ctx.text?
125
-
126
- # Set the href attribute to the resolved href.
127
- a[:href] = href
128
-
129
- # Links never get any text decoration.
130
- a.style[TEXT_DECORATION] = NONE
131
-
132
- if ctx.browser?
133
-
134
- # Programmatically we can install onclick listeners for hosted versions.
135
- # Check to see if one is specified and the Javascript is permitted in
136
- # this version.
137
- onclick = opt[:onclick]
138
- a[:onclick] = quote(onclick) unless onclick.blank?
139
-
140
- end
141
-
142
- html = ''
143
-
144
- if ctx.text?
145
- html << a[:href]
146
-
147
- else
148
-
149
- klass = opt[:class]
150
- a.classes << klass unless klass.blank?
151
-
152
- mix_responsive a, opt, ctx
153
-
154
- # Some responsive modes (e.g. button) change the display type from in-line
155
- # to block. This change can cause unexpected whitespace or other unexpected
156
- # layout changes. Outlook doesn't support block display on link elements
157
- # so the best workaround is simply to wrap the element in <div> tags.
158
- if a.responsive_styles.any?(&:block?)
159
- html << '<div>'
160
-
161
- # Remember that we made this element block-display so that we can append
162
- # the extra div when we close the tag.
163
- opt[:block] = true
164
-
165
- end
166
-
167
- html << a.to_s
168
-
169
- end
170
-
171
- html
192
+ [ id, href, target_blank ]
172
193
  end
173
194
 
174
195
  private
@@ -196,7 +217,7 @@ module Inkcite
196
217
  # Signifies an auto-incrementing link ID.
197
218
  PLUS_PLUS = '++'
198
219
 
199
- def add_tagging id, href, ctx
220
+ def self.add_tagging id, href, ctx
200
221
 
201
222
  # Check to see if we're tagging links.
202
223
  tag = ctx[TAG_LINKS]
@@ -214,7 +235,7 @@ module Inkcite
214
235
  href
215
236
  end
216
237
 
217
- def add_tracking id, href, ctx
238
+ def self.add_tracking id, href, ctx
218
239
 
219
240
  # Check to see if a trackable link string has been defined.
220
241
  tracking = ctx[Inkcite::Email::TRACK_LINKS]
@@ -226,7 +247,7 @@ module Inkcite
226
247
  href
227
248
  end
228
249
 
229
- def replace_tag tag, id, ctx
250
+ def self.replace_tag tag, id, ctx
230
251
 
231
252
  # Inject the link's ID into the tag - that's the only value that can't
232
253
  # be resolved from the context.
@@ -235,6 +256,17 @@ module Inkcite
235
256
  Inkcite::Renderer.render(tag, ctx)
236
257
  end
237
258
 
259
+ # Tests whether or not the href provided is a valid http(s) link.
260
+ # Courtest http://stackoverflow.com/questions/7167895/whats-a-good-way-to-validate-links-urls-in-rails
261
+ def self.valid? url
262
+ begin
263
+ uri = URI.parse(url)
264
+ uri.kind_of?(URI::HTTP)
265
+ rescue URI::InvalidURIError
266
+ false
267
+ end
268
+ end
269
+
238
270
  end
239
271
  end
240
272
  end
@@ -16,7 +16,7 @@ module Inkcite
16
16
  # we don't want it to ship accidentally.
17
17
  ctx.error 'Email contains Lorem Ipsum' unless opt[:force]
18
18
 
19
- if type == :headline
19
+ html = if type == :headline
20
20
  words = (limit || 4).to_i
21
21
  Faker::Lorem.words(words).join(SPACE).titlecase
22
22
 
@@ -30,6 +30,14 @@ module Inkcite
30
30
 
31
31
  end
32
32
 
33
+ # Replace the last space in the generated text with a
34
+ # non-breaking space so that the last two words always
35
+ # wrap together - no dangling words in rapid design mode.
36
+ if last_space = html.rindex(' ')
37
+ html[last_space] = '&nbsp;'
38
+ end
39
+
40
+ html
33
41
  end
34
42
 
35
43
  private
@@ -7,15 +7,6 @@ module Inkcite
7
7
  # Image swapping technique
8
8
  def render tag, opt, ctx
9
9
 
10
- tag_stack = ctx.tag_stack(:mobile_image)
11
-
12
- if tag == '/mobile-img'
13
- tag_stack.pop
14
- return '</span>'
15
- end
16
-
17
- tag_stack << opt
18
-
19
10
  # This is a transient, wrapper Element that we're going to use to
20
11
  # style the attributes of the object that will appear when the
21
12
  # email is viewed on a mobile device.
@@ -25,6 +16,8 @@ module Inkcite
25
16
 
26
17
  mix_background img, opt, ctx
27
18
 
19
+ mix_margins img, opt, ctx
20
+
28
21
  display = opt[:display]
29
22
  img.style[:display] = "#{display}" if display && display != BLOCK && display != DEFAULT
30
23
 
@@ -58,10 +51,9 @@ module Inkcite
58
51
  # Add the class that handles inserting the correct background image.
59
52
  ctx.media_query << span.add_rule(Rule.new('span', klass, img.style))
60
53
 
61
- span.to_s
54
+ span + '</span>'
62
55
  end
63
56
 
64
57
  end
65
58
  end
66
59
  end
67
-
@@ -0,0 +1,48 @@
1
+ # Fresh thinking on mobile-only content courtesy of FreshInbox's technique
2
+ # http://freshinbox.com/blog/bulletproof-solution-to-hiding-mobile-content-when-opened-in-non-mobile-email-clients/
3
+ module Inkcite
4
+ module Renderer
5
+ class MobileOnly < Responsive
6
+
7
+ def render tag, opt, ctx
8
+
9
+ # True if this is the open tag ({mobile-only})
10
+ is_open = tag == 'mobile-only'
11
+
12
+ html = ''
13
+
14
+ if is_open
15
+
16
+ # Intentionally NOT using 'mso-hide: all' version as it requires all
17
+ # nested tables to have that attribute applied. Why have all that extra
18
+ # markup - just use this simple conditional instead.
19
+ html << '<!--[if !mso 9]><!-->'
20
+
21
+ # These elements style the div such that it is invisible in all
22
+ # other major email clients.
23
+ div = Element.new('div')
24
+ div.style[:display] = 'none'
25
+ div.style[:'max-height'] = 0
26
+ div.style[:'overflow'] = 'hidden'
27
+
28
+ klass = opt[:inline] ? 'show-inline' : 'show'
29
+ mix_responsive_klass div, opt, ctx, klass
30
+
31
+ html << div.to_s
32
+
33
+ else
34
+
35
+ # Close the div
36
+ html << '</div>'
37
+
38
+ # Close the outlook conditional for the close tag.
39
+ html << '<!--<![endif]-->'
40
+
41
+ end
42
+
43
+ html
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -1,10 +1,19 @@
1
1
  module Inkcite
2
2
  module Renderer
3
- class OutlookBackground < Base
3
+
4
+ # Outlook background image support courtesy of @stigm via Campaign Monitor's
5
+ # Bulletproof Background Images: https://backgrounds.cm/
6
+ #
7
+ # {outlook-bg src=YJOX1PC.png bgcolor=#7bceeb height=92 width=120}
8
+ # ...
9
+ # {/outlook-bg}
10
+ #
11
+ class OutlookBackground < ImageBase
4
12
 
5
13
  def render tag, opt, ctx
6
14
 
7
- # Do nothing if vml is disabled globally.
15
+ # Do nothing if vml is disabled globally. Disable by setting
16
+ # 'vml: false' in config.yml
8
17
  return nil unless ctx.vml_enabled?
9
18
 
10
19
  html = '<!--[if gte mso 9]>'
@@ -16,22 +25,61 @@ module Inkcite
16
25
 
17
26
  else
18
27
 
19
- src = opt[:src]
20
- raise 'Outlook background missing required src attribute' if src.blank?
28
+ # Get the fully-qualified URL to the image or placeholder image if it's
29
+ # missing from the images directory.
30
+ src = image_url(opt[:src], opt, ctx, false)
31
+
32
+ rect = Element.new('v:rect', {
33
+ :'xmlns:v' => quote('urn:schemas-microsoft-com:vml'),
34
+ :fill => quote(true),
35
+ :stroke => quote(false)
36
+ })
21
37
 
22
- width = opt[:width].to_i
38
+ width = opt[:width]
23
39
  height = opt[:height].to_i
24
- raise "Outlook background requires dimensions: #{width}x#{height} " if width <= 0 || height <= 0
25
40
 
26
- html << render_tag('v:rect',
27
- { :'xmlns:v' => quote('urn:schemas-microsoft-com:vml'), :fill => quote(true), :stroke => quote(false) },
28
- { :width => px(width), :height => px(height) }
29
- )
41
+ # When width is omitted, set to 100% or marked as 'fill' then
42
+ # make the image fill the available space. It will tile.
43
+ if width.nil? || width == 'fill' || width == '100%' || width.to_i <= 0
44
+
45
+ # The number you pass to 'mso-width-percent' is ten times the percentage you'd like.
46
+ # https://www.emailonacid.com/blog/article/email-development/emailology_vector_markup_language_and_backgrounds
47
+ rect.style[:'mso-width-percent'] = 1000
48
+
49
+ else
50
+ rect.style[:width] = px(width)
51
+
52
+ end
53
+
54
+ # True if the height of the background image will fit to content within the
55
+ # background element (specified by omitting the 'height' attribute).
56
+ fit_to_shape = height <= 0
57
+ rect.style[:height] = px(height) unless fit_to_shape
58
+
59
+ fill = Element.new('v:fill', {
60
+ :type => '"tile"',
61
+ :src => src,
62
+ :self_close => true
63
+ })
64
+
65
+ # Check for a background color.
66
+ bgcolor = opt[:bgcolor]
67
+ fill[:color] = quote(hex(bgcolor)) unless bgcolor.blank?
68
+
69
+ textbox = Element.new('v:textbox', :inset => '"0,0,0,0"')
70
+ textbox.style[:'mso-fit-shape-to-text'] = 'true' if fit_to_shape
71
+
72
+ html << rect.to_s
73
+ html << fill.to_s
74
+ html << textbox.to_s
75
+
76
+ div = Element.new('div')
30
77
 
31
- html << render_tag('v:fill', { :type => 'tile', :src => quote(ctx.image_url(src)), :color => hex(opt[:bgcolor]), :self_close => true })
78
+ # Font family and other attributes get reset within the v:textbox so allow
79
+ # the font series of attributes to be applied.
80
+ mix_font div, opt, ctx
32
81
 
33
- html << render_tag('v:textbox', { :inset => '0,0,0,0' })
34
- html << '<div>'
82
+ html << div.to_s
35
83
 
36
84
  # Flag the context as having had VML used within it.
37
85
  ctx.vml_used!
@@ -22,7 +22,7 @@ module Inkcite
22
22
  # onto the stack. No need to push opts and pollute the stack if
23
23
  # there is no closing tag to take advantage of them.
24
24
  close_tag = "#{SLASH}#{tag}"
25
- ctx.tag_stack(tag) << opt unless ctx[close_tag].blank?
25
+ ctx.tag_stack(tag) << opt unless ctx[close_tag].nil?
26
26
 
27
27
  else
28
28
 
@@ -11,6 +11,7 @@ module Inkcite
11
11
  HIDE = 'hide'
12
12
  IMAGE = 'img'
13
13
  SHOW = 'show'
14
+ SHOW_INLINE = 'show-inline'
14
15
  SWITCH = 'switch'
15
16
  SWITCH_UP = 'switch-up'
16
17
  TOGGLE = 'toggle'
@@ -135,7 +136,8 @@ module Inkcite
135
136
  styles << Rule.new(UNIVERSAL, HIDE, 'display: none !important;', false)
136
137
 
137
138
  # SHOW, which means the element is hidden on desktop but shown on mobile.
138
- styles << Rule.new(UNIVERSAL, SHOW, 'display: block !important;', false)
139
+ styles << Rule.new('div', SHOW, 'display: block !important; max-height: none !important;', false)
140
+ styles << Rule.new('div', SHOW_INLINE, 'display: inline !important; max-height: none !important;', false)
139
141
 
140
142
  # Brian Graves' Column Drop Pattern: Table goes to 100% width by way of
141
143
  # the FILL rule and its cells stack vertically.
@@ -162,11 +164,10 @@ module Inkcite
162
164
 
163
165
  button_styles = {
164
166
  :color => "#{cfg.color} !important",
165
- :display => 'block',
166
- BACKGROUND_COLOR => cfg.bgcolor,
167
- TEXT_SHADOW => "0 -1px 0 #{cfg.text_shadow}"
167
+ :display => 'block'
168
168
  }
169
169
 
170
+ button_styles[BACKGROUND_COLOR] = cfg.bgcolor unless cfg.bgcolor.blank?
170
171
  button_styles[:border] = cfg.border unless cfg.border.blank?
171
172
  button_styles[BORDER_BOTTOM] = cfg.border_bottom if cfg.bevel > 0
172
173
  button_styles[BORDER_RADIUS] = Renderer.px(cfg.border_radius) unless cfg.border_radius.blank?
@@ -175,6 +176,7 @@ module Inkcite
175
176
  button_styles[MARGIN_TOP] = Renderer.px(cfg.margin_top) if cfg.margin_top > 0
176
177
  button_styles[:padding] = Renderer.px(cfg.padding) unless cfg.padding.blank?
177
178
  button_styles[TEXT_ALIGN] = 'center'
179
+ button_styles[TEXT_SHADOW] = "0 -1px 0 #{cfg.text_shadow}" unless cfg.text_shadow.blank?
178
180
 
179
181
  styles << Rule.new('a', BUTTON, button_styles, false)
180
182
 
@@ -211,6 +213,9 @@ module Inkcite
211
213
  line_height = detect_font(MOBILE_LINE_HEIGHT, font, opt, parent, ctx)
212
214
  style[LINE_HEIGHT] = "#{px(line_height)} !important" unless line_height.blank?
213
215
 
216
+ color = detect_font(MOBILE_FONT_COLOR, font, opt, parent, ctx)
217
+ style[:color] = "#{hex(color)} !important" unless color.blank?
218
+
214
219
  mix_responsive_style element, opt, ctx, Renderer.render_styles(style) unless style.blank?
215
220
 
216
221
  font
@@ -283,10 +288,6 @@ module Inkcite
283
288
 
284
289
  end
285
290
 
286
- # If the rule is SHOW (only on mobile) we need to restyle the element
287
- # so it is hidden.
288
- element.style[:display] = 'none' if klass == SHOW
289
-
290
291
  # Add the responsive rule to the element
291
292
  element.add_rule rule
292
293
 
@@ -347,6 +348,7 @@ module Inkcite
347
348
  UNIVERSAL = '*'
348
349
 
349
350
  # For font overrides on mobile devices.
351
+ MOBILE_FONT_COLOR = :'mobile-color'
350
352
  MOBILE_FONT_SIZE = :'mobile-font-size'
351
353
  MOBILE_LINE_HEIGHT = :'mobile-line-height'
352
354