inkcite 1.0.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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +110 -0
  4. data/Rakefile +8 -0
  5. data/assets/facebook-like.css +62 -0
  6. data/assets/facebook-like.js +59 -0
  7. data/assets/init/config.yml +97 -0
  8. data/assets/init/helpers.tsv +31 -0
  9. data/assets/init/source.html +60 -0
  10. data/assets/init/source.txt +6 -0
  11. data/bin/inkcite +6 -0
  12. data/inkcite.gemspec +42 -0
  13. data/lib/inkcite.rb +32 -0
  14. data/lib/inkcite/cli/base.rb +128 -0
  15. data/lib/inkcite/cli/build.rb +130 -0
  16. data/lib/inkcite/cli/init.rb +58 -0
  17. data/lib/inkcite/cli/preview.rb +30 -0
  18. data/lib/inkcite/cli/server.rb +123 -0
  19. data/lib/inkcite/cli/test.rb +61 -0
  20. data/lib/inkcite/email.rb +219 -0
  21. data/lib/inkcite/mailer.rb +140 -0
  22. data/lib/inkcite/minifier.rb +151 -0
  23. data/lib/inkcite/parser.rb +111 -0
  24. data/lib/inkcite/renderer.rb +177 -0
  25. data/lib/inkcite/renderer/base.rb +186 -0
  26. data/lib/inkcite/renderer/button.rb +168 -0
  27. data/lib/inkcite/renderer/div.rb +29 -0
  28. data/lib/inkcite/renderer/element.rb +82 -0
  29. data/lib/inkcite/renderer/footnote.rb +132 -0
  30. data/lib/inkcite/renderer/google_analytics.rb +35 -0
  31. data/lib/inkcite/renderer/image.rb +95 -0
  32. data/lib/inkcite/renderer/image_base.rb +82 -0
  33. data/lib/inkcite/renderer/in_browser.rb +38 -0
  34. data/lib/inkcite/renderer/like.rb +73 -0
  35. data/lib/inkcite/renderer/link.rb +243 -0
  36. data/lib/inkcite/renderer/litmus.rb +33 -0
  37. data/lib/inkcite/renderer/lorem.rb +39 -0
  38. data/lib/inkcite/renderer/mobile_image.rb +67 -0
  39. data/lib/inkcite/renderer/mobile_style.rb +40 -0
  40. data/lib/inkcite/renderer/mobile_toggle.rb +27 -0
  41. data/lib/inkcite/renderer/outlook_background.rb +48 -0
  42. data/lib/inkcite/renderer/partial.rb +31 -0
  43. data/lib/inkcite/renderer/preheader.rb +22 -0
  44. data/lib/inkcite/renderer/property.rb +39 -0
  45. data/lib/inkcite/renderer/responsive.rb +334 -0
  46. data/lib/inkcite/renderer/span.rb +21 -0
  47. data/lib/inkcite/renderer/table.rb +67 -0
  48. data/lib/inkcite/renderer/table_base.rb +149 -0
  49. data/lib/inkcite/renderer/td.rb +92 -0
  50. data/lib/inkcite/uploader.rb +173 -0
  51. data/lib/inkcite/util.rb +85 -0
  52. data/lib/inkcite/version.rb +3 -0
  53. data/lib/inkcite/view.rb +745 -0
  54. data/lib/inkcite/view/context.rb +38 -0
  55. data/lib/inkcite/view/media_query.rb +60 -0
  56. data/lib/inkcite/view/tag_stack.rb +38 -0
  57. data/test/email_spec.rb +16 -0
  58. data/test/parser_spec.rb +72 -0
  59. data/test/project/config.yml +98 -0
  60. data/test/project/helpers.tsv +56 -0
  61. data/test/project/images/inkcite.jpg +0 -0
  62. data/test/project/source.html +58 -0
  63. data/test/project/source.txt +6 -0
  64. data/test/renderer/button_spec.rb +45 -0
  65. data/test/renderer/div_spec.rb +101 -0
  66. data/test/renderer/element_spec.rb +31 -0
  67. data/test/renderer/footnote_spec.rb +57 -0
  68. data/test/renderer/image_spec.rb +82 -0
  69. data/test/renderer/link_spec.rb +84 -0
  70. data/test/renderer/mobile_image_spec.rb +27 -0
  71. data/test/renderer/mobile_style_spec.rb +37 -0
  72. data/test/renderer/td_spec.rb +126 -0
  73. data/test/renderer_spec.rb +28 -0
  74. data/test/view_spec.rb +15 -0
  75. metadata +333 -0
@@ -0,0 +1,33 @@
1
+ module Inkcite
2
+ module Renderer
3
+ class Litmus < Base
4
+
5
+ def render tag, opt, ctx
6
+
7
+ # Litmus tracking is enabled only for production emails.
8
+ return nil unless ctx.production? && ctx.email?
9
+
10
+ code = opt[:code] || opt[:id]
11
+ return nil if code.blank?
12
+
13
+ merge_tag = opt[MERGE_TAG] || ctx[MERGE_TAG]
14
+
15
+ ctx.styles << "@media print{#_t { background-image: url('https://#{code}.emltrk.com/#{code}?p&d=#{merge_tag}');}}"
16
+ ctx.styles << "div.OutlookMessageHeader {background-image:url('https://#{code}.emltrk.com/#{code}?f&d=#{merge_tag}')}"
17
+ ctx.styles << "table.moz-email-headers-table {background-image:url('https://#{code}.emltrk.com/#{code}?f&d=#{merge_tag}')}"
18
+ ctx.styles << "blockquote #_t {background-image:url('https://#{code}.emltrk.com/#{code}?f&d=#{merge_tag}')}"
19
+ ctx.styles << "#MailContainerBody #_t {background-image:url('https://#{code}.emltrk.com/#{code}?f&d=#{merge_tag}')}"
20
+
21
+ ctx.footer << '<div id="_t"></div>'
22
+ ctx.footer << "<img src=\"https://#{code}.emltrk.com/#{code}?d=#{merge_tag}\" width=1 height=1 border=0 />"
23
+
24
+ nil
25
+ end
26
+
27
+ private
28
+
29
+ MERGE_TAG = :'merge-tag'
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,39 @@
1
+ module Inkcite
2
+ module Renderer
3
+ class Lorem < Base
4
+
5
+ def render tag, opt, ctx
6
+
7
+ # Lazy load only if Lorem is used in the email.
8
+ require 'faker'
9
+
10
+ type = (opt[:type] || :sentences).to_sym
11
+
12
+ # Get the limit (e.g. the number of sentences )
13
+ limit = opt[:sentences] || opt[:size] || opt[:limit] || opt[:count]
14
+
15
+ # Always warn the creator that there is Lorem Ipsum in the email because
16
+ # we don't want it to ship accidentally.
17
+ ctx.error 'Email contains Lorem Ipsum'
18
+
19
+ if type == :headline
20
+
21
+ words = (limit || 4).to_i
22
+ Faker::Lorem.words(words).join(SPACE).titlecase
23
+
24
+ else
25
+
26
+ sentences = (limit || 3).to_i
27
+ Faker::Lorem.sentences(sentences).join(SPACE)
28
+
29
+ end
30
+
31
+ end
32
+
33
+ private
34
+
35
+ SPACE = ' '
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,67 @@
1
+ # Image swapping technique courtesy of Email on Acid.
2
+ # http://www.emailonacid.com/blog/details/C13/a_slick_new_image_swapping_technique_for_responsive_emails
3
+ module Inkcite
4
+ module Renderer
5
+ class MobileImage < ImageBase
6
+
7
+ # Image swapping technique
8
+ def render tag, opt, ctx
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
+ # This is a transient, wrapper Element that we're going to use to
20
+ # style the attributes of the object that will appear when the
21
+ # email is viewed on a mobile device.
22
+ img = Element.new('mobile-img')
23
+
24
+ mix_dimensions img, opt
25
+
26
+ mix_background img, opt
27
+
28
+ display = opt[:display]
29
+ img.style[:display] = "#{display}" if display && display != BLOCK && display != DEFAULT
30
+
31
+ align = opt[:align]
32
+ img.style[:float] = align unless align.blank?
33
+
34
+ # Create a custom klass from the mobile image source name.
35
+ klass = klass_name(opt[:src], ctx)
36
+
37
+ src = image_url(opt[:src], opt, ctx)
38
+ img.style[BACKGROUND_IMAGE] = "url(#{src})"
39
+
40
+ # Initially, copy the height and width into the CSS so that the
41
+ # span assumes the exact dimensions of the image.
42
+ DIMENSIONS.each { |dim| img.style[dim] = px(opt[dim]) }
43
+
44
+ mobile = opt[:mobile]
45
+
46
+ # For FILL-style mobile images, override the width. The height (in px)
47
+ # will ensure that the span displays at a desireable size and the
48
+ # 'cover' attribute will ensure that the image fills the available
49
+ # space ala responsive web design.
50
+ # http://www.campaignmonitor.com/guides/mobile/optimizing-images/
51
+ img.style[:width] = '100%' if mobile == FILL
52
+
53
+ # Now visualize a span element
54
+ span = Element.new('span')
55
+
56
+ mix_responsive span, opt, ctx, IMAGE
57
+
58
+ # Add the class that handles inserting the correct background image.
59
+ ctx.media_query << span.add_rule(Rule.new('span', klass, img.style))
60
+
61
+ span.to_s
62
+ end
63
+
64
+ end
65
+ end
66
+ end
67
+
@@ -0,0 +1,40 @@
1
+ module Inkcite
2
+ module Renderer
3
+
4
+ class MobileStyle < Responsive
5
+
6
+ def render tag, opt, ctx
7
+
8
+ klass = detect(opt[:name], opt[:id])
9
+ if klass.blank?
10
+ ctx.error('Declaring a mobile style requires a name attribute')
11
+
12
+ else
13
+
14
+ mq = ctx.media_query
15
+
16
+ declarations = opt[:style]
17
+ if declarations.blank?
18
+ ctx.error('Declaring a mobile style requires a style attribute', { :name => klass })
19
+
20
+ elsif !mq.find_by_klass(klass).nil?
21
+ ctx.error('A mobile style was already defined with that class name', { :name => klass, :style => declarations })
22
+
23
+ else
24
+
25
+ # Create a new rule with the specified klass and declarations but mark
26
+ # it inactive. Like other rule presets, it will be activated on first use.
27
+ mq << Rule.new(UNIVERSAL, klass, declarations, false)
28
+
29
+ end
30
+
31
+ end
32
+
33
+ nil
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+ end
40
+
@@ -0,0 +1,27 @@
1
+ # Brian Graves' Toggle Responsive Pattern
2
+ # http://briangraves.github.io/ResponsiveEmailPatterns/patterns/navigation/toggle.html
3
+ module Inkcite
4
+ module Renderer
5
+
6
+ class MobileToggleOn < Responsive
7
+
8
+ def render tag, opt, ctx
9
+
10
+ return '{/a}' if tag == '/mobile-toggle-on'
11
+
12
+ id = opt[:id]
13
+ if id.blank?
14
+ ctx.error('The mobile-toggle-on requires an id')
15
+
16
+ else
17
+ "{a href=\"##{id}\" mobile=\"show\"}"
18
+
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+ end
27
+
@@ -0,0 +1,48 @@
1
+ module Inkcite
2
+ module Renderer
3
+ class OutlookBackground < Base
4
+
5
+ def render tag, opt, ctx
6
+
7
+ # Do nothing if vml is disabled globally.
8
+ return nil unless ctx.vml_enabled?
9
+
10
+ html = '<!--[if gte mso 9]>'
11
+
12
+ if tag == '/outlook-bg'
13
+ html << '</div>'
14
+ html << '</v:textbox>'
15
+ html << '</v:rect>'
16
+
17
+ else
18
+
19
+ src = opt[:src]
20
+ raise 'Outlook background missing required src attribute' if src.blank?
21
+
22
+ width = opt[:width].to_i
23
+ height = opt[:height].to_i
24
+ raise "Outlook background requires dimensions: #{width}x#{height} " if width <= 0 || height <= 0
25
+
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
+ )
30
+
31
+ html << render_tag('v:fill', { :type => 'tile', :src => quote(ctx.image_url(src)), :color => hex(opt[:bgcolor]), :self_close => true })
32
+
33
+ html << render_tag('v:textbox', { :inset => '0,0,0,0' })
34
+ html << '<div>'
35
+
36
+ # Flag the context as having had VML used within it.
37
+ ctx.vml_used!
38
+
39
+ end
40
+
41
+ html << '<![endif]-->'
42
+
43
+ html
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,31 @@
1
+ module Inkcite
2
+ module Renderer
3
+ class Partial < Base
4
+
5
+ def render tag, opt, ctx
6
+
7
+ # Get the name of the file to include and then resolve the full
8
+ # path to the file relative to the email's project directory.
9
+ file_name = opt[:file]
10
+ file = ctx.email.project_file(file_name)
11
+
12
+ # Verify the file exists and route it through ERB. Otherwise
13
+ # let the designer know that the file is missing.
14
+ if File.exist?(file)
15
+ ctx.eval_erb(File.open(file).read, file_name)
16
+
17
+ else
18
+ ctx.error "Include not found", :file => file
19
+
20
+ # Return an empty string so that the renderer has something
21
+ # to process - otherwise it throws an additional error on
22
+ # the command line.
23
+ ''
24
+
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,22 @@
1
+ module Inkcite
2
+ module Renderer
3
+ class Preheader < Base
4
+
5
+ def render tag, opt, ctx
6
+
7
+ if tag == '/preheader'
8
+ '</span>'
9
+
10
+ else
11
+
12
+ # Preheader text styling courtesy "Don’t forget about preheader text" section of
13
+ # Lee Munroe's blog entry: http://www.leemunroe.com/building-html-email/
14
+ '<span style="color: transparent; display: none !important; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">'
15
+
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,39 @@
1
+ module Inkcite
2
+ module Renderer
3
+ class Property < Base
4
+
5
+ def render tag, opt, ctx
6
+
7
+ html = ctx[tag]
8
+ if html.nil?
9
+ ctx.error 'Unknown tag or property', { :tag => tag, :opt => "[#{opt.to_query}]" }
10
+ return nil
11
+ end
12
+
13
+ # Need to clone the property - otherwise, we modify the original property.
14
+ # Which is bad.
15
+ html = html.clone
16
+
17
+ Parser.each html, VARIABLE_REGEX do |pair|
18
+
19
+ # Split the declaration on the equals sign.
20
+ variable, default = pair.split(EQUALS, 2)
21
+
22
+ # Check to see if the variable has been defined in the parameters. If so, use that
23
+ # value - otherwise, inherit the default.
24
+ (opt[variable.to_sym] || default).to_s
25
+
26
+ end
27
+
28
+ end
29
+
30
+ private
31
+
32
+ VARIABLE_REGEX = /\$([^\$]+)\$/
33
+
34
+ DOLLAR = '$'
35
+ EQUALS = '='
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,334 @@
1
+ module Inkcite
2
+ module Renderer
3
+ class Responsive < Base
4
+
5
+ BUTTON = 'button'
6
+ DROP = 'drop'
7
+ FILL = 'fill'
8
+ HIDE = 'hide'
9
+ IMAGE = 'img'
10
+ SHOW = 'show'
11
+ SWITCH = 'switch'
12
+ SWITCH_UP = 'switch-up'
13
+ TOGGLE = 'toggle'
14
+
15
+ # For elements that take on different background properties
16
+ # when they go responsive
17
+ MOBILE_BGCOLOR = :'mobile-bgcolor'
18
+ MOBILE_BACKGROUND = :'mobile-background'
19
+ MOBILE_BACKGROUND_COLOR = :'mobile-background-color'
20
+ MOBILE_BACKGROUND_IMAGE = :'mobile-background-image'
21
+ MOBILE_BACKGROUND_REPEAT = :'mobile-background-repeat'
22
+ MOBILE_BACKGROUND_POSITION = :'mobile-background-position'
23
+ MOBILE_BACKGROUND_SIZE = :'mobile-background-size'
24
+
25
+ class Rule
26
+
27
+ attr_reader :declarations
28
+ attr_reader :klass
29
+
30
+ def initialize tags, klass, declarations, active=true
31
+ @klass = klass
32
+ @declarations = declarations
33
+
34
+ @tags = Set.new [*tags]
35
+
36
+ # By default, a rule isn't considered active until it has
37
+ # been marked used. This allows the view to declare built-in
38
+ # styles (such as hide or stack) that don't show up in the
39
+ # rendered HTML unless the author references them.
40
+ @active = active
41
+
42
+ end
43
+
44
+ def << tag
45
+ @tags << tag
46
+ end
47
+
48
+ def activate!
49
+ @active = true
50
+ end
51
+
52
+ def active?
53
+ @active
54
+ end
55
+
56
+ def att_selector_string
57
+ "[class~=#{Renderer.quote(@klass)}]"
58
+ end
59
+
60
+ def block?
61
+ declaration_string.downcase.include?('block')
62
+ end
63
+
64
+ def declaration_string
65
+ if @declarations.is_a?(Hash)
66
+ Renderer.render_styles(@declarations)
67
+ elsif @declarations.is_a?(Array)
68
+ @declarations.join(' ')
69
+ else
70
+ @declarations.to_s
71
+ end
72
+ end
73
+
74
+ def include? tag
75
+ universal? || @tags.include?(tag)
76
+ end
77
+
78
+ def to_css
79
+
80
+ rule = ""
81
+
82
+ att_selector = att_selector_string
83
+
84
+ if universal?
85
+
86
+ # Only the attribute selector is needed when the rule is universal.
87
+ # http://www.w3.org/TR/CSS2/selector.html#universal-selector
88
+ rule << att_selector
89
+
90
+ else
91
+
92
+ # Create an attribute selector that targets each tag.
93
+ @tags.sort.each do |tag|
94
+ rule << ',' unless rule.blank?
95
+ rule << tag
96
+ rule << att_selector
97
+ end
98
+
99
+ end
100
+
101
+ rule << " { "
102
+ rule << declaration_string
103
+ rule << " }"
104
+
105
+ rule
106
+ end
107
+
108
+ def universal?
109
+ @tags.include?(UNIVERSAL)
110
+ end
111
+
112
+ end
113
+
114
+ class TargetRule < Rule
115
+
116
+ def initialize tag, klass
117
+ super tag, klass, 'display: block !important;'
118
+ end
119
+
120
+ def att_selector_string
121
+ "[id=#{@klass}]:target"
122
+ end
123
+
124
+ end
125
+
126
+ def self.presets ctx
127
+
128
+ styles = []
129
+
130
+ # HIDE, which can be used on any responsive element, makes it disappear
131
+ # on mobile devices.
132
+ styles << Rule.new(UNIVERSAL, HIDE, 'display: none !important;', false)
133
+
134
+ # SHOW, which means the element is hidden on desktop but shown on mobile.
135
+ styles << Rule.new(UNIVERSAL, SHOW, 'display: block !important;', false)
136
+
137
+ # Brian Graves' Column Drop Pattern: Table goes to 100% width by way of
138
+ # the FILL rule and its cells stack vertically.
139
+ # http://briangraves.github.io/ResponsiveEmailPatterns/patterns/layouts/column-drop.html
140
+ styles << Rule.new('td', DROP, 'display: block; width: 100% !important; background-size: 100% auto !important; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box;', false)
141
+
142
+ # Brian Graves' Column Switch Pattern: Allows columns in a table to
143
+ # be reordered based on up and down states.
144
+ # http://www.degdigital.com/blog/content-choreography-in-responsive-email/
145
+ styles << Rule.new('td', SWITCH, 'display: table-footer-group; width: 100% !important; background-size: 100% auto !important;')
146
+ styles << Rule.new('td', SWITCH_UP, 'display: table-header-group; width: 100% !important; background-size: 100% auto !important;')
147
+
148
+ # FILL causes specific types of elements to expand to 100% of the available
149
+ # width of the mobile device.
150
+ styles << Rule.new('img', FILL, 'width: 100% !important; height: auto !important;', false)
151
+ styles << Rule.new([ 'table', 'td' ], FILL, 'width: 100% !important; background-size: 100% auto !important;', false)
152
+
153
+ # For mobile-image tags.
154
+ styles << Rule.new('span', IMAGE, 'display: block; background-position: center; background-size: cover;', false)
155
+
156
+ # BUTTON causes ordinary links to transform into buttons based
157
+ # on the styles configured by the developer.
158
+ cfg = Button::Config.new(ctx)
159
+
160
+ button_styles = {
161
+ :color => "#{cfg.color} !important",
162
+ :display => 'block',
163
+ BACKGROUND_COLOR => cfg.bgcolor,
164
+ TEXT_SHADOW => "0 -1px 0 #{cfg.text_shadow}"
165
+ }
166
+
167
+ button_styles[:border] = cfg.border unless cfg.border.blank?
168
+ button_styles[BORDER_BOTTOM] = cfg.border_bottom if cfg.bevel > 0
169
+ button_styles[BORDER_RADIUS] = Renderer.px(cfg.border_radius) if cfg.border_radius > 0
170
+ button_styles[FONT_WEIGHT] = cfg.font_weight unless cfg.font_weight.blank?
171
+ button_styles[:height] = Renderer.px(cfg.height) if cfg.height > 0
172
+ button_styles[MARGIN_TOP] = Renderer.px(cfg.margin_top) if cfg.margin_top > 0
173
+ button_styles[:padding] = Renderer.px(cfg.padding) if cfg.padding > 0
174
+ button_styles[TEXT_ALIGN] = 'center'
175
+
176
+ styles << Rule.new('a', BUTTON, button_styles, false)
177
+
178
+ styles
179
+ end
180
+
181
+ protected
182
+
183
+ def mix_font element, opt, ctx, parent=nil
184
+
185
+ # Let the super class do its thing and grab the name of the font
186
+ # style that was applied, if any.
187
+ font = super
188
+
189
+ # Will hold the mobile font overrides for this element, if any.
190
+ style = { }
191
+
192
+ font_size = detect_font(MOBILE_FONT_SIZE, font, opt, parent, ctx)
193
+ style[FONT_SIZE] = "#{px(font_size)} !important" unless font_size.blank?
194
+
195
+ line_height = detect_font(MOBILE_LINE_HEIGHT, font, opt, parent, ctx)
196
+ style[LINE_HEIGHT] = "#{px(line_height)} !important" unless line_height.blank?
197
+
198
+ mix_responsive_style element, opt, ctx, Renderer.render_styles(style) unless style.blank?
199
+
200
+ font
201
+ end
202
+
203
+ def mix_responsive element, opt, ctx, klass=nil
204
+
205
+ # Apply the "mobile" attribute or use the override if one was provided.
206
+ mix_responsive_klass element, opt, ctx, klass || opt[:mobile]
207
+
208
+ # Apply the "mobile-style" attribute if one was provided.
209
+ mix_responsive_style element, opt, ctx, opt[MOBILE_STYLE]
210
+
211
+ end
212
+
213
+ def mix_responsive_klass element, opt, ctx, klass
214
+
215
+ # Nothing to do if there is no class specified.s
216
+ return nil if klass.blank?
217
+
218
+ mq = ctx.media_query
219
+
220
+ # The element's tag - e.g. table, td, etc.
221
+ tag = element.tag
222
+
223
+ # Special handling for TOGGLE-able elements which are made
224
+ # visible by another element being clicked.
225
+ if klass == TOGGLE
226
+
227
+ id = opt[:id]
228
+ if id.blank?
229
+ ctx.errors 'Mobile elements with toggle behavior require an ID attribute', { :tag => tag} if id.blank?
230
+
231
+ else
232
+
233
+ # Make sure the element's ID field is populated
234
+ element[:id] = id
235
+
236
+ # Add a rule which makes this element visible when the target
237
+ # field matches the identity.
238
+ mq << TargetRule.new(tag, id)
239
+
240
+ # Toggle-able elements are HIDE on mobile by default.
241
+ klass = HIDE
242
+
243
+ end
244
+ end
245
+
246
+ # Check to see if there is already a rule that specifically matches this klass
247
+ # and tag combination - e.g. td.hide
248
+ rule = mq.find_by_tag_and_klass(tag, klass)
249
+ if rule.nil?
250
+
251
+ # If no rule was found then find the first that matches the klass.
252
+ rule = mq.find_by_klass(klass)
253
+
254
+ # If no rule was found and the declaration is blank then we have
255
+ # an unknown mobile behavior.
256
+ if rule.nil?
257
+ ctx.error 'Undefined mobile behavior - are you missing a mobile-style declaration?', { :tag => tag, :mobile => klass }
258
+ return nil
259
+ end
260
+
261
+ rule << tag if !rule.include?(tag)
262
+
263
+ end
264
+
265
+ # If the rule is SHOW (only on mobile) we need to restyle the element
266
+ # so it is hidden.
267
+ element.style[:display] = 'none' if klass == SHOW
268
+
269
+ # Add the responsive rule to the element
270
+ element.add_rule rule
271
+
272
+ end
273
+
274
+ def mix_responsive_style element, opt, ctx, declarations=nil
275
+
276
+ # Check to see if a mobile style (e.g. "mobile-style='background-color: #ff0;'")
277
+ # has been declared for this element.
278
+ declarations ||= opt[MOBILE_STYLE]
279
+ return if declarations.blank?
280
+
281
+ mq = ctx.media_query
282
+
283
+ tag = element.tag
284
+
285
+ # If no klass was specified, check to see if any previously defined rule matches
286
+ # the style declarations. If so, we'll reuse that rule and apply the klass
287
+ # to this object to avoid unnecessary duplication in the HTML.
288
+ rule = mq.find_by_declaration(declarations);
289
+ if rule.nil?
290
+
291
+ # Generate a unique class name for this style if it has not already been declared.
292
+ # These are of the form m001, etc. Redability is not important because it's
293
+ # dynamically generated and referenced.
294
+ klass = unique_klass(ctx)
295
+
296
+ rule = Rule.new(tag, klass, declarations)
297
+
298
+ # Add the rule to the list of those that will be rendered into the
299
+ # completed email.
300
+ mq << rule
301
+
302
+ elsif !rule.include?(tag)
303
+
304
+ # Make sure this tag is included in the list of those that
305
+ # the CSS will match against.
306
+ rule << tag
307
+
308
+ end
309
+
310
+ # Add the responsive rule to the element which automatically adds its
311
+ # class to the element's list.
312
+ element.add_rule rule
313
+
314
+ end
315
+
316
+ def unique_klass ctx
317
+ "m%1d" % ctx.unique_id(:m)
318
+ end
319
+
320
+ private
321
+
322
+ # Attribute used to declare custom mobile styles for an element.
323
+ MOBILE_STYLE = :'mobile-style'
324
+
325
+ # Universal CSS selector.
326
+ UNIVERSAL = '*'
327
+
328
+ # For font overrides on mobile devices.
329
+ MOBILE_FONT_SIZE = :'mobile-font-size'
330
+ MOBILE_LINE_HEIGHT = :'mobile-line-height'
331
+
332
+ end
333
+ end
334
+ end