inkcite 1.12.1 → 1.13.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,8 +11,11 @@ module Inkcite
11
11
  BACKGROUND_SIZE = :'background-size'
12
12
  BORDER_BOTTOM = :'border-bottom'
13
13
  BORDER_COLLAPSE = :'border-collapse'
14
+ BORDER_LEFT = :'border-left'
14
15
  BORDER_RADIUS = :'border-radius'
16
+ BORDER_RIGHT = :'border-right'
15
17
  BORDER_SPACING = :'border-spacing'
18
+ BORDER_TOP = :'border-top'
16
19
  BOX_SHADOW = :'box-shadow'
17
20
  FONT_FAMILY = :'font-family'
18
21
  FONT_SIZE = :'font-size'
@@ -37,11 +40,8 @@ module Inkcite
37
40
  WEBKIT_ANIMATION = :'-webkit-animation'
38
41
  WHITE_SPACE = :'white-space'
39
42
 
40
- # CSS Margins
41
- MARGINS = [MARGIN_TOP, MARGIN_LEFT, MARGIN_BOTTOM, MARGIN_RIGHT]
42
-
43
- # CSS Directions
44
- DIRECTIONS = [:top, :right, :bottom, :left]
43
+ # CSS direction suffixes including nil/empty for convenience.
44
+ DIRECTIONS = [ nil, :top, :right, :bottom, :left]
45
45
 
46
46
  # Attribute and CSS dimensions
47
47
  DIMENSIONS = [:width, :height]
@@ -85,6 +85,11 @@ module Inkcite
85
85
  none?(bgcolor) ? nil : hex(bgcolor)
86
86
  end
87
87
 
88
+ def detect_bggradient opt, default=nil
89
+ bggradient = detect(opt[:gradient], opt[:bggradient], opt[BACKGROUND_GRADIENT])
90
+ none?(bggradient) ? nil : hex(bggradient)
91
+ end
92
+
88
93
  # Convenience pass-thru to Renderer's static helper method.
89
94
  def hex color
90
95
  Renderer.hex(color)
@@ -118,14 +123,13 @@ module Inkcite
118
123
  element.style[BACKGROUND_COLOR] = bgcolor if bgcolor
119
124
 
120
125
  # Background gradient support
121
- bggradient = detect(opt[:bggradient] || opt[BACKGROUND_GRADIENT])
126
+ bggradient = detect_bggradient(opt)
122
127
  unless none?(bggradient)
123
128
 
124
129
  # As a shortcut a gradient can be specified simply by designating
125
130
  # both a bgcolor and the gradient color - this will insert a radial
126
131
  # gradient automatically.
127
132
  if bggradient.start_with?('#')
128
- bggradient = hex(bggradient)
129
133
 
130
134
  # If a bgcolor is provided, the gradient goes bgcolor -> bggradient.
131
135
  # Otherwise, it goes bggradient->darker(bggradient)
@@ -141,18 +145,7 @@ module Inkcite
141
145
  end
142
146
 
143
147
  def mix_border element, opt, ctx
144
-
145
- border = opt[:border]
146
- element.style[:border] = border unless border.blank?
147
-
148
- # Iterate through each of the possible borders and apply them individually
149
- # to the style if they are defined.
150
- DIRECTIONS.each do |dir|
151
- key = :"border-#{dir}"
152
- border = opt[key]
153
- element.style[key] = border unless border.blank? || border == NONE
154
- end
155
-
148
+ mix_directional element, element.style, opt, ctx, :border
156
149
  end
157
150
 
158
151
  def mix_border_radius element, opt, ctx
@@ -162,6 +155,27 @@ module Inkcite
162
155
 
163
156
  end
164
157
 
158
+ # Helper to mix CSS properties that can be defined as either a
159
+ # singular shorthand (e.g. border) or in one or more of the
160
+ # compass directions (e.g. border-top, border-left).
161
+ def mix_directional element, into, opt, ctx, opt_key, css_key=nil, as_px=false
162
+
163
+ css_key = opt_key if css_key.nil?
164
+
165
+ # Iterate through each of the possible directions (including blank)
166
+ # and apply them each to the element's style hash.
167
+ DIRECTIONS.each do |dir|
168
+ dir_opt_key = add_directional_suffix(opt_key, dir)
169
+ dir_css_key = add_directional_suffix(css_key, dir)
170
+ value = opt[dir_opt_key]
171
+ next if none?(value)
172
+
173
+ value = px(value) if as_px
174
+ into[dir_css_key] = value
175
+ end
176
+
177
+ end
178
+
165
179
  def mix_font element, opt, ctx, parent=nil
166
180
 
167
181
  # Always ensure we have a parent to inherit from.
@@ -201,19 +215,16 @@ module Inkcite
201
215
 
202
216
  def mix_margins element, opt, ctx
203
217
 
204
- # Check to see if the 'all' margin attribute is specified.
205
- all_margin = opt[MARGIN]
218
+ # Outlook supports Margin, not margin.
219
+ mix_directional element, element.style, opt, ctx, :margin, :Margin, true
206
220
 
207
- # Applying individual margins helps ensure compatibility across
208
- # all email clients. Cough! Cough! Looking at you Outlook.
209
- MARGINS.each do |margin|
221
+ end
210
222
 
211
- # Check for specific direction margin (e.g. margin-left) which
212
- # would override all margins. Otherwise, inherit all margin.
213
- amt = (opt[margin] || all_margin).to_i
214
- element.style[margin] = px(amt) if amt > 0
223
+ # Text alignment - left, right, center.
224
+ def mix_text_align element, opt, ctx
215
225
 
216
- end
226
+ align = detect(opt[:align] || opt[TEXT_ALIGN])
227
+ element.style[TEXT_ALIGN] = align unless none?(align)
217
228
 
218
229
  end
219
230
 
@@ -280,6 +291,24 @@ module Inkcite
280
291
  html
281
292
  end
282
293
 
294
+ private
295
+
296
+ # Helper method which adds the directional suffix (e.g. top)
297
+ # to the provided key (border) and converts to a symbol.
298
+ def add_directional_suffix key, dir
299
+
300
+ # Nothing to do if the direction isn't provided.
301
+ return key if dir.blank?
302
+
303
+ # Need to convert the key to a string since it is likely
304
+ # a symbol that has been provided.
305
+ dir_key = ''
306
+ dir_key << key.to_s
307
+ dir_key << '-'
308
+ dir_key << dir.to_s
309
+ dir_key.to_sym
310
+ end
311
+
283
312
  end
284
313
  end
285
314
  end
@@ -11,12 +11,17 @@ module Inkcite
11
11
  mix_border element, opt, ctx
12
12
  mix_border_radius element, opt, ctx
13
13
  mix_font element, opt, ctx
14
+ mix_margins element, opt, ctx
14
15
  mix_text_align element, opt, ctx
15
16
 
16
17
  # Supports both integers and mixed padding (e.g. 10px 20px)
17
18
  padding = opt[:padding]
18
19
  element.style[:padding] = px(padding) unless none?(padding)
19
20
 
21
+ # Supports custom padding on mobile - e.g. mobile-padding="15px 5px"
22
+ mobile_padding = opt[MOBILE_PADDING]
23
+ element.mobile_style[:padding] = px(mobile_padding) unless none?(mobile_padding)
24
+
20
25
  # Vertical alignment - top, middle, bottom.
21
26
  valign = opt[:valign]
22
27
  element.style[VERTICAL_ALIGN] = valign unless none?(valign)
@@ -33,11 +38,13 @@ module Inkcite
33
38
  element.to_s
34
39
  end
35
40
 
36
- # Text alignment - left, right, center.
37
- def mix_text_align element, opt, ctx
41
+ def mix_width element, opt, ctx
42
+
43
+ width = opt[:width]
44
+ element.style[:width] = px(width) unless width.blank?
38
45
 
39
- align = opt[:align]
40
- element.style[TEXT_ALIGN] = align unless none?(align)
46
+ mobile_width = opt[MOBILE_WIDTH]
47
+ element.mobile_style[:width] = px(mobile_width) unless mobile_width.blank?
41
48
 
42
49
  end
43
50
 
@@ -8,8 +8,7 @@ module Inkcite
8
8
 
9
9
  div = Element.new('div')
10
10
 
11
- width = opt[:width]
12
- div.style[:width] = px(width) unless width.blank?
11
+ mix_width div, opt, ctx
13
12
 
14
13
  height = opt[:height].to_i
15
14
  div.style[:height] = px(height) if height > 0
@@ -2,7 +2,7 @@ module Inkcite
2
2
  module Renderer
3
3
  class Element
4
4
 
5
- attr_reader :tag
5
+ attr_reader :mobile_style, :style, :tag
6
6
 
7
7
  def initialize tag, att={}
8
8
 
@@ -19,7 +19,10 @@ module Inkcite
19
19
 
20
20
  # For caller convenience, accept a style hash from the attributes
21
21
  # or initialize it here.
22
- @styles = att.delete(:style) || {}
22
+ @style = att.delete(:style) || {}
23
+
24
+ # Collection of mobile-only CSS properties for this element.
25
+ @mobile_style = att.delete(:mobile_style) || {}
23
26
 
24
27
  end
25
28
 
@@ -67,10 +70,6 @@ module Inkcite
67
70
  @self_close
68
71
  end
69
72
 
70
- def style
71
- @styles
72
- end
73
-
74
73
  # Generates a Helper tag rather than a string tag - e.g. {img src=test.png}
75
74
  # rather than <img src=test.png>
76
75
  def to_helper
@@ -80,7 +79,7 @@ module Inkcite
80
79
  def to_s open='<', close='>'
81
80
 
82
81
  # Convert the style hash into CSS style attribute.
83
- @att[:style] = Renderer.quote(Renderer.render_styles(@styles)) unless @styles.blank?
82
+ @att[:style] = Renderer.quote(Renderer.render_styles(@style)) unless @style.blank?
84
83
 
85
84
  # Convert the list of CSS classes assigned to this element into an attribute
86
85
  self[:class] = Renderer.quote(@classes.to_a.sort.join(' ')) unless @classes.blank?
@@ -2,29 +2,34 @@ module Inkcite
2
2
  module Renderer
3
3
  class Responsive < Base
4
4
 
5
- BUTTON = 'button'
6
- DROP = 'drop'
7
- FILL = 'fill'
8
- FLUID = 'fluid'
9
- FLUID_DROP = 'fluid-drop'
5
+ BUTTON = 'button'
6
+ DROP = 'drop'
7
+ FILL = 'fill'
8
+ FLUID = 'fluid'
9
+ FLUID_DROP = 'fluid-drop'
10
10
  FLUID_STACK = 'fluid-stack'
11
- HIDE = 'hide'
12
- IMAGE = 'img'
13
- SHOW = 'show'
11
+ HIDE = 'hide'
12
+ IMAGE = 'img'
13
+ SHOW = 'show'
14
14
  SHOW_INLINE = 'show-inline'
15
- SWITCH = 'switch'
16
- SWITCH_UP = 'switch-up'
17
- TOGGLE = 'toggle'
15
+ SWITCH = 'switch'
16
+ SWITCH_UP = 'switch-up'
17
+ TOGGLE = 'toggle'
18
18
 
19
19
  # For elements that take on different background properties
20
20
  # when they go responsive
21
- MOBILE_BGCOLOR = :'mobile-bgcolor'
22
- MOBILE_BACKGROUND = :'mobile-background'
23
- MOBILE_BACKGROUND_COLOR = :'mobile-background-color'
24
- MOBILE_BACKGROUND_IMAGE = :'mobile-background-image'
25
- MOBILE_BACKGROUND_REPEAT = :'mobile-background-repeat'
21
+ MOBILE_BGCOLOR = :'mobile-bgcolor'
22
+ MOBILE_BACKGROUND = :'mobile-background'
23
+ MOBILE_BACKGROUND_COLOR = :'mobile-background-color'
24
+ MOBILE_BACKGROUND_IMAGE = :'mobile-background-image'
25
+ MOBILE_BACKGROUND_REPEAT = :'mobile-background-repeat'
26
26
  MOBILE_BACKGROUND_POSITION = :'mobile-background-position'
27
- MOBILE_BACKGROUND_SIZE = :'mobile-background-size'
27
+ MOBILE_BACKGROUND_SIZE = :'mobile-background-size'
28
+ MOBILE_SRC = :'mobile-src'
29
+
30
+ # Other mobile-specific properties
31
+ MOBILE_PADDING = :'mobile-padding'
32
+
28
33
 
29
34
  class Rule
30
35
 
@@ -58,7 +63,7 @@ module Inkcite
58
63
  end
59
64
 
60
65
  def att_selector_string
61
- "[class~=#{Renderer.quote(@klass)}]"
66
+ ".#{@klass}"
62
67
  end
63
68
 
64
69
  def block?
@@ -153,7 +158,7 @@ module Inkcite
153
158
  # FILL causes specific types of elements to expand to 100% of the available
154
159
  # width of the mobile device.
155
160
  styles << Rule.new('img', FILL, 'width: 100% !important; height: auto !important;', false)
156
- styles << Rule.new([ 'table', 'td' ], FILL, 'width: 100% !important; background-size: 100% auto !important;', false)
161
+ styles << Rule.new(['table', 'td'], FILL, 'width: 100% !important; background-size: 100% auto !important;', false)
157
162
 
158
163
  # For mobile-image tags.
159
164
  styles << Rule.new('span', IMAGE, 'display: block; background-position: center; background-size: cover;', false)
@@ -198,6 +203,11 @@ module Inkcite
198
203
  mobile == FLUID_DROP || mobile == FLUID_STACK
199
204
  end
200
205
 
206
+ def mix_border element, opt, ctx
207
+ super
208
+ mix_directional element, element.mobile_style, opt, ctx, MOBILE_BORDER, :border
209
+ end
210
+
201
211
  def mix_font element, opt, ctx, parent=nil
202
212
 
203
213
  # Let the super class do its thing and grab the name of the font
@@ -205,29 +215,39 @@ module Inkcite
205
215
  font = super
206
216
 
207
217
  # Will hold the mobile font overrides for this element, if any.
208
- style = { }
218
+ font_family = detect_font(MOBILE_FONT_FAMILY, font, opt, parent, ctx)
219
+ element.mobile_style[FONT_FAMILY] = font_family unless font_family.blank?
209
220
 
210
221
  font_size = detect_font(MOBILE_FONT_SIZE, font, opt, parent, ctx)
211
- style[FONT_SIZE] = "#{px(font_size)} !important" unless font_size.blank?
212
-
213
- line_height = detect_font(MOBILE_LINE_HEIGHT, font, opt, parent, ctx)
214
- style[LINE_HEIGHT] = "#{px(line_height)} !important" unless line_height.blank?
222
+ element.mobile_style[FONT_SIZE] = px(font_size) unless font_size.blank?
215
223
 
216
224
  color = detect_font(MOBILE_FONT_COLOR, font, opt, parent, ctx)
217
- style[:color] = "#{hex(color)} !important" unless color.blank?
225
+ element.mobile_style[:color] = hex(color) unless color.blank?
226
+
227
+ font_weight = detect_font(MOBILE_FONT_WEIGHT, font, opt, parent, ctx)
228
+ element.mobile_style[FONT_WEIGHT] = font_weight unless font_weight.blank?
218
229
 
219
- mix_responsive_style element, opt, ctx, Renderer.render_styles(style) unless style.blank?
230
+ letter_spacing = detect_font(MOBILE_LETTER_SPACING, font, opt, parent, ctx)
231
+ element.mobile_style[LETTER_SPACING] = px(letter_spacing) unless none?(letter_spacing)
232
+
233
+ line_height = detect_font(MOBILE_LINE_HEIGHT, font, opt, parent, ctx)
234
+ element.mobile_style[LINE_HEIGHT] = px(line_height) unless line_height.blank?
220
235
 
221
236
  font
222
237
  end
223
238
 
239
+ def mix_margins element, opt, ctx
240
+ super
241
+ mix_directional element, element.mobile_style, opt, ctx, MOBILE_MARGIN, :margin, true
242
+ end
243
+
224
244
  def mix_responsive element, opt, ctx, klass=nil
225
245
 
226
246
  # Apply the "mobile" attribute or use the override if one was provided.
227
247
  mix_responsive_klass element, opt, ctx, klass || opt[:mobile]
228
248
 
229
249
  # Apply the "mobile-style" attribute if one was provided.
230
- mix_responsive_style element, opt, ctx, opt[MOBILE_STYLE]
250
+ mix_responsive_style element, opt, ctx
231
251
 
232
252
  end
233
253
 
@@ -293,11 +313,54 @@ module Inkcite
293
313
 
294
314
  end
295
315
 
296
- def mix_responsive_style element, opt, ctx, declarations=nil
316
+ def mix_responsive_style element, opt, ctx
317
+
318
+ # Warn that mobile-style is no longer supported. Developers should
319
+ # use the stronger, faster, better-er mobile-* attributes
320
+ __unsupported_style = element[MOBILE_STYLE]
321
+ ctx.errors('The mobile-style attribute is no longer supported', { :element => element.to_s, :mobile_style => __unsupported_style }) unless __unsupported_style.blank?
322
+
323
+ _mobile_style = element.mobile_style
324
+ return if _mobile_style.blank?
325
+
326
+ # Will hold a preprocessed list of direction-free, lowercased properties
327
+ # (ahem, Outlook Margin) so we can easily determine if a mobile style
328
+ # needs !important to override its desktop style value.
329
+ desktop_style_keys = Set.new
330
+ desktop_style = {}
331
+ element.style.each_pair do |key, css|
297
332
 
298
- # Check to see if a mobile style (e.g. "mobile-style='background-color: #ff0;'")
299
- # has been declared for this element.
300
- declarations ||= opt[MOBILE_STYLE]
333
+ key = key.to_s.downcase
334
+ desktop_style[key.to_sym] = css
335
+
336
+ base_key = get_directionless_key(key)
337
+ desktop_style_keys.add(base_key)
338
+
339
+ end
340
+
341
+ # This will hold the decorated list of CSS properties. If the element
342
+ # has any existing styles that are being overridden in the mobile styles
343
+ # we need to append the !important flag.
344
+ decorated_style = {}
345
+
346
+ # Iterate through the defined mobile styles, determine which need to
347
+ # have !important and assemble a new hash to be rendered as CSS.
348
+ _mobile_style.each_pair do |key, css|
349
+
350
+ # No need to put attributes in the mobile style if they match
351
+ # the existing desktop style of the element.
352
+ next if css == desktop_style[key]
353
+
354
+ # Append !important to the CSS if it overrides a value in the
355
+ # element's in-lined styles. Need to test bo
356
+ base_key = get_directionless_key(key)
357
+ css = "#{css} !important" if desktop_style_keys.include?(base_key)
358
+
359
+ decorated_style[key] = css
360
+ end
361
+
362
+ # Render the array of styles to a CSS declaration string
363
+ declarations = Renderer.render_styles decorated_style
301
364
  return if declarations.blank?
302
365
 
303
366
  mq = ctx.media_query
@@ -307,11 +370,11 @@ module Inkcite
307
370
  # If no klass was specified, check to see if any previously defined rule matches
308
371
  # the style declarations. If so, we'll reuse that rule and apply the klass
309
372
  # to this object to avoid unnecessary duplication in the HTML.
310
- rule = mq.find_by_declaration(declarations);
373
+ rule = mq.find_by_declaration(declarations)
311
374
  if rule.nil?
312
375
 
313
376
  # Generate a unique class name for this style if it has not already been declared.
314
- # These are of the form m001, etc. Redability is not important because it's
377
+ # These are of the form m001, etc. Readability is not important because it's
315
378
  # dynamically generated and referenced.
316
379
  klass = unique_klass(ctx)
317
380
 
@@ -336,22 +399,46 @@ module Inkcite
336
399
  end
337
400
 
338
401
  def unique_klass ctx
339
- "m%1d" % ctx.unique_id(:m)
402
+ 'm%1d' % ctx.unique_id(:m)
340
403
  end
341
404
 
342
405
  private
343
406
 
344
407
  # Attribute used to declare custom mobile styles for an element.
408
+ MOBILE_BORDER = :'mobile-border'
409
+ MOBILE_MARGIN = :'mobile-margin'
345
410
  MOBILE_STYLE = :'mobile-style'
411
+ MOBILE_WIDTH = :'mobile-width'
346
412
 
347
413
  # Universal CSS selector.
348
414
  UNIVERSAL = '*'
349
415
 
350
- # For font overrides on mobile devices.
351
- MOBILE_FONT_COLOR = :'mobile-color'
352
- MOBILE_FONT_SIZE = :'mobile-font-size'
416
+ # For font overrides on mobile devices. These values are read from
417
+ # the object's attributes and installed into the element's mobile_styles.
418
+ MOBILE_FONT_COLOR = :'mobile-color'
419
+ MOBILE_FONT_FAMILY = :'mobile-font-family'
420
+ MOBILE_FONT_SIZE = :'mobile-font-size'
421
+ MOBILE_FONT_WEIGHT = :'mobile-font-weight'
422
+ MOBILE_LETTER_SPACING = :'mobile-letter-spacing'
353
423
  MOBILE_LINE_HEIGHT = :'mobile-line-height'
354
424
 
425
+ # Accepts a key, such as border or border-left, and returns the
426
+ # key sans direction suffix - so border and border respectively.
427
+ # The opposite of add_directional_suffix().
428
+ def get_directionless_key key
429
+ key = key.to_s
430
+
431
+ # Iterate through the possible directions and if the key ends
432
+ # with the separator and direction (e.g. -left) trim that off
433
+ # and return it.
434
+ DIRECTIONS.each do |dir|
435
+ dir = "-#{dir}"
436
+ return key[0, dir.length] if key.end_with?(dir)
437
+ end
438
+
439
+ key
440
+ end
441
+
355
442
  end
356
443
  end
357
444
  end