inkcite 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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