inkcite 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +110 -0
- data/Rakefile +8 -0
- data/assets/facebook-like.css +62 -0
- data/assets/facebook-like.js +59 -0
- data/assets/init/config.yml +97 -0
- data/assets/init/helpers.tsv +31 -0
- data/assets/init/source.html +60 -0
- data/assets/init/source.txt +6 -0
- data/bin/inkcite +6 -0
- data/inkcite.gemspec +42 -0
- data/lib/inkcite.rb +32 -0
- data/lib/inkcite/cli/base.rb +128 -0
- data/lib/inkcite/cli/build.rb +130 -0
- data/lib/inkcite/cli/init.rb +58 -0
- data/lib/inkcite/cli/preview.rb +30 -0
- data/lib/inkcite/cli/server.rb +123 -0
- data/lib/inkcite/cli/test.rb +61 -0
- data/lib/inkcite/email.rb +219 -0
- data/lib/inkcite/mailer.rb +140 -0
- data/lib/inkcite/minifier.rb +151 -0
- data/lib/inkcite/parser.rb +111 -0
- data/lib/inkcite/renderer.rb +177 -0
- data/lib/inkcite/renderer/base.rb +186 -0
- data/lib/inkcite/renderer/button.rb +168 -0
- data/lib/inkcite/renderer/div.rb +29 -0
- data/lib/inkcite/renderer/element.rb +82 -0
- data/lib/inkcite/renderer/footnote.rb +132 -0
- data/lib/inkcite/renderer/google_analytics.rb +35 -0
- data/lib/inkcite/renderer/image.rb +95 -0
- data/lib/inkcite/renderer/image_base.rb +82 -0
- data/lib/inkcite/renderer/in_browser.rb +38 -0
- data/lib/inkcite/renderer/like.rb +73 -0
- data/lib/inkcite/renderer/link.rb +243 -0
- data/lib/inkcite/renderer/litmus.rb +33 -0
- data/lib/inkcite/renderer/lorem.rb +39 -0
- data/lib/inkcite/renderer/mobile_image.rb +67 -0
- data/lib/inkcite/renderer/mobile_style.rb +40 -0
- data/lib/inkcite/renderer/mobile_toggle.rb +27 -0
- data/lib/inkcite/renderer/outlook_background.rb +48 -0
- data/lib/inkcite/renderer/partial.rb +31 -0
- data/lib/inkcite/renderer/preheader.rb +22 -0
- data/lib/inkcite/renderer/property.rb +39 -0
- data/lib/inkcite/renderer/responsive.rb +334 -0
- data/lib/inkcite/renderer/span.rb +21 -0
- data/lib/inkcite/renderer/table.rb +67 -0
- data/lib/inkcite/renderer/table_base.rb +149 -0
- data/lib/inkcite/renderer/td.rb +92 -0
- data/lib/inkcite/uploader.rb +173 -0
- data/lib/inkcite/util.rb +85 -0
- data/lib/inkcite/version.rb +3 -0
- data/lib/inkcite/view.rb +745 -0
- data/lib/inkcite/view/context.rb +38 -0
- data/lib/inkcite/view/media_query.rb +60 -0
- data/lib/inkcite/view/tag_stack.rb +38 -0
- data/test/email_spec.rb +16 -0
- data/test/parser_spec.rb +72 -0
- data/test/project/config.yml +98 -0
- data/test/project/helpers.tsv +56 -0
- data/test/project/images/inkcite.jpg +0 -0
- data/test/project/source.html +58 -0
- data/test/project/source.txt +6 -0
- data/test/renderer/button_spec.rb +45 -0
- data/test/renderer/div_spec.rb +101 -0
- data/test/renderer/element_spec.rb +31 -0
- data/test/renderer/footnote_spec.rb +57 -0
- data/test/renderer/image_spec.rb +82 -0
- data/test/renderer/link_spec.rb +84 -0
- data/test/renderer/mobile_image_spec.rb +27 -0
- data/test/renderer/mobile_style_spec.rb +37 -0
- data/test/renderer/td_spec.rb +126 -0
- data/test/renderer_spec.rb +28 -0
- data/test/view_spec.rb +15 -0
- 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
|