inkcite 1.12.1 → 1.13.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 729a1ea12dcb8c0180541d0b4aebb22346985f2f
4
- data.tar.gz: c10280cade618463c9ecebe81e50b1acca46a49e
3
+ metadata.gz: db3a1196fe9bcc49e718925d27731a469807acdc
4
+ data.tar.gz: 4dfc1aa8998ec37e5ed03a81facaf7f75ac6bfc9
5
5
  SHA512:
6
- metadata.gz: 39f5ebde420c845be3b262d7b4266a491bed516f561a5679616bd4d235693adbba3c6990bdc9b51c61facb00dcfd35b418c2c2f7f3602f13f7daeb7de971bdf6
7
- data.tar.gz: 2f8441f0f65e4faf1323229c30a8fbdd2ea4b31fc8cdc61f4a1520921bc23fb3cb6727e76cd898b104227f047a924d1af7150180dd8e152fa6e36d14c5ae586a
6
+ metadata.gz: e1e085b9a0668c30f9c6bd968f962caa42205b45702c4c9cba5893bb71ea1fe0b63f6e85c90e767912c42640f36f3a94ed11b54078d189323fc1886a5d37ab7c
7
+ data.tar.gz: 92166f271e19bbcc7058136e2d77a441179a341ef8107d19c24a0f15f8ddc883c319d5410a50edfecfdac89180b1d518dd5270a1614fd09846f8f3145c3d08c6
@@ -1,135 +1,157 @@
1
1
  module Inkcite
2
- module Animation
2
+ class Animation
3
3
 
4
4
  class Keyframe
5
5
 
6
- attr_reader :percent
6
+ attr_reader :percent, :style
7
+
8
+ def initialize percent, ctx, styles={}
7
9
 
8
- def initialize percent, styles={}
9
10
  # Animation percents are always rounded to the nearest whole number.
10
11
  @percent = percent.round(0)
11
- @styles = styles
12
+
13
+ # Instantiate a new Style for this percentage.
14
+ @style = Inkcite::Renderer::Style.new("#{@percent}%", ctx, styles)
15
+
12
16
  end
13
17
 
14
18
  def [] key
15
- @styles[key]
19
+ @style[key]
16
20
  end
17
21
 
18
22
  def []= key, val
19
- @styles[key] = val
23
+ @style[key] = val
20
24
  end
21
25
 
26
+ # For style chaining - e.g. keyframe.add(:key1, 'val').add(:key)
22
27
  def add key, val
23
- self[key] = val
28
+ @style[key] = val
24
29
  self
25
30
  end
26
31
 
32
+ # Appends a value to an existing key
33
+ def append key, val
34
+
35
+ @style[key] ||= ''
36
+ @style[key] << ' ' unless @style[key].blank?
37
+ @style[key] << val
38
+
39
+ end
40
+
27
41
  def add_with_prefixes key, val, ctx
28
42
 
29
- Animation.get_prefixes(ctx).each do |prefix|
43
+ ctx.prefixes.each do |prefix|
30
44
  _key = "#{prefix}#{key}".to_sym
31
45
  self[_key] = val
32
46
  end
47
+
33
48
  self
34
49
  end
35
50
 
36
- def to_s
51
+ def to_css prefix
52
+ @style.to_css(prefix)
53
+ end
37
54
 
38
- css = ''
39
- css << " #{@percent}%"
40
- css << ' ' * (7 - css.length)
41
- css << '{ '
42
- css << Renderer.render_styles(@styles)
43
- css << ' }'
55
+ private
44
56
 
45
- css
46
- end
57
+ # Creates a copy of the array of styles with the appropriate
58
+ # properties (e.g. transform) prefixed.
59
+ def get_prefixed_styles prefix
47
60
 
48
- end
61
+ _styles = {}
49
62
 
50
- class Keyframes
63
+ @styles.each_pair do |key, val|
64
+ key = "#{prefix}#{key}".to_sym if Inkcite::Renderer::Style.needs_prefixing?(key)
65
+ _styles[key] = val
66
+ end
51
67
 
52
- def initialize name, context
53
- @name = name
54
- @ctx = context
55
- @keyframes = []
68
+ _styles
56
69
  end
57
70
 
58
- def << keyframe
59
- @keyframes << keyframe
60
- end
71
+ end
61
72
 
62
- def add_keyframe percent, styles
63
- self << Keyframe.new(percent, styles)
64
- end
73
+ # Infinite iteration count
74
+ INFINITE = 'infinite'
65
75
 
66
- def to_s
76
+ # Timing functions
77
+ LINEAR = 'linear'
78
+ EASE = 'ease'
79
+ EASE_IN_OUT = 'ease-in-out'
67
80
 
68
- css = ''
81
+ # Animation name, view context and array of keyframes
82
+ attr_reader :name, :ctx
69
83
 
70
- keyframe_css = @keyframes.sort { |kf1,kf2| kf1.percent <=> kf2.percent }.collect(&:to_s).join("\n")
84
+ attr_accessor :duration, :timing_function, :delay, :iteration_count
71
85
 
72
- prefixes = Animation.get_prefixes(@ctx)
86
+ def initialize name, ctx
87
+ @name = name
88
+ @ctx = ctx
73
89
 
74
- prefixes.each do |prefix|
75
- css << "@#{prefix}keyframes #{@name} {\n"
76
- css << keyframe_css
77
- css << "\n}\n"
78
- end
90
+ # Default values for the animation's properties
91
+ @duration = 1
92
+ @delay = 0
93
+ @iteration_count = INFINITE
94
+ @timing_function = LINEAR
79
95
 
80
- css
81
- end
96
+ # Initialize the keyframes
97
+ @keyframes = []
82
98
 
83
99
  end
84
100
 
85
- def self.get_prefixes ctx
86
- ALL_BROWSERS
87
- end
101
+ def add_keyframe percent, styles={}
102
+ keyframe = Keyframe.new(percent, @ctx, styles)
88
103
 
89
- # True if we're limiting the animation to webkit only. In development
90
- # or in the browser version of the email, the animation should be as
91
- # compatible as possible but in all other cases it should be webkit only.
92
- def self.webkit_only? ctx
93
- false #&& !(ctx.development? || ctx.browser?)
104
+ @keyframes << keyframe
105
+
106
+ keyframe
94
107
  end
95
108
 
96
- # Renders the CSS with the appropriate browser prefixes based
97
- # on whether or not this version of the email is webkit only.
98
- def self.with_browser_prefixes css, ctx, opts={}
109
+ def to_keyframe_css
110
+
111
+ css = ''
112
+
113
+ # Sort the keyframes by percent in ascending order.
114
+ sorted_keyframes = @keyframes.sort { |kf1, kf2| kf1.percent <=> kf2.percent }
99
115
 
100
- indentation = opts[:indentation] || ''
116
+ # Iterate through each prefix and render a set of keyframes
117
+ # for each.
118
+ @ctx.prefixes.each do |prefix|
119
+ css << "@#{prefix}keyframes #{@name} {\n"
120
+ css << sorted_keyframes.collect { |kf| kf.to_css(prefix) }.join("\n")
121
+ css << "\n}\n"
122
+ end
123
+
124
+ css
101
125
 
102
- # Convert an integer indentation value into that number of spaces.
103
- indentation = ' ' * indentation if indentation.is_a?(Integer)
126
+ end
104
127
 
105
- separator = opts[:separator] || "\n"
128
+ # Renders this Animation declaration in the syntax defined here
129
+ # https://developer.mozilla.org/en-US/docs/Web/CSS/animation
130
+ # e.g. "3s ease-in 1s 2 reverse both paused slidein"
131
+ def to_s
106
132
 
107
- # Determine which prefixes will be applied.
108
- browser_prefixes = webkit_only?(ctx) ? WEBKIT_BROWSERS : ALL_BROWSERS
133
+ # The desired format is: duration | timing-function | delay |
134
+ # iteration-count | direction | fill-mode | play-state | name
135
+ # Although currently not all attributes are supported.
136
+ css = [
137
+ seconds(@duration),
138
+ @timing_function
139
+ ]
109
140
 
110
- # This will hold the completed CSS with all prefixes applied.
111
- _css = ''
141
+ css << seconds(@delay) if @delay > 0
112
142
 
113
- # Iterate through the prefixes and apply them with the indentation
114
- # and CSS declaration with line breaks.
115
- browser_prefixes.each do |prefix|
116
- _css << indentation
117
- _css << prefix
118
- _css << css
119
- _css << separator
120
- end
143
+ css << @iteration_count
121
144
 
122
- _css
145
+ css << @name
146
+
147
+ css.join(' ')
123
148
  end
124
149
 
125
150
  private
126
151
 
127
-
128
- # Static arrays with browser prefixes. Turns out that Firefox, IE and Opera
129
- # don't require a prefix so to target everything we need the non-prefixed version
130
- # (hence the blank entry) plus the webkit prefix.
131
- WEBKIT_BROWSERS = ['-webkit-']
132
- ALL_BROWSERS = [''] + WEBKIT_BROWSERS
152
+ def seconds val
153
+ "#{val}s"
154
+ end
133
155
 
134
156
  end
135
157
  end
@@ -49,6 +49,9 @@ module Inkcite
49
49
  :aliases => '-a',
50
50
  :desc => 'Add one or more (space-separated) recipients to this specific mailing',
51
51
  :type => :array
52
+ option :'no-upload',
53
+ :desc => 'Skip the asset upload, email the preview immediately',
54
+ :type => :boolean
52
55
  def preview to=:developer
53
56
  require_relative 'preview'
54
57
  Cli::Preview.invoke(email, to, options)
@@ -100,6 +103,9 @@ module Inkcite
100
103
  end
101
104
 
102
105
  desc 'test [options]', 'Tests (or re-tests) the email with Litmus or Email on Acid'
106
+ option :'no-upload',
107
+ :desc => 'Skip the asset upload, test the email immediately',
108
+ :type => :boolean
103
109
  option :version,
104
110
  :aliases => '-v',
105
111
  :desc => 'Test a specific version of the email'
@@ -8,7 +8,7 @@ module Inkcite
8
8
 
9
9
  # Push the browser preview(s) up to the server to ensure that the
10
10
  # latest images and "view in browser" versions are available.
11
- email.upload
11
+ email.upload unless opt[:'no-upload']
12
12
 
13
13
  also = opt[:also]
14
14
  unless also.blank?
@@ -29,9 +29,9 @@ module Inkcite
29
29
  USAGE
30
30
  end
31
31
 
32
- # Push the browser preview up to the server to ensure that the
33
- # latest images are available.
34
- email.upload
32
+ # Unless disabled, push the browser preview up to the server to ensure
33
+ # that the latest images are available.
34
+ email.upload unless opts[:'no-upload']
35
35
 
36
36
  Inkcite::Mailer.send(email, opts.merge({ :to => send_to }))
37
37
 
@@ -2,9 +2,12 @@ require_relative 'renderer/base'
2
2
  require_relative 'renderer/element'
3
3
  require_relative 'renderer/responsive'
4
4
  require_relative 'renderer/container_base'
5
+ require_relative 'renderer/special_effect'
5
6
  require_relative 'renderer/image_base'
6
7
  require_relative 'renderer/table_base'
8
+ require_relative 'renderer/style'
7
9
 
10
+ require_relative 'renderer/background'
8
11
  require_relative 'renderer/button'
9
12
  require_relative 'renderer/div'
10
13
  require_relative 'renderer/footnote'
@@ -20,7 +23,6 @@ require_relative 'renderer/mobile_image'
20
23
  require_relative 'renderer/mobile_only'
21
24
  require_relative 'renderer/mobile_style'
22
25
  require_relative 'renderer/mobile_toggle'
23
- require_relative 'renderer/outlook_background'
24
26
  require_relative 'renderer/partial'
25
27
  require_relative 'renderer/preheader'
26
28
  require_relative 'renderer/property'
@@ -28,6 +30,7 @@ require_relative 'renderer/redacted'
28
30
  require_relative 'renderer/snow'
29
31
  require_relative 'renderer/social'
30
32
  require_relative 'renderer/span'
33
+ require_relative 'renderer/sparkle'
31
34
  require_relative 'renderer/table'
32
35
  require_relative 'renderer/td'
33
36
  require_relative 'renderer/video_preview'
@@ -68,7 +71,7 @@ module Inkcite
68
71
  def self.hex color
69
72
 
70
73
  # Convert #rgb into #rrggbb
71
- if !color.blank? && color.length < 7
74
+ if !color.blank? && color.length == 4 && color.start_with?('#')
72
75
  red = color[1]
73
76
  green = color[2]
74
77
  blue = color[3]
@@ -167,6 +170,7 @@ module Inkcite
167
170
  @renderers ||= {
168
171
  :'++' => Increment.new,
169
172
  :a => Link.new,
173
+ :background => Background.new,
170
174
  :button => Button.new,
171
175
  :div => Div.new,
172
176
  :facebook => Social::Facebook.new,
@@ -183,12 +187,12 @@ module Inkcite
183
187
  :'mobile-only' => MobileOnly.new,
184
188
  :'mobile-style' => MobileStyle.new,
185
189
  :'mobile-toggle-on' => MobileToggleOn.new,
186
- :'outlook-bg' => OutlookBackground.new,
187
190
  :pintrest => Social::Pintrest.new,
188
191
  :preheader => Preheader.new,
189
192
  :redacted => Redacted.new,
190
193
  :snow => Snow.new,
191
194
  :span => Span.new,
195
+ :sparkle => Sparkle.new,
192
196
  :table => Table.new,
193
197
  :td => Td.new,
194
198
  :twitter => Social::Twitter.new,
@@ -0,0 +1,153 @@
1
+ module Inkcite
2
+ module Renderer
3
+
4
+ # Bulletproof background image support courtesy of @stigm via Campaign Monitor
5
+ # https://backgrounds.cm/
6
+ #
7
+ # {background src=YJOX1PC.png bgcolor=#7bceeb height=92 width=120}
8
+ # ...
9
+ # {/background}
10
+ #
11
+ class Background < ImageBase
12
+
13
+ def render tag, opt, ctx
14
+
15
+ html = ''
16
+
17
+ if tag == '/background'
18
+
19
+ html << '</div>'
20
+
21
+ # If VML is enabled, then close the textbox and rect that were created
22
+ # by the opening tags.
23
+ if ctx.vml_enabled?
24
+ html << '{outlook-only}'
25
+ html << '</v:textbox>'
26
+ html << '</v:rect>'
27
+ html << '{/outlook-only}'
28
+ end
29
+
30
+ html << '{/td}'
31
+ html << '{/table}'
32
+
33
+ else
34
+
35
+ # Primary background image
36
+ src = opt[:src]
37
+
38
+ # Dimensions
39
+ width = opt[:width]
40
+ height = opt[:height].to_i
41
+
42
+ # True if the background image's width should fill the available
43
+ # horizontal space. Specified by either leaving the width blank or
44
+ # specifying 'fill' or '100%'
45
+ fill_width = width.nil? || width == 'fill' || width == '100%' || width.to_i <= 0
46
+
47
+ table = Element.new('table')
48
+ table[:height] = height if height > 0
49
+ table[:width] = (fill_width ? '100%' : width)
50
+ table[:background] = quote(src) unless none?(src)
51
+
52
+ # Iterate through the list of the parameters that are copied straight into
53
+ # the internal {table} Helper. This is a sanitized list of supported
54
+ # parameters to prevent the user from setting things inadvertently that
55
+ # might interfere with the display of the background (e.g. padding)
56
+ TABLE_PASSTHRU_OPS.each do |key|
57
+ val = opt[key]
58
+ table[key] = quote(val) unless none?(val)
59
+ end
60
+
61
+ # Determine if a fallback background color has been defined.
62
+ bgcolor = detect_bgcolor(opt)
63
+ table[:bgcolor] = quote(bgcolor) unless none?(bgcolor)
64
+
65
+ # Check for a background gradient
66
+ bggradient = detect_bggradient(opt)
67
+ table[:bggradient] = quote(bggradient) unless none?(bggradient)
68
+
69
+ td = Element.new('td')
70
+
71
+ valign = opt[:valign]
72
+ td[:valign] = valign unless valign.blank?
73
+
74
+ html << table.to_helper
75
+ html << td.to_helper
76
+
77
+ # VML is only added if it is enabled for the project.
78
+ if ctx.vml_enabled?
79
+
80
+ # Get the fully-qualified URL to the image or placeholder image if it's
81
+ # missing from the images directory. This comes back with quotes around it.
82
+ outlook_src = image_url(opt[OUTLOOK_SRC] || src, opt, ctx, false)
83
+
84
+ # True if the height of the background image will fit to content within the
85
+ # background element (specified by omitting the 'height' attribute).
86
+ fit_to_shape = height <= 0
87
+
88
+ rect = Element.new('v:rect', { :'xmlns:v' => quote('urn:schemas-microsoft-com:vml'), :fill => quote('t'), :stroke => quote('f') })
89
+
90
+ if fill_width
91
+
92
+ # The number you pass to 'mso-width-percent' is ten times the percentage you'd like.
93
+ # https://www.emailonacid.com/blog/article/email-development/emailology_vector_markup_language_and_backgrounds
94
+ rect.style[:'mso-width-percent'] = 1000
95
+
96
+ else
97
+ rect.style[:width] = px(width)
98
+
99
+ end
100
+
101
+ rect.style[:height] = px(height) unless fit_to_shape
102
+
103
+ fill = Element.new('v:fill', { :type => '"tile"', :src => outlook_src, :self_close => true })
104
+ fill[:color] = quote(bgcolor) unless none?(bgcolor)
105
+
106
+ textbox = Element.new('v:textbox', :inset => '"0,0,0,0"')
107
+ textbox.style[:'mso-fit-shape-to-text'] = 'True' if fit_to_shape
108
+
109
+ html << '{outlook-only}'
110
+ html << rect.to_s
111
+ html << fill.to_s
112
+ html << textbox.to_s
113
+ html << '{/outlook-only}'
114
+
115
+ # Flag the context as having had VML used within it.
116
+ ctx.vml_used!
117
+
118
+ end
119
+
120
+ div = Element.new('div')
121
+
122
+ # Font family and other attributes get reset within the v:textbox so allow
123
+ # the font series of attributes to be applied.
124
+ mix_font div, opt, ctx
125
+
126
+ # Text alignment within the div.
127
+ mix_text_align div, opt, ctx
128
+
129
+ html << div.to_s
130
+
131
+ end
132
+
133
+ html
134
+ end
135
+
136
+ private
137
+
138
+ # The custom
139
+ MOBILE_SRC = :'mobile-src'
140
+
141
+ # These are the parameters that are passed directly from
142
+ # the provided opt to the {table} rendered within the
143
+ # background Helper.
144
+ TABLE_PASSTHRU_OPS = [
145
+ BACKGROUND_POSITION, :border, BORDER_BOTTOM, BORDER_LEFT, BORDER_RADIUS, BORDER_RIGHT,
146
+ BORDER_SPACING, BORDER_TOP, :mobile, MOBILE_BGCOLOR, MOBILE_BACKGROUND, MOBILE_BACKGROUND_COLOR,
147
+ MOBILE_BACKGROUND_IMAGE, MOBILE_BACKGROUND_REPEAT, MOBILE_BACKGROUND_POSITION, MOBILE_PADDING,
148
+ MOBILE_SRC, MOBILE_BACKGROUND_SIZE
149
+ ]
150
+
151
+ end
152
+ end
153
+ end