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.
- 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
|