inkcite 1.9.1 → 1.10.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/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
|