inkcite 1.12.1 → 1.13.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 +4 -4
- data/lib/inkcite/animation.rb +96 -74
- data/lib/inkcite/cli/base.rb +6 -0
- data/lib/inkcite/cli/preview.rb +1 -1
- data/lib/inkcite/cli/test.rb +3 -3
- data/lib/inkcite/renderer.rb +7 -3
- data/lib/inkcite/renderer/background.rb +153 -0
- data/lib/inkcite/renderer/base.rb +58 -29
- data/lib/inkcite/renderer/container_base.rb +11 -4
- data/lib/inkcite/renderer/div.rb +1 -2
- data/lib/inkcite/renderer/element.rb +6 -7
- data/lib/inkcite/renderer/responsive.rb +124 -37
- data/lib/inkcite/renderer/snow.rb +53 -248
- data/lib/inkcite/renderer/sparkle.rb +77 -0
- data/lib/inkcite/renderer/special_effect.rb +429 -0
- data/lib/inkcite/renderer/style.rb +81 -0
- data/lib/inkcite/renderer/table_base.rb +4 -12
- data/lib/inkcite/renderer/td.rb +6 -24
- data/lib/inkcite/renderer/video_preview.rb +17 -7
- data/lib/inkcite/version.rb +1 -1
- data/lib/inkcite/view.rb +53 -18
- data/lib/inkcite/view/media_query.rb +1 -1
- data/test/animation_spec.rb +14 -10
- data/test/renderer/background_spec.rb +59 -0
- data/test/renderer/div_spec.rb +11 -1
- data/test/renderer/image_spec.rb +1 -1
- data/test/renderer/mobile_image_spec.rb +3 -3
- data/test/renderer/mobile_style_spec.rb +1 -1
- data/test/renderer/span_spec.rb +1 -1
- data/test/renderer/table_spec.rb +22 -7
- data/test/renderer/td_spec.rb +29 -8
- data/test/renderer/video_preview_spec.rb +3 -3
- metadata +8 -5
- data/lib/inkcite/renderer/outlook_background.rb +0 -96
- data/test/renderer/outlook_background_spec.rb +0 -61
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: db3a1196fe9bcc49e718925d27731a469807acdc
|
4
|
+
data.tar.gz: 4dfc1aa8998ec37e5ed03a81facaf7f75ac6bfc9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e1e085b9a0668c30f9c6bd968f962caa42205b45702c4c9cba5893bb71ea1fe0b63f6e85c90e767912c42640f36f3a94ed11b54078d189323fc1886a5d37ab7c
|
7
|
+
data.tar.gz: 92166f271e19bbcc7058136e2d77a441179a341ef8107d19c24a0f15f8ddc883c319d5410a50edfecfdac89180b1d518dd5270a1614fd09846f8f3145c3d08c6
|
data/lib/inkcite/animation.rb
CHANGED
@@ -1,135 +1,157 @@
|
|
1
1
|
module Inkcite
|
2
|
-
|
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
|
-
|
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
|
-
@
|
19
|
+
@style[key]
|
16
20
|
end
|
17
21
|
|
18
22
|
def []= key, val
|
19
|
-
@
|
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
|
-
|
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
|
-
|
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
|
51
|
+
def to_css prefix
|
52
|
+
@style.to_css(prefix)
|
53
|
+
end
|
37
54
|
|
38
|
-
|
39
|
-
css << " #{@percent}%"
|
40
|
-
css << ' ' * (7 - css.length)
|
41
|
-
css << '{ '
|
42
|
-
css << Renderer.render_styles(@styles)
|
43
|
-
css << ' }'
|
55
|
+
private
|
44
56
|
|
45
|
-
|
46
|
-
|
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
|
-
|
61
|
+
_styles = {}
|
49
62
|
|
50
|
-
|
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
|
-
|
53
|
-
@name = name
|
54
|
-
@ctx = context
|
55
|
-
@keyframes = []
|
68
|
+
_styles
|
56
69
|
end
|
57
70
|
|
58
|
-
|
59
|
-
@keyframes << keyframe
|
60
|
-
end
|
71
|
+
end
|
61
72
|
|
62
|
-
|
63
|
-
|
64
|
-
end
|
73
|
+
# Infinite iteration count
|
74
|
+
INFINITE = 'infinite'
|
65
75
|
|
66
|
-
|
76
|
+
# Timing functions
|
77
|
+
LINEAR = 'linear'
|
78
|
+
EASE = 'ease'
|
79
|
+
EASE_IN_OUT = 'ease-in-out'
|
67
80
|
|
68
|
-
|
81
|
+
# Animation name, view context and array of keyframes
|
82
|
+
attr_reader :name, :ctx
|
69
83
|
|
70
|
-
|
84
|
+
attr_accessor :duration, :timing_function, :delay, :iteration_count
|
71
85
|
|
72
|
-
|
86
|
+
def initialize name, ctx
|
87
|
+
@name = name
|
88
|
+
@ctx = ctx
|
73
89
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
81
|
-
|
96
|
+
# Initialize the keyframes
|
97
|
+
@keyframes = []
|
82
98
|
|
83
99
|
end
|
84
100
|
|
85
|
-
def
|
86
|
-
|
87
|
-
end
|
101
|
+
def add_keyframe percent, styles={}
|
102
|
+
keyframe = Keyframe.new(percent, @ctx, styles)
|
88
103
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
def self.webkit_only? ctx
|
93
|
-
false #&& !(ctx.development? || ctx.browser?)
|
104
|
+
@keyframes << keyframe
|
105
|
+
|
106
|
+
keyframe
|
94
107
|
end
|
95
108
|
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
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
|
-
|
103
|
-
indentation = ' ' * indentation if indentation.is_a?(Integer)
|
126
|
+
end
|
104
127
|
|
105
|
-
|
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
|
-
#
|
108
|
-
|
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
|
-
|
111
|
-
_css = ''
|
141
|
+
css << seconds(@delay) if @delay > 0
|
112
142
|
|
113
|
-
|
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
|
-
|
145
|
+
css << @name
|
146
|
+
|
147
|
+
css.join(' ')
|
123
148
|
end
|
124
149
|
|
125
150
|
private
|
126
151
|
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
data/lib/inkcite/cli/base.rb
CHANGED
@@ -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'
|
data/lib/inkcite/cli/preview.rb
CHANGED
data/lib/inkcite/cli/test.rb
CHANGED
@@ -29,9 +29,9 @@ module Inkcite
|
|
29
29
|
USAGE
|
30
30
|
end
|
31
31
|
|
32
|
-
#
|
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
|
|
data/lib/inkcite/renderer.rb
CHANGED
@@ -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
|
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
|