inkcite 1.9.1 → 1.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +8 -1
- data/lib/inkcite/cli/base.rb +9 -0
- data/lib/inkcite/cli/scope.rb +127 -0
- data/lib/inkcite/minifier.rb +9 -1
- data/lib/inkcite/parser.rb +9 -0
- data/lib/inkcite/renderer.rb +3 -2
- data/lib/inkcite/renderer/base.rb +10 -2
- data/lib/inkcite/renderer/container_base.rb +36 -0
- data/lib/inkcite/renderer/div.rb +2 -17
- data/lib/inkcite/renderer/image.rb +9 -3
- data/lib/inkcite/renderer/image_base.rb +25 -11
- data/lib/inkcite/renderer/link.rb +10 -4
- data/lib/inkcite/renderer/mobile_image.rb +2 -2
- data/lib/inkcite/renderer/partial.rb +1 -1
- data/lib/inkcite/renderer/property.rb +8 -1
- data/lib/inkcite/renderer/responsive.rb +2 -2
- data/lib/inkcite/renderer/span.rb +6 -16
- data/lib/inkcite/renderer/table.rb +1 -3
- data/lib/inkcite/renderer/table_base.rb +1 -1
- data/lib/inkcite/version.rb +1 -1
- data/lib/inkcite/view.rb +24 -9
- data/test/minifier_spec.rb +8 -0
- data/test/parser_spec.rb +9 -0
- data/test/project/config.yml +1 -1
- data/test/renderer/image_spec.rb +10 -2
- data/test/renderer/link_spec.rb +3 -1
- data/test/renderer/mobile_image_spec.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9bb7e08b66c836a2c76155628f8f95a6bc26fb8a
|
4
|
+
data.tar.gz: 4861793716a8cffefea911601e915c6d9faf6fdc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f0dee482e05a25f5a7be7acb9f10e21b05ee346c59ba4e5428fe78b36614da4e2c2883df544e85bf6b44f8d134c9a7f5831d9ea72f1f5a2001081aaedf2f75c3
|
7
|
+
data.tar.gz: 38084033ae6eb914af1da477c6ceb803bf6025d02d9ced8e55a983247b416526e231805a78ccc9a3669ba81c7f930454bb73bce68a4a254b4b62d733dc4fd1da
|
data/README.md
CHANGED
@@ -5,10 +5,12 @@ Like [Middleman] is to static web sites, Inkcite makes it easy for email
|
|
5
5
|
developers to keep their code DRY (don’t repeat yourself) and integrate
|
6
6
|
versioning, testing and minification into their workflow.
|
7
7
|
|
8
|
+
* Powerful media query and fluid-hybrid responsive support
|
8
9
|
* Easy, flexible templates, variables and Helpers
|
9
10
|
* ERB for dynamic content and easy A/B Testing and Versioning
|
10
11
|
* Automatic link tagging and tracking
|
11
|
-
*
|
12
|
+
* Instant compatibility testing with [Email on Acid] or [Litmus]
|
13
|
+
* Automatic [Litmus] Engagement analytics integration
|
12
14
|
* Email preview distribution lists
|
13
15
|
* Automatic image optimization using ImageOptim
|
14
16
|
* Failsafe rules to double-check your work
|
@@ -92,6 +94,9 @@ of other preflight features.
|
|
92
94
|
|
93
95
|
## Learn More
|
94
96
|
|
97
|
+
A step-by-step [tutorial] for building a modern, responsive email from start to
|
98
|
+
finish is available on the Inkceptional blog.
|
99
|
+
|
95
100
|
Documentation for Inkcite is generously hosted by the friendly folks at [Readme].
|
96
101
|
Get started here: https://inkcite.readme.io/
|
97
102
|
|
@@ -111,7 +116,9 @@ Copyright (c) 2014-2015 Jeffrey D. Hoffman. MIT Licensed, see [LICENSE] for
|
|
111
116
|
details.
|
112
117
|
|
113
118
|
[Middleman]: http://middlemanapp.com
|
119
|
+
[Email On Acid]: http://emailonacid.com
|
114
120
|
[Litmus]: http://litmus.com
|
115
121
|
[rubyinstaller]: http://rubyinstaller.org/
|
116
122
|
[LICENSE]: https://github.com/inkceptional/inkcite/blob/master/LICENSE
|
117
123
|
[Readme]: https://readme.io
|
124
|
+
[tutorial]: http://blog.inkceptional.com/build-a-modern-responsive-email-with-inkcite/
|
data/lib/inkcite/cli/base.rb
CHANGED
@@ -50,6 +50,15 @@ module Inkcite
|
|
50
50
|
Cli::Preview.invoke(email, to, options)
|
51
51
|
end
|
52
52
|
|
53
|
+
desc 'scope [options]', 'Share this email using Litmus Scope (https://litmus.com/scope/)'
|
54
|
+
option :version,
|
55
|
+
:aliases => '-v',
|
56
|
+
:desc => 'Scope a specific version of the email'
|
57
|
+
def scope
|
58
|
+
require_relative 'scope'
|
59
|
+
Cli::Scope.invoke(email, options)
|
60
|
+
end
|
61
|
+
|
53
62
|
desc 'server [options]', 'Start the preview server'
|
54
63
|
option :environment,
|
55
64
|
:aliases => '-e',
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'inkcite/mailer'
|
2
|
+
require 'json'
|
3
|
+
require 'uri'
|
4
|
+
require 'net/http'
|
5
|
+
require 'net/https'
|
6
|
+
|
7
|
+
module Inkcite
|
8
|
+
module Cli
|
9
|
+
class Scope
|
10
|
+
|
11
|
+
def self.invoke email, opts
|
12
|
+
|
13
|
+
# Push the browser preview(s) up to the server to ensure that the
|
14
|
+
# latest images and "view in browser" versions are available.
|
15
|
+
email.upload
|
16
|
+
|
17
|
+
puts "Scoping your email ..."
|
18
|
+
|
19
|
+
# Check to see if the Litmus section has been configured in the
|
20
|
+
# config.yml file - if so, we'll use their Litmus credentials
|
21
|
+
# so the email is associated with their account.
|
22
|
+
config = email.config[:litmus]
|
23
|
+
|
24
|
+
# True if the designer has a Litmus account. We'll use their
|
25
|
+
# username and password to authenticate the request in an effort
|
26
|
+
# to associate it with their Litmus account.
|
27
|
+
has_litmus = !config.blank?
|
28
|
+
if has_litmus
|
29
|
+
username = config[:username]
|
30
|
+
password = config[:password]
|
31
|
+
end
|
32
|
+
|
33
|
+
# Litmus Scope endpoint
|
34
|
+
uri = URI.parse('https://litmus.com/scope/api/v1/emails/')
|
35
|
+
https = Net::HTTP.new(uri.host, uri.port)
|
36
|
+
https.use_ssl = true
|
37
|
+
|
38
|
+
# Check to see if a specific version is requested or if unspecified
|
39
|
+
# all versions of the email should be sent.
|
40
|
+
versions = Array(opts[:version] || email.versions)
|
41
|
+
|
42
|
+
versions.each do |version|
|
43
|
+
|
44
|
+
# The version of the email we will be sending.
|
45
|
+
view = email.view(:preview, :email, version)
|
46
|
+
|
47
|
+
subject = view.subject
|
48
|
+
|
49
|
+
# Use Mail to assemble the SMTP-formatted content of the email
|
50
|
+
# but don't actually send the message. The to: and from:
|
51
|
+
# addresses do not need to be legitimate addresses.
|
52
|
+
mail = Mail.new do
|
53
|
+
from '"Inkcite" <inkcite@inkceptional.com>'
|
54
|
+
to '"Awesome Designer" <xxxxxxx@xxxxxxxxxxxx.xxx>'
|
55
|
+
subject subject
|
56
|
+
|
57
|
+
html_part do
|
58
|
+
content_type 'text/html; charset=UTF-8'
|
59
|
+
body view.render!
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Send an HTTPS post to Litmus with the encoded SMTP content
|
64
|
+
# produced by Mail.
|
65
|
+
scope_request = Net::HTTP::Post.new(uri.path)
|
66
|
+
scope_request.basic_auth(username, password) unless username.blank?
|
67
|
+
scope_request.set_form_data('email[source]' => mail.to_s)
|
68
|
+
|
69
|
+
begin
|
70
|
+
|
71
|
+
response = https.request(scope_request)
|
72
|
+
case response
|
73
|
+
when Net::HTTPSuccess
|
74
|
+
result = JSON.parse(response.body)
|
75
|
+
slug = result['email']['slug']
|
76
|
+
puts "'#{subject}' shared to https://litmus.com/scope/#{slug}"
|
77
|
+
|
78
|
+
when Net::HTTPUnauthorized
|
79
|
+
abort <<-ERROR.strip_heredoc
|
80
|
+
|
81
|
+
Oops! Inkcite wasn't able to scope your email because of an
|
82
|
+
authentication problem with Litmus. Please check the settings
|
83
|
+
in config.yml:
|
84
|
+
|
85
|
+
litmus:
|
86
|
+
username: '#{username}'
|
87
|
+
password: '#{password}'
|
88
|
+
|
89
|
+
ERROR
|
90
|
+
|
91
|
+
when Net::HTTPServerError
|
92
|
+
abort <<-ERROR.strip_heredoc
|
93
|
+
|
94
|
+
Oops! Inkcite wasn't able to scope your email because Litmus'
|
95
|
+
server returned an error. Please try again later.
|
96
|
+
|
97
|
+
#{response.message}
|
98
|
+
|
99
|
+
ERROR
|
100
|
+
else
|
101
|
+
raise response.message
|
102
|
+
end
|
103
|
+
|
104
|
+
rescue Exception => e
|
105
|
+
abort <<-ERROR.strip_heredoc
|
106
|
+
|
107
|
+
Oops! Inkcite wasn't able to scope your email because of an
|
108
|
+
unexpected error. Please try again later.
|
109
|
+
|
110
|
+
#{e.message}
|
111
|
+
|
112
|
+
ERROR
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
unless has_litmus
|
119
|
+
puts 'Note! Your scoped email will expire in 15 days.'
|
120
|
+
end
|
121
|
+
|
122
|
+
true
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/inkcite/minifier.rb
CHANGED
@@ -136,6 +136,10 @@ module Inkcite
|
|
136
136
|
minify?(ctx) ? js_compressor(ctx).compress(code) : code
|
137
137
|
end
|
138
138
|
|
139
|
+
def self.remove_comments html, ctx
|
140
|
+
minify?(ctx) ? html.gsub(HTML_COMMENT_REGEX, '') : html
|
141
|
+
end
|
142
|
+
|
139
143
|
private
|
140
144
|
|
141
145
|
# Name of the Image Optim configuration yml file that can be
|
@@ -143,7 +147,6 @@ module Inkcite
|
|
143
147
|
# optimization process.
|
144
148
|
IMAGE_OPTIM_CONFIG_YML = 'image_optim.yml'
|
145
149
|
|
146
|
-
|
147
150
|
NEW_LINE = "\n"
|
148
151
|
MAXIMUM_LINE_LENGTH = 800
|
149
152
|
|
@@ -151,6 +154,11 @@ module Inkcite
|
|
151
154
|
# the entire email.
|
152
155
|
INLINE_STYLE_REGEX = /style=\"([^\"]+)\"/
|
153
156
|
|
157
|
+
# Regex to match HTML comments when removal is necessary. The ? makes
|
158
|
+
# the regex ungreedy. The /m at the end ensures the regex matches
|
159
|
+
# multiple lines.
|
160
|
+
HTML_COMMENT_REGEX = /<!--(.*?)-->/m
|
161
|
+
|
154
162
|
def self.minify? ctx
|
155
163
|
ctx.is_enabled?(:minify)
|
156
164
|
end
|
data/lib/inkcite/parser.rb
CHANGED
@@ -81,6 +81,15 @@ module Inkcite
|
|
81
81
|
params[key] = value
|
82
82
|
value = ''
|
83
83
|
key = nil
|
84
|
+
|
85
|
+
# If a space is encountered but a value has been previously collected,
|
86
|
+
# a boolean attribute has been encountered - e.g. 'selected' or 'flush'.
|
87
|
+
# Use the value as the symbolized key and set the value to true.
|
88
|
+
elsif !value.blank?
|
89
|
+
|
90
|
+
params[value.to_sym] = true
|
91
|
+
value = ''
|
92
|
+
|
84
93
|
end
|
85
94
|
|
86
95
|
else
|
data/lib/inkcite/renderer.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require_relative 'renderer/base'
|
2
2
|
require_relative 'renderer/element'
|
3
3
|
require_relative 'renderer/responsive'
|
4
|
+
require_relative 'renderer/container_base'
|
4
5
|
require_relative 'renderer/image_base'
|
5
6
|
require_relative 'renderer/table_base'
|
6
7
|
|
@@ -44,8 +45,8 @@ module Inkcite
|
|
44
45
|
value.gsub!(/…/, '...')
|
45
46
|
|
46
47
|
else
|
47
|
-
value.gsub!(
|
48
|
-
value.gsub!(
|
48
|
+
value.gsub!(/–/, '–')
|
49
|
+
value.gsub!(/—/, '—')
|
49
50
|
value.gsub!(/™/, '™')
|
50
51
|
value.gsub!(/®/, '®')
|
51
52
|
value.gsub!(/[‘’`]/, '’')
|
@@ -33,6 +33,7 @@ module Inkcite
|
|
33
33
|
TEXT_SHADOW_BLUR = :'shadow-blur'
|
34
34
|
TEXT_SHADOW_OFFSET = :'shadow-offset'
|
35
35
|
VERTICAL_ALIGN = :'vertical-align'
|
36
|
+
WHITE_SPACE = :'white-space'
|
36
37
|
|
37
38
|
# CSS Margins
|
38
39
|
MARGINS = [MARGIN_TOP, MARGIN_LEFT, MARGIN_BOTTOM, MARGIN_RIGHT]
|
@@ -93,7 +94,7 @@ module Inkcite
|
|
93
94
|
|
94
95
|
# Sets the element's in-line bgcolor style if it has been defined
|
95
96
|
# in the provided options.
|
96
|
-
def mix_background element, opt
|
97
|
+
def mix_background element, opt, ctx
|
97
98
|
|
98
99
|
# Background color of the image, if populated.
|
99
100
|
bgcolor = detect_bgcolor(opt)
|
@@ -101,7 +102,7 @@ module Inkcite
|
|
101
102
|
|
102
103
|
end
|
103
104
|
|
104
|
-
def mix_border element, opt
|
105
|
+
def mix_border element, opt, ctx
|
105
106
|
|
106
107
|
border = opt[:border]
|
107
108
|
element.style[:border] = border unless border.blank?
|
@@ -116,6 +117,13 @@ module Inkcite
|
|
116
117
|
|
117
118
|
end
|
118
119
|
|
120
|
+
def mix_border_radius element, opt, ctx
|
121
|
+
|
122
|
+
border_radius = opt[BORDER_RADIUS].to_i
|
123
|
+
element.style[BORDER_RADIUS] = px(border_radius) if border_radius > 0
|
124
|
+
|
125
|
+
end
|
126
|
+
|
119
127
|
def mix_font element, opt, ctx, parent=nil
|
120
128
|
|
121
129
|
# Always ensure we have a parent to inherit from.
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Inkcite
|
2
|
+
module Renderer
|
3
|
+
class ContainerBase < Responsive
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def mix_all element, opt, ctx
|
8
|
+
|
9
|
+
mix_background element, opt, ctx
|
10
|
+
mix_border element, opt, ctx
|
11
|
+
mix_border_radius element, opt, ctx
|
12
|
+
mix_font element, opt, ctx
|
13
|
+
|
14
|
+
# Supports both integers and mixed padding (e.g. 10px 20px)
|
15
|
+
padding = opt[:padding]
|
16
|
+
element.style[:padding] = px(padding) unless none?(padding)
|
17
|
+
|
18
|
+
# Text alignment - left, right, center.
|
19
|
+
align = opt[:align]
|
20
|
+
element.style[TEXT_ALIGN] = align unless none?(align)
|
21
|
+
|
22
|
+
# Vertical alignment - top, middle, bottom.
|
23
|
+
valign = opt[:valign]
|
24
|
+
element.style[VERTICAL_ALIGN] = valign unless none?(valign)
|
25
|
+
|
26
|
+
display = opt[:display]
|
27
|
+
element.style[:display] = display unless display.blank?
|
28
|
+
|
29
|
+
mix_responsive element, opt, ctx
|
30
|
+
|
31
|
+
element.to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/inkcite/renderer/div.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Inkcite
|
2
2
|
module Renderer
|
3
|
-
class Div <
|
3
|
+
class Div < ContainerBase
|
4
4
|
|
5
5
|
def render tag, opt, ctx
|
6
6
|
|
@@ -14,22 +14,7 @@ module Inkcite
|
|
14
14
|
height = opt[:height].to_i
|
15
15
|
div.style[:height] = px(height) if height > 0
|
16
16
|
|
17
|
-
|
18
|
-
mix_font div, opt, ctx
|
19
|
-
|
20
|
-
# Text alignment - left, right, center.
|
21
|
-
align = opt[:align]
|
22
|
-
div.style[TEXT_ALIGN] = align unless none?(align)
|
23
|
-
|
24
|
-
valign = opt[:valign]
|
25
|
-
div.style[VERTICAL_ALIGN] = valign unless valign.blank?
|
26
|
-
|
27
|
-
display = opt[:display]
|
28
|
-
div.style[:display] = display unless display.blank?
|
29
|
-
|
30
|
-
mix_responsive div, opt, ctx
|
31
|
-
|
32
|
-
div.to_s
|
17
|
+
mix_all div, opt, ctx
|
33
18
|
end
|
34
19
|
|
35
20
|
end
|
@@ -7,14 +7,14 @@ module Inkcite
|
|
7
7
|
img = Element.new('img', { :border => 0 })
|
8
8
|
|
9
9
|
# Ensure that height and width are defined in the image's attributes.
|
10
|
-
mix_dimensions img, opt
|
10
|
+
mix_dimensions img, opt, ctx
|
11
11
|
|
12
12
|
# Get the fully-qualified URL to the image or placeholder image if it's
|
13
13
|
# missing from the images directory.
|
14
14
|
img[:src] = image_url(opt[:src], opt, ctx)
|
15
15
|
|
16
|
-
mix_background img, opt
|
17
|
-
mix_border img, opt
|
16
|
+
mix_background img, opt, ctx
|
17
|
+
mix_border img, opt, ctx
|
18
18
|
|
19
19
|
# Check to see if there is alt text specified for this image. We are
|
20
20
|
# testing against nil because sometimes the author desires an empty
|
@@ -40,6 +40,12 @@ module Inkcite
|
|
40
40
|
text_align = opt[TEXT_ALIGN]
|
41
41
|
img.style[TEXT_ALIGN] = text_align unless text_align.blank?
|
42
42
|
|
43
|
+
# Check to see if the alt text contains line breaks. If so, automatically add
|
44
|
+
# the white-space style set to 'pre' which forces the alt text to render with
|
45
|
+
# that line breaks visible.
|
46
|
+
# https://litmus.com/community/discussions/418-line-breaks-within-alt-text
|
47
|
+
img.style[WHITE_SPACE] = 'pre' if alt.match(/[\n\r\f]/)
|
48
|
+
|
43
49
|
end
|
44
50
|
|
45
51
|
end
|
@@ -42,22 +42,36 @@ module Inkcite
|
|
42
42
|
width = opt[:width]
|
43
43
|
height = opt[:height]
|
44
44
|
|
45
|
-
#
|
46
|
-
#
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
45
|
+
# Will hold the query parameters being passed to the placeholder service.
|
46
|
+
# I didn't name these parameters - they're from the imgix.net API.
|
47
|
+
query = {
|
48
|
+
:txtsize => 18,
|
49
|
+
:txttrack => 0,
|
50
|
+
:w => width,
|
51
|
+
:h => height,
|
52
|
+
:fm => :jpg,
|
53
|
+
}
|
54
54
|
|
55
55
|
# Check to see if the designer specified FPO text for this placeholder -
|
56
56
|
# otherwise default to the dimensions of the image.
|
57
57
|
fpo = opt[:fpo]
|
58
58
|
fpo = _src.dup if fpo.blank?
|
59
59
|
fpo << "\n(#{width}×#{height})"
|
60
|
-
|
60
|
+
query[:txt] = fpo
|
61
|
+
|
62
|
+
# Check to see if the image has a background color. If so, we'll use that
|
63
|
+
# to set the background color of the placeholder. We'll also pick a
|
64
|
+
# contrasting color for the foreground text.
|
65
|
+
bgcolor = detect_bgcolor(opt)
|
66
|
+
unless none?(bgcolor)
|
67
|
+
query[:bg] = bgcolor.gsub('#', '')
|
68
|
+
query[:txtclr] = Util::contrasting_text_color(bgcolor).gsub('#', '')
|
69
|
+
end
|
70
|
+
|
71
|
+
# Replace the missing image with an imgix.net-powered placeholder using
|
72
|
+
# the query parameters assembled above.
|
73
|
+
# e.g. https://placeholdit.imgix.net/~text?txtsize=18&txt=left.jpg%0A%28155%C3%97155%29&w=155&h=155&fm=jpg&txttrack=0
|
74
|
+
src = "//placeholdit.imgix.net/~text?#{query.to_query}"
|
61
75
|
|
62
76
|
end
|
63
77
|
|
@@ -78,7 +92,7 @@ module Inkcite
|
|
78
92
|
DIMENSIONS.any? { |dim| att[dim].to_i <= 0 }
|
79
93
|
end
|
80
94
|
|
81
|
-
def mix_dimensions img, opt
|
95
|
+
def mix_dimensions img, opt, ctx
|
82
96
|
DIMENSIONS.each { |dim| img[dim] = opt[dim].to_i }
|
83
97
|
end
|
84
98
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Inkcite
|
2
2
|
module Renderer
|
3
|
-
class Link <
|
3
|
+
class Link < ContainerBase
|
4
4
|
|
5
5
|
def render tag, opt, ctx
|
6
6
|
|
@@ -37,14 +37,20 @@ module Inkcite
|
|
37
37
|
|
38
38
|
a = Element.new('a')
|
39
39
|
|
40
|
-
|
40
|
+
# Mixes the attributes common to all container elements
|
41
|
+
# including font, background color, border, etc.
|
42
|
+
mix_all a, opt, ctx
|
41
43
|
|
42
44
|
id = opt[:id]
|
43
45
|
href = opt[:href]
|
44
46
|
|
45
47
|
# If a URL wasn't provided in the HTML, then check to see if there is
|
46
|
-
# a link declared in the project's links_tsv file.
|
47
|
-
|
48
|
+
# a link declared in the project's links_tsv file. If so, we need to
|
49
|
+
# duplicate it so that tagging doesn't get applied multiple times.
|
50
|
+
if href.blank?
|
51
|
+
links_tsv_href = ctx.links_tsv[id]
|
52
|
+
href = links_tsv_href.dup unless links_tsv_href.blank?
|
53
|
+
end
|
48
54
|
|
49
55
|
# True if the href is missing. If so, we may try to look it up by it's ID
|
50
56
|
# or we'll insert a default TBD link.
|
@@ -21,9 +21,9 @@ module Inkcite
|
|
21
21
|
# email is viewed on a mobile device.
|
22
22
|
img = Element.new('mobile-img')
|
23
23
|
|
24
|
-
mix_dimensions img, opt
|
24
|
+
mix_dimensions img, opt, ctx
|
25
25
|
|
26
|
-
mix_background img, opt
|
26
|
+
mix_background img, opt, ctx
|
27
27
|
|
28
28
|
display = opt[:display]
|
29
29
|
img.style[:display] = "#{display}" if display && display != BLOCK && display != DEFAULT
|
@@ -12,7 +12,7 @@ module Inkcite
|
|
12
12
|
# Verify the file exists and route it through ERB. Otherwise
|
13
13
|
# let the designer know that the file is missing.
|
14
14
|
if File.exist?(file)
|
15
|
-
ctx.
|
15
|
+
ctx.read_source(file)
|
16
16
|
|
17
17
|
else
|
18
18
|
ctx.error "Include not found", :file => file
|
@@ -33,7 +33,14 @@ module Inkcite
|
|
33
33
|
tag_stack = ctx.tag_stack(open_tag)
|
34
34
|
|
35
35
|
# The provided opts take precedence over the ones passed to the open tag.
|
36
|
-
|
36
|
+
if tag_stack
|
37
|
+
|
38
|
+
# Need to verify that there are open opts to pop - if a tag is closed that
|
39
|
+
# hasn't been opened (e.g. {h3}...{/h2}) the open_opt can be nil.
|
40
|
+
open_opt = tag_stack.pop
|
41
|
+
opt = open_opt.merge(opt) if open_opt
|
42
|
+
|
43
|
+
end
|
37
44
|
|
38
45
|
end
|
39
46
|
|
@@ -169,11 +169,11 @@ module Inkcite
|
|
169
169
|
|
170
170
|
button_styles[:border] = cfg.border unless cfg.border.blank?
|
171
171
|
button_styles[BORDER_BOTTOM] = cfg.border_bottom if cfg.bevel > 0
|
172
|
-
button_styles[BORDER_RADIUS] = Renderer.px(cfg.border_radius)
|
172
|
+
button_styles[BORDER_RADIUS] = Renderer.px(cfg.border_radius) unless cfg.border_radius.blank?
|
173
173
|
button_styles[FONT_WEIGHT] = cfg.font_weight unless cfg.font_weight.blank?
|
174
174
|
button_styles[:height] = Renderer.px(cfg.height) if cfg.height > 0
|
175
175
|
button_styles[MARGIN_TOP] = Renderer.px(cfg.margin_top) if cfg.margin_top > 0
|
176
|
-
button_styles[:padding] = Renderer.px(cfg.padding)
|
176
|
+
button_styles[:padding] = Renderer.px(cfg.padding) unless cfg.padding.blank?
|
177
177
|
button_styles[TEXT_ALIGN] = 'center'
|
178
178
|
|
179
179
|
styles << Rule.new('a', BUTTON, button_styles, false)
|
@@ -1,23 +1,13 @@
|
|
1
1
|
module Inkcite
|
2
2
|
module Renderer
|
3
|
-
class Span <
|
3
|
+
class Span < ContainerBase
|
4
4
|
|
5
5
|
def render tag, opt, ctx
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
padding = opt[:padding].to_i
|
12
|
-
span.style[:padding] = px(padding) if padding > 0
|
13
|
-
|
14
|
-
mix_font span, opt, ctx
|
15
|
-
|
16
|
-
mix_background span, opt
|
17
|
-
|
18
|
-
mix_responsive span, opt, ctx
|
19
|
-
|
20
|
-
span.to_s
|
6
|
+
if tag == '/span'
|
7
|
+
'</span>'
|
8
|
+
else
|
9
|
+
mix_all Element.new('span'), opt, ctx
|
10
|
+
end
|
21
11
|
end
|
22
12
|
|
23
13
|
end
|
@@ -69,13 +69,11 @@ module Inkcite
|
|
69
69
|
align = opt[:align] || opt[:float]
|
70
70
|
table[:align] = align unless align.blank?
|
71
71
|
|
72
|
-
|
73
|
-
table.style[BORDER_RADIUS] = px(border_radius) if border_radius > 0
|
72
|
+
mix_border_radius table, opt, ctx
|
74
73
|
|
75
74
|
border_collapse = opt[BORDER_COLLAPSE]
|
76
75
|
table.style[BORDER_COLLAPSE] = border_collapse unless border_collapse.blank?
|
77
76
|
|
78
|
-
|
79
77
|
# For both fluid and fluid-drop share certain setup which is performed here.
|
80
78
|
if is_fluid?(mobile)
|
81
79
|
|
data/lib/inkcite/version.rb
CHANGED
data/lib/inkcite/view.rb
CHANGED
@@ -333,11 +333,10 @@ module Inkcite
|
|
333
333
|
@environment == :production
|
334
334
|
end
|
335
335
|
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
source_file << (text?? TXT_EXTENSION : HTML_EXTENSION)
|
336
|
+
# Helper method which reads the designated file (e.g. source.html) and
|
337
|
+
# performs ERB on it, strips illegal characters and comments (if minified)
|
338
|
+
# and returns the filtered content.
|
339
|
+
def read_source source_file
|
341
340
|
|
342
341
|
# Will be used to assemble the parameters passed to File.open.
|
343
342
|
# First, always open the file in read mode.
|
@@ -346,19 +345,35 @@ module Inkcite
|
|
346
345
|
# Detect abnormal file encoding and construct the string to
|
347
346
|
# convert such encoding to UTF-8 if specified.
|
348
347
|
encoding = self[SOURCE_ENCODING]
|
349
|
-
|
348
|
+
unless encoding.blank? || encoding == UTF_8
|
350
349
|
mode << encoding
|
351
350
|
mode << UTF_8
|
352
351
|
end
|
353
352
|
|
354
353
|
# Read the original source which may include embedded Ruby.
|
355
|
-
source = File.open(
|
354
|
+
source = File.open(source_file, mode.join(':')).read
|
356
355
|
|
357
356
|
# Run the content through Erubis
|
358
|
-
|
357
|
+
source = self.eval_erb(source, source_file)
|
358
|
+
|
359
|
+
# If minification is enabled this will remove anything that has been
|
360
|
+
# <!-- commented out --> to ensure the email is as small as possible.
|
361
|
+
source = Minifier.remove_comments(source, self)
|
359
362
|
|
360
363
|
# Protect against unsupported characters
|
361
|
-
Renderer.fix_illegal_characters
|
364
|
+
source = Renderer.fix_illegal_characters(source, self)
|
365
|
+
|
366
|
+
source
|
367
|
+
end
|
368
|
+
|
369
|
+
def render!
|
370
|
+
raise "Already rendered" unless @content.blank?
|
371
|
+
|
372
|
+
source_file = 'source'
|
373
|
+
source_file << (text?? TXT_EXTENSION : HTML_EXTENSION)
|
374
|
+
|
375
|
+
# Read the original source which may include embedded Ruby.
|
376
|
+
filtered = read_source(@email.project_file(source_file))
|
362
377
|
|
363
378
|
# Filter each of the lines of text and push them onto the stack of lines
|
364
379
|
# that we be written into the text or html file.
|
data/test/minifier_spec.rb
CHANGED
@@ -17,4 +17,12 @@ describe Inkcite::View do
|
|
17
17
|
Inkcite::Minifier.html(["This string has trailing line-breaks.\n\r\f"], @view).must_equal('This string has trailing line-breaks.')
|
18
18
|
end
|
19
19
|
|
20
|
+
it "removes HTML comments" do
|
21
|
+
Inkcite::Minifier.remove_comments(%Q(I am <!-- This is an HTML comment -->not commented<!-- This is another comment --> out), @view).must_equal('I am not commented out')
|
22
|
+
end
|
23
|
+
|
24
|
+
it "removes multi-line HTML comments" do
|
25
|
+
Inkcite::Minifier.remove_comments(%Q(I am not <!-- This is a\n\nmulti-line HTML\ncomment -->commented out), @view).must_equal('I am not commented out')
|
26
|
+
end
|
27
|
+
|
20
28
|
end
|
data/test/parser_spec.rb
CHANGED
@@ -8,6 +8,15 @@ describe Inkcite::Parser do
|
|
8
8
|
Inkcite::Parser.parameters('border=1').must_equal({ :border => '1' })
|
9
9
|
end
|
10
10
|
|
11
|
+
it 'can resolve name-only boolean parameters' do
|
12
|
+
Inkcite::Parser.parameters('selected').must_equal({ :selected => true })
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'can resolve combinations of name=value and boolean parameters' do
|
16
|
+
Inkcite::Parser.parameters('border=1 selected').must_equal({ :border => '1', :selected => true })
|
17
|
+
Inkcite::Parser.parameters('selected border=1').must_equal({ :border => '1', :selected => true })
|
18
|
+
end
|
19
|
+
|
11
20
|
it 'can resolve parameters with dashes in the name' do
|
12
21
|
Inkcite::Parser.parameters('border-radius=5').must_equal({ :'border-radius' => '5' })
|
13
22
|
end
|
data/test/project/config.yml
CHANGED
@@ -91,7 +91,7 @@ preview:
|
|
91
91
|
# These overrides apply to the final, ready-to-send files.
|
92
92
|
production:
|
93
93
|
cache-bust: false
|
94
|
-
image-host: "http://production.imagehost.com/emails/myemail"
|
94
|
+
image-host: "http://production.imagehost.com/emails/myemail/"
|
95
95
|
|
96
96
|
email:
|
97
97
|
view-in-browser-url: 'http://production.contenthost.com/path/{filename}'
|
data/test/renderer/image_spec.rb
CHANGED
@@ -20,12 +20,12 @@ describe Inkcite::Renderer::Image do
|
|
20
20
|
|
21
21
|
it 'substitutes a placeholder for a missing image of sufficient size' do
|
22
22
|
@view.config[Inkcite::Email::IMAGE_PLACEHOLDERS] = true
|
23
|
-
Inkcite::Renderer.render('{img src=missing.jpg height=50 width=100}', @view).must_equal('<img border=0 height=50 src="
|
23
|
+
Inkcite::Renderer.render('{img src=missing.jpg height=50 width=100}', @view).must_equal('<img border=0 height=50 src="//placeholdit.imgix.net/~text?fm=jpg&h=50&txt=missing.jpg%0A%28100%C3%9750%29&txtsize=18&txttrack=0&w=100" style="display:block" width=100>')
|
24
24
|
end
|
25
25
|
|
26
26
|
it 'has configurable placeholder text' do
|
27
27
|
@view.config[Inkcite::Email::IMAGE_PLACEHOLDERS] = true
|
28
|
-
Inkcite::Renderer.render('{img src=missing.jpg height=50 width=100 fpo="F P O"}', @view).must_equal('<img border=0 height=50 src="
|
28
|
+
Inkcite::Renderer.render('{img src=missing.jpg height=50 width=100 fpo="F P O"}', @view).must_equal('<img border=0 height=50 src="//placeholdit.imgix.net/~text?fm=jpg&h=50&txt=F+P+O%0A%28100%C3%9750%29&txtsize=18&txttrack=0&w=100" style="display:block" width=100>')
|
29
29
|
end
|
30
30
|
|
31
31
|
it 'does not substitute placeholders for small images' do
|
@@ -87,4 +87,12 @@ describe Inkcite::Renderer::Image do
|
|
87
87
|
Inkcite::Renderer.render('{img src=inkcite.jpg height=200 width=325 mobile=fluid}', @view).must_equal('<img border=0 src="images/inkcite.jpg" style="display:block;height:auto;max-width:325px;width:100%" width=325>')
|
88
88
|
end
|
89
89
|
|
90
|
+
it 'supports multi-line alt text' do
|
91
|
+
Inkcite::Renderer.render(%Q({img src=inkcite.jpg height=150 width=100 font=none alt="Multiple\nLine\nAlt\nText"}), @view).must_equal(%Q(<img alt="Multiple\nLine\nAlt\nText" border=0 height=150 src="images/inkcite.jpg" style="display:block;white-space:pre" width=100>))
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'supports multi-line alt text in production, too' do
|
95
|
+
Inkcite::Renderer.render(%Q({img src=inkcite.jpg height=150 width=100 font=none alt="Multiple\nLine\nAlt\nText"}), Inkcite::Email.new('test/project/').view(:production, :email)).must_equal(%Q(<img alt="Multiple\nLine\nAlt\nText" border=0 height=150 src="http://production.imagehost.com/emails/myemail/inkcite.jpg" style="display:block;white-space:pre" width=100>))
|
96
|
+
end
|
97
|
+
|
90
98
|
end
|
data/test/renderer/link_spec.rb
CHANGED
@@ -36,7 +36,9 @@ describe Inkcite::Renderer::Link do
|
|
36
36
|
|
37
37
|
it 'tags a reused link once and only once' do
|
38
38
|
@view.config[:'tag-links'] = "tag=inkcite|{id}"
|
39
|
-
|
39
|
+
@view.links_tsv['litmus'] = 'http://litmus.com'
|
40
|
+
|
41
|
+
Inkcite::Renderer.render('{a id="litmus"}Test Emails Here{/a}{a id="litmus"}Also Here{/a}', @view).must_equal('<a href="http://litmus.com?tag=inkcite|litmus" style="color:#0099cc;text-decoration:none" target=_blank>Test Emails Here</a><a href="http://litmus.com?tag=inkcite|litmus" style="color:#0099cc;text-decoration:none" target=_blank>Also Here</a>')
|
40
42
|
|
41
43
|
end
|
42
44
|
|
@@ -17,7 +17,7 @@ describe Inkcite::Renderer::MobileImage do
|
|
17
17
|
it 'substitutes a placeholder for a missing image of sufficient size' do
|
18
18
|
@view.config[Inkcite::Email::IMAGE_PLACEHOLDERS] = true
|
19
19
|
Inkcite::Renderer.render('{mobile-img src=inkcite-mobile.jpg height=100 width=300}{/mobile-img}', @view).must_equal('<span class="i01 img"></span>')
|
20
|
-
@view.media_query.find_by_klass('i01').to_css.must_equal('span[class~="i01"] { background-image:url("
|
20
|
+
@view.media_query.find_by_klass('i01').to_css.must_equal('span[class~="i01"] { background-image:url("//placeholdit.imgix.net/~text?fm=jpg&h=100&txt=inkcite-mobile.jpg%0A%28300%C3%97100%29&txtsize=18&txttrack=0&w=300");height:100px;width:300px }')
|
21
21
|
end
|
22
22
|
|
23
23
|
it 'hides any images it wraps' do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: inkcite
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeffrey D. Hoffman
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-01-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -333,6 +333,7 @@ files:
|
|
333
333
|
- lib/inkcite/cli/build.rb
|
334
334
|
- lib/inkcite/cli/init.rb
|
335
335
|
- lib/inkcite/cli/preview.rb
|
336
|
+
- lib/inkcite/cli/scope.rb
|
336
337
|
- lib/inkcite/cli/server.rb
|
337
338
|
- lib/inkcite/cli/test.rb
|
338
339
|
- lib/inkcite/email.rb
|
@@ -342,6 +343,7 @@ files:
|
|
342
343
|
- lib/inkcite/renderer.rb
|
343
344
|
- lib/inkcite/renderer/base.rb
|
344
345
|
- lib/inkcite/renderer/button.rb
|
346
|
+
- lib/inkcite/renderer/container_base.rb
|
345
347
|
- lib/inkcite/renderer/div.rb
|
346
348
|
- lib/inkcite/renderer/element.rb
|
347
349
|
- lib/inkcite/renderer/footnote.rb
|