inkcite 1.15.0 → 1.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -1
  3. data/inkcite.gemspec +0 -3
  4. data/lib/inkcite.rb +0 -1
  5. data/lib/inkcite/cli/base.rb +12 -8
  6. data/lib/inkcite/cli/preview.rb +0 -10
  7. data/lib/inkcite/cli/server.rb +9 -1
  8. data/lib/inkcite/email.rb +5 -10
  9. data/lib/inkcite/facade/animation.rb +4 -1
  10. data/lib/inkcite/image_minifier.rb +96 -0
  11. data/lib/inkcite/mailer.rb +2 -2
  12. data/lib/inkcite/minifier.rb +1 -1
  13. data/lib/inkcite/renderer.rb +10 -0
  14. data/lib/inkcite/renderer/base.rb +47 -2
  15. data/lib/inkcite/renderer/button.rb +15 -5
  16. data/lib/inkcite/renderer/container_base.rb +15 -1
  17. data/lib/inkcite/renderer/image.rb +1 -1
  18. data/lib/inkcite/renderer/image_base.rb +8 -0
  19. data/lib/inkcite/renderer/list.rb +104 -0
  20. data/lib/inkcite/renderer/mobile_image.rb +3 -0
  21. data/lib/inkcite/renderer/responsive.rb +2 -0
  22. data/lib/inkcite/renderer/slant.rb +207 -0
  23. data/lib/inkcite/renderer/snow.rb +15 -1
  24. data/lib/inkcite/renderer/special_effect.rb +7 -0
  25. data/lib/inkcite/renderer/sup.rb +18 -4
  26. data/lib/inkcite/renderer/table.rb +9 -1
  27. data/lib/inkcite/renderer/table_base.rb +20 -2
  28. data/lib/inkcite/renderer/td.rb +7 -2
  29. data/lib/inkcite/renderer/video_preview.rb +1 -1
  30. data/lib/inkcite/uploader.rb +40 -27
  31. data/lib/inkcite/version.rb +1 -1
  32. data/lib/inkcite/view.rb +133 -13
  33. data/lib/inkcite/view/context.rb +6 -1
  34. data/lib/inkcite/view/developer_scripts.rb +56 -0
  35. data/test/renderer/background_spec.rb +4 -4
  36. data/test/renderer/button_spec.rb +14 -8
  37. data/test/renderer/div_spec.rb +26 -3
  38. data/test/renderer/image_spec.rb +9 -4
  39. data/test/renderer/list_spec.rb +36 -0
  40. data/test/renderer/mobile_image_spec.rb +5 -0
  41. data/test/renderer/slant_spec.rb +47 -0
  42. data/test/renderer/span_spec.rb +12 -1
  43. data/test/renderer/table_spec.rb +1 -1
  44. data/test/renderer/td_spec.rb +24 -8
  45. data/test/renderer/trademark_spec.rb +6 -6
  46. metadata +11 -50
  47. data/lib/inkcite/image/base.rb +0 -38
  48. data/lib/inkcite/image/guetzli_minifier.rb +0 -62
  49. data/lib/inkcite/image/image_minifier.rb +0 -143
  50. data/lib/inkcite/image/image_optim_minifier.rb +0 -90
  51. data/lib/inkcite/image/mozjpeg_minifier.rb +0 -92
@@ -47,6 +47,10 @@ module Inkcite
47
47
  @opt[:font] || @ctx[BUTTON_FONT]
48
48
  end
49
49
 
50
+ def font_family
51
+ @opt[Base::FONT_FAMILY] || @ctx[BUTTON_FONT_FAMILY]
52
+ end
53
+
50
54
  def font_size
51
55
  (@opt[Base::FONT_SIZE] || @ctx[BUTTON_FONT_SIZE]).to_i
52
56
  end
@@ -96,6 +100,7 @@ module Inkcite
96
100
  BUTTON_COLOR = :'button-color'
97
101
  BUTTON_FLOAT = :'button-float'
98
102
  BUTTON_FONT = :'button-font'
103
+ BUTTON_FONT_FAMILY = :'button-font-family'
99
104
  BUTTON_FONT_SIZE = :'button-font-size'
100
105
  BUTTON_FONT_WEIGHT = :'button-font-weight'
101
106
  BUTTON_HEIGHT = :'button-height'
@@ -124,9 +129,13 @@ module Inkcite
124
129
 
125
130
  cfg = Config.new(ctx, opt)
126
131
 
132
+ no_tag = opt[:'no-tag'] == true
133
+
127
134
  # Wrap the table in a link to make the whole thing clickable. This works
128
135
  # in most email clients but doesn't work in Outlook (for a change).
129
- html << "{a id=\"#{id}\" href=\"#{href}\" color=\"none\"}"
136
+ html << %Q({a id="#{id}" href="#{href}" color="none")
137
+ html << ' no-tag' if no_tag
138
+ html << '}'
130
139
 
131
140
  # Responsive button is just a highly styled table/td combination with optional
132
141
  # curved corners and a lower bevel (border).
@@ -146,10 +155,11 @@ module Inkcite
146
155
  html << " width=#{cfg.width}" if cfg.width > 0
147
156
  html << " float=#{cfg.float}" if cfg.float
148
157
  html << %Q( mobile="fill"}\n)
149
- html << "{td align=center"
150
- html << " height=#{cfg.height} valign=middle" if cfg.height > 0
158
+ html << '{td align=center'
159
+ html << %Q( height=#{cfg.height} valign=middle) if cfg.height > 0
151
160
  html << %Q( font="#{cfg.font}" color="none")
152
- html << " line-height=#{cfg.line_height}" unless cfg.line_height.blank?
161
+ html << %Q( font-family="#{cfg.font_family}") unless cfg.font_family.blank?
162
+ html << %Q( line-height=#{cfg.line_height}) unless cfg.line_height.blank?
153
163
  html << %Q( font-size="#{cfg.font_size}") if cfg.font_size > 0
154
164
  html << %Q( font-weight="#{cfg.font_weight}") unless cfg.font_weight.blank?
155
165
 
@@ -164,9 +174,9 @@ module Inkcite
164
174
  # clickable.
165
175
  html << %Q({a id="#{id}" href="#{href}" color="#{cfg.color}")
166
176
  html << %Q( letter-spacing="#{cfg.letter_spacing}") unless cfg.letter_spacing.blank?
177
+ html << %q( no-tag) if no_tag
167
178
  html << %q(})
168
179
 
169
-
170
180
  else
171
181
 
172
182
  html << "{/a}{/td}\n{/table}{/a}"
@@ -16,7 +16,17 @@ module Inkcite
16
16
 
17
17
  # Supports both integers and mixed padding (e.g. 10px 20px)
18
18
  padding = opt[:padding]
19
- element.style[:padding] = px(padding) unless none?(padding)
19
+ unless none?(padding)
20
+ paddingpx = px(padding)
21
+ element.style[:padding] = paddingpx
22
+
23
+ # Copy the padding into the MSO custom padding attribute for high-DPI
24
+ # compatibility
25
+ paddingpx = "#{paddingpx} #{paddingpx} #{paddingpx} #{paddingpx}" unless paddingpx.include?(' ')
26
+ element.style[MSO_PADDING_ALT] = paddingpx
27
+
28
+ end
29
+
20
30
 
21
31
  # Vertical alignment - top, middle, bottom.
22
32
  valign = opt[:valign]
@@ -32,6 +42,10 @@ module Inkcite
32
42
  # Support for mobile-padding and mobile-padding-(direction)
33
43
  mix_mobile_padding element, opt, ctx
34
44
 
45
+ # White space wrapping can be controlled with mobile-no-wrap or mobile-wrap
46
+ mobile_white_space = (:nowrap if opt[MOBILE_NOWRAP]) || (:normal if opt[MOBILE_WRAP])
47
+ element.mobile_style[WHITE_SPACE] = mobile_white_space unless mobile_white_space.nil?
48
+
35
49
  mix_responsive element, opt, ctx
36
50
 
37
51
  element.to_s
@@ -25,7 +25,7 @@ module Inkcite
25
25
 
26
26
  # Need to add an extra space for the email clients (ahem, Gmail,
27
27
  # cough) that don't support alt text with line breaks.
28
- alt.gsub!("\n", " \n")
28
+ alt.gsub!("\n", " \n ")
29
29
 
30
30
  # Remove all HTML from the alt text. Ran into a situation where a
31
31
  # custom Helper was applying styled text as image alt text. Since
@@ -98,6 +98,14 @@ module Inkcite
98
98
  def mix_dimensions img, opt, ctx
99
99
  super
100
100
  DIMENSIONS.each { |dim| img[dim] = opt[dim] }
101
+
102
+ # Allow images to be easily scaled by percentage.
103
+ scale = opt[:scale].to_f
104
+ scale = 1 if scale > 1
105
+ scale = 0 if scale < 0
106
+
107
+ DIMENSIONS.each { |dim| img[dim] = (opt[dim].to_f * scale).round } if scale > 0 && scale < 1
108
+
101
109
  end
102
110
 
103
111
  private
@@ -0,0 +1,104 @@
1
+ module Inkcite
2
+ module Renderer
3
+ class List < Base
4
+
5
+ def render tag, opt, ctx
6
+
7
+ html = ''
8
+
9
+ # Get the stack of list helpers which will used based on whether this is
10
+ # a list item or a list open/close
11
+ tag_stack = ctx.tag_stack(:list)
12
+
13
+ # Check for the close of any list
14
+ if tag == 'ul' || tag == 'ol'
15
+ html << '{table}'
16
+
17
+ # Add the type of list as an attribute so the list item can determine the
18
+ # appropriate bullet/number.
19
+ opt[:type] = tag
20
+
21
+ # Initialize the child count to zero which will be incremented with each
22
+ # list child added.
23
+ opt[:children] = 0
24
+
25
+ # Push the options onto the stack so they can be referenced by the list items.
26
+ tag_stack << opt
27
+
28
+ elsif tag == '/ul' || tag == '/ol'
29
+ html << '</td>{/table}'
30
+
31
+ # Remove the previously open list declaration.
32
+ tag_stack.pop
33
+
34
+ elsif tag == 'li'
35
+
36
+ # Get the options declared when the list was started.
37
+ parent_opts = tag_stack.opts
38
+
39
+ # Increment the child count in the parent opts
40
+ child_count = parent_opts[:children] = parent_opts[:children] + 1
41
+
42
+ if child_count > 1
43
+
44
+ # Add a separator between this and previous list items
45
+ spacing = (parent_opts[:spacing] || 3).to_i
46
+ if spacing > 0
47
+ html << "{div height=#{spacing} line-height=#{spacing} font-size=#{spacing}}&nbsp;{/div}</td>"
48
+ end
49
+
50
+ # Start a new row after the previous one.
51
+ html << '</tr><tr>'
52
+
53
+ end
54
+
55
+ bullet = if parent_opts[:type] == 'ol'
56
+ "#{child_count}."
57
+ else
58
+ detect(opt[:bullet], parent_opts[:bullet], '&bull;')
59
+ end
60
+
61
+ # Lists can be repurposed sans bullets to be tables of items with
62
+ # adjustable space between them by setting the bullet to none.
63
+ unless none?(bullet)
64
+
65
+ # Start a new <td> to hold the bullet and spacing.
66
+ bullet_td = Element.new('td', :valign => :top)
67
+
68
+ # Apply fonts to the bullet to it displays consistently.
69
+ mix_font bullet_td, opt, ctx, parent_opts
70
+
71
+ html << bullet_td.to_s
72
+
73
+ # Number of non-breaking spaces to put before the bullet.
74
+ indent = (parent_opts[:indent] || 2).to_i
75
+ html << '&nbsp;' * indent if indent > 0
76
+
77
+ html << bullet
78
+ html << '&nbsp;</td>'
79
+
80
+ end
81
+
82
+ # Start a new <td> to hold the list item itself.
83
+ item_td = Element.new('td')
84
+
85
+ # Consistent font handling for the items int he list.
86
+ mix_font item_td, opt, ctx, parent_opts
87
+
88
+ html << item_td.to_s
89
+
90
+ elsif tag == '/li'
91
+
92
+ # This space left intentionally blank. The closing {/td} is added either
93
+ # by the next child in the list (so that vertical space can be injected into
94
+ # /this/ list item) or by closing the list itself.
95
+
96
+ end
97
+
98
+ html
99
+ end
100
+
101
+
102
+ end
103
+ end
104
+ end
@@ -34,6 +34,9 @@ module Inkcite
34
34
  # span assumes the exact dimensions of the image.
35
35
  DIMENSIONS.each { |dim| img.style[dim] = px(opt[dim]) }
36
36
 
37
+ # Image borders on mobile
38
+ mix_border img, opt, ctx
39
+
37
40
  mobile = opt[:mobile]
38
41
 
39
42
  # For FILL-style mobile images, override the width. The height (in px)
@@ -30,7 +30,9 @@ module Inkcite
30
30
  # Other mobile-specific properties
31
31
  MOBILE_HEIGHT = :'mobile-height'
32
32
  MOBILE_MAX_WIDTH = :'mobile-max-width'
33
+ MOBILE_NOWRAP = :'mobile-nowrap'
33
34
  MOBILE_PADDING = :'mobile-padding'
35
+ MOBILE_WRAP = :'mobile-wrap'
34
36
  MOBILE_WIDTH = :'mobile-width'
35
37
 
36
38
  class Rule
@@ -0,0 +1,207 @@
1
+ module Inkcite
2
+ module Renderer
3
+
4
+ # @helper Slanted Edge
5
+ # @summary Bulletproof, responsive, sloped edges for modern email designs
6
+ #
7
+ # @description
8
+ # The {slant} Helper provides consistent, reliable sloped edges between sections of your email. You can customize the size, color and dimensions of the slant. It renders using CSS in modern email clients and has a VML fallback for Outlook.
9
+ #
10
+ # @warning Slants are not compatible with Outlook 2016 which renders a thin, white border around the slant.
11
+ # @warning Older email clients render angled CSS borders without anti-aliasing
12
+ #
13
+ # @credit
14
+ # Slanted edges based on @M_J_Robbins sloped edges for email concept.
15
+ # https://codepen.io/M_J_Robbins/pen/rpzLNx
16
+ #
17
+ # @usage
18
+ # {div width=600 padding=15 bgcolor=#009}
19
+ # ...
20
+ # {/div}
21
+ # {slant bgcolor=#009 color=#909 bottom right width=600 height=50}
22
+ # {div width=600 padding=15 bgcolor=#909}
23
+ # ...
24
+ # {/div}
25
+ class Slant < Responsive
26
+ def render tag, opt, ctx
27
+
28
+ html = ''
29
+
30
+ # @attribute no-fallback If present, disables VML fallback so the slant doesn't appear in Outlook 2007-2013.; alias no-vml
31
+ no_vml = opt[NO_FALLBACK] || opt[NO_VML]
32
+
33
+ # Check to see if VML is enabled for this email
34
+ include_fallback = ctx.vml_enabled? && !no_vml
35
+
36
+ # The first time the slant is used, we need to initialize the VML shapes
37
+ # and the responsive class that will be used to scale this.
38
+ if include_fallback
39
+ if ctx.once?(:slant)
40
+
41
+ # Notify the context that VML has been used in this email.
42
+ ctx.vml_used!
43
+
44
+ html << IF_OUTLOOK_LT_2016
45
+
46
+ # These need to be wrapped in a div with zero height otherwise unwanted whitespace will
47
+ # appear in the email where these shapes are initialized.
48
+ # https://litmus.com/community/discussions/538-vml-outlook-07-10-13-unwanted-20px-padding-at-the-bottom
49
+ html << '{div font-size=0 line-height=0}'
50
+
51
+ html << render_vml_triangle('stl', [ [0, 1], [1, 0], [0, 0] ])
52
+ html << render_vml_triangle('str', [ [0, 0], [1, 0], [1, 1] ])
53
+ html << render_vml_triangle('sbl', [ [0, 0], [1, 1], [0, 1] ])
54
+ html << render_vml_triangle('sbr', [ [0, 1], [1, 1], [1, 0] ])
55
+
56
+ html << '{/div}'
57
+ html << '{/if}'
58
+
59
+ end
60
+ end
61
+
62
+ # @attribute height The height of the slope in pixels; required
63
+ height = opt[:height].to_i
64
+ # @attribute width The width of the slope in pixels; required
65
+ width = opt[:width].to_i
66
+ if height <= 0 || width <= 0
67
+ ctx.error('Missing slant dimensions', opt)
68
+ return nil
69
+ end
70
+
71
+ # Pre-calculate half the width and height of the slant.
72
+ half_height = (height / 2.0).round(0)
73
+ half_width = (width / 2.0).round(0)
74
+
75
+ # @attribute color The foreground color of the slanted edge.; default #234
76
+ color = hex(opt[:color] || '#234')
77
+
78
+ directions = [
79
+ (opt[:bottom] ? :bottom : :top),
80
+ (opt[:left] ? :left : :right)
81
+ ]
82
+
83
+ border_colors = []
84
+ DIRECTIONS.each do |d|
85
+ next if d.nil?
86
+ border_colors << (directions.include?(d) ? color : TRANSPARENT)
87
+ end
88
+
89
+ # @attribute no-wrap If present, disables the table that wraps the slant. Use this if the slant is already in a container (e.g. table or div) that provides width and background color.
90
+ no_wrap = opt[NO_WRAP]
91
+ unless no_wrap
92
+ wrap_div = Element.new('table')
93
+ wrap_div[:width] = width
94
+
95
+ # @attribute bgcolor The color that appears behind the slanted edge; default transparent; alias background-color
96
+ bgcolor = detect_bgcolor(opt)
97
+ wrap_div[:bgcolor] = bgcolor unless none?(bgcolor)
98
+
99
+ wrap_div[:mobile] = :fill
100
+ html << wrap_div.to_helper
101
+ html << '{td}'
102
+ end
103
+
104
+ # This zero-sized div will have beefed up borders that create the
105
+ # slant effect.
106
+ slant_div = Element.new('div')
107
+ slant_div.style[BORDER_COLOR] = border_colors.join(' ')
108
+ slant_div.style[BORDER_STYLE] = :solid
109
+ slant_div.style[BORDER_WIDTH] = "#{px(half_height)} #{px(half_width)}"
110
+ slant_div.style[MSO_HIDE] = :all
111
+
112
+ # Fix to trigger antialiasing in the slanted border on webkit clients
113
+ # like Outlook for iOS
114
+ # https://stackoverflow.com/a/27506977
115
+ slant_div.style[:'-webkit-transform'] = 'rotate(0.0005deg)'
116
+
117
+ # Create a custom mobile class name for this slant. Then check to
118
+ # see if that responsive rule has already been registered - otherwise
119
+ # add it.
120
+ klass = "slant#{half_height}"
121
+ slant_rule = ctx.media_query.find_by_klass(klass) || Rule.new('div', klass, "border-width: #{px(half_height)} 50vw !important;")
122
+ ctx.media_query << slant_div.add_rule(slant_rule)
123
+
124
+ html << slant_div.to_s
125
+ html << '</div>'
126
+
127
+ if include_fallback
128
+ backward = false;
129
+
130
+ vshape = Element.new('v:shape')
131
+
132
+ # Generate the unique VML shape ID from the direction of the
133
+ # 90-degree corner.
134
+ type_id = "s" # Slant
135
+ type_id << (opt[:bottom] ? 'b' : 't')
136
+ type_id << (opt[:left] ? 'l' : 'r')
137
+
138
+ vshape[:type] = quote(type_id)
139
+ vshape.style[:width] = px(width)
140
+ vshape.style[:height] = px(height)
141
+ vshape.style[:'mso-position-horizontal'] = :center
142
+ vshape[:fillcolor] = color
143
+ vshape[:stroked] = :f
144
+
145
+ # Render the outlook-only VML-based fallback. Once again, it needs
146
+ # to be wrapped in a zero-height div to prevent unwanted space from
147
+ # being injected on certain version of outlook.
148
+ html << IF_OUTLOOK_LT_2016
149
+ html << '{div font-size=0 line-height=0}'
150
+ html << vshape.to_s
151
+ html << '<o:lock selection="t"/>'
152
+ html << '</v:shape>'
153
+ html << '{/div}'
154
+ html << '{/if}'
155
+
156
+ end
157
+
158
+ # Close the wrap element.
159
+ html << '{/td}{/table}' unless no_wrap
160
+
161
+ html
162
+ end
163
+
164
+ private
165
+
166
+ def render_vml_coordinate coord
167
+ x, y = coord
168
+ "#{x * COORDINATE_SCALE},#{y * COORDINATE_SCALE}"
169
+ end
170
+
171
+ def render_vml_triangle id, coords
172
+
173
+ path = ''
174
+ path << 'm' # moveto
175
+ path << render_vml_coordinate(coords[0])
176
+ path << 'l' # lineto
177
+ path << render_vml_coordinate(coords[1])
178
+ path << ','
179
+ path << render_vml_coordinate(coords[2])
180
+ path << 'x' # close
181
+ path << 'e' # end
182
+
183
+ %Q(<v:shapetype id="#{id}" path="#{path}" xmlns:v="urn:schemas-microsoft-com:vml"/>)
184
+ end
185
+
186
+ # This is the MSO conditional used to prevent the slant from
187
+ # appearing in Outlook 2016 where it has thin lines around the
188
+ # VML according to Litmus results.
189
+ IF_OUTLOOK_LT_2016 = '{if test="lt mso 16"}'
190
+
191
+ # Flags allowing the user to disable VML fallbacks.
192
+ NO_FALLBACK = :'no-fallback'
193
+ NO_VML = :'no-vml'
194
+
195
+ # Tag allowing the user to disable the wrap element if they don't want it
196
+ NO_WRAP = :'no-wrap'
197
+
198
+ # Static constant for the color used where the border is transparent
199
+ # to create the slanted edge.
200
+ TRANSPARENT = 'transparent'
201
+
202
+ # Arbitrary multiplier present in the original codepen.
203
+ COORDINATE_SCALE = 21600
204
+
205
+ end
206
+ end
207
+ end
@@ -2,6 +2,10 @@ module Inkcite
2
2
  module Renderer
3
3
  class Snow < SpecialEffect
4
4
 
5
+ # Name of the array of value distribution used
6
+ # to make the animation start at different time points.
7
+ STARTING_PERCENT = :'starting-percent'
8
+
5
9
  protected
6
10
 
7
11
  def config_all_children style, sfx
@@ -23,7 +27,13 @@ module Inkcite
23
27
  style[:opacity] = opacity if opacity < OPACITY_CEIL
24
28
 
25
29
  animation.duration = speed
26
- animation.delay = ((sfx.time / sfx.count) * n).round(1)
30
+
31
+ # If you start an animation with a negative delay, browsers and email clients
32
+ # start the animation as if it had already begun some time in the past
33
+ # https://css-tricks.com/starting-css-animations-mid-way/
34
+ starting_percent = sfx[STARTING_PERCENT][n] / 100.0
35
+ animation.delay = 0 - (speed * starting_percent).round
36
+
27
37
  animation.timing_function = Animation::LINEAR
28
38
 
29
39
  start_left = sfx.positions_x[n]
@@ -62,6 +72,10 @@ module Inkcite
62
72
  # the container's width in random order.
63
73
  sfx.positions_x.shuffle!
64
74
 
75
+ # Generate a random assortment of percent completed for the
76
+ # snowflakes so they can start mid-fall.
77
+ sfx[STARTING_PERCENT] = sfx.equal_distribution(0..100, sfx.count).shuffle
78
+
65
79
  end
66
80
 
67
81
  def defaults opt, ctx