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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: aabad99d9252f3ff61ea16a6fcd79c145de6f399
4
- data.tar.gz: 0642dc04ebf7c0ca068bbe6bb782bb6cf1539e2c
3
+ metadata.gz: 9bb7e08b66c836a2c76155628f8f95a6bc26fb8a
4
+ data.tar.gz: 4861793716a8cffefea911601e915c6d9faf6fdc
5
5
  SHA512:
6
- metadata.gz: 0d7fd3bd14490e024b7e3a06e928c3fb796c66d47a7ada0ae4902ddff30f60cc444f178a535290ec51e32281891799eb6301c72e8f5677d066817c73f3f5279d
7
- data.tar.gz: 3975bf906623676bdbbebe8cccc763326e67739a959fdadf4e3f21a7c9222dfe3385cef18ce1fa67b044dad6dae9c14f9b3d44041dcfb1ac3210d0986eb6544a
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
- * [Litmus]-integrated compatibility testing and analytics
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/
@@ -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
@@ -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
@@ -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
@@ -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!(/[–—]/, '&#8211;')
48
- value.gsub!(/\-\-/, '&#8211;')
48
+ value.gsub!(/–/, '&ndash;')
49
+ value.gsub!(/—/, '&mdash;')
49
50
  value.gsub!(/™/, '&trade;')
50
51
  value.gsub!(/®/, '&reg;')
51
52
  value.gsub!(/[‘’`]/, '&#8217;')
@@ -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
@@ -1,6 +1,6 @@
1
1
  module Inkcite
2
2
  module Renderer
3
- class Div < Responsive
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
- mix_background div, opt
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
- # As a convenience, replace missing images with placehold.it as long as they
46
- # meet the minimum dimensions. No need to spam the design with tiny, tiny
47
- # placeholders.
48
- src = "http://placehold.it/#{width}x#{height}.jpg"
49
-
50
- # Check to see if the image has a background color. If so, we'll use that
51
- # to set the background color of the placeholder.
52
- bgcolor = detect_bgcolor(opt)
53
- src << "/#{bgcolor}".gsub('#', '') unless none?(bgcolor)
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
- src << "?text=#{URI::encode(fpo)}"
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 < Responsive
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
- mix_font a, opt, ctx
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
- href = ctx.links_tsv[id].dup if href.blank? && ctx.links_tsv[id]
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.eval_erb(File.open(file).read, file_name)
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
- opt = tag_stack.pop.merge(opt) if tag_stack
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) if cfg.border_radius > 0
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) if cfg.padding > 0
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 < Responsive
3
+ class Span < ContainerBase
4
4
 
5
5
  def render tag, opt, ctx
6
-
7
- return '</span>' if tag == '/span'
8
-
9
- span = Element.new('span')
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
- border_radius = opt[BORDER_RADIUS].to_i
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
 
@@ -13,7 +13,7 @@ module Inkcite
13
13
  def mix_all element, opt, ctx
14
14
 
15
15
  mix_background element, opt, ctx
16
- mix_border element, opt
16
+ mix_border element, opt, ctx
17
17
  mix_dimensions element, opt, ctx
18
18
 
19
19
  end
@@ -1,3 +1,3 @@
1
1
  module Inkcite
2
- VERSION = "1.9.1"
2
+ VERSION = "1.10.0"
3
3
  end
data/lib/inkcite/view.rb CHANGED
@@ -333,11 +333,10 @@ module Inkcite
333
333
  @environment == :production
334
334
  end
335
335
 
336
- def render!
337
- raise "Already rendered" unless @content.blank?
338
-
339
- source_file = 'source'
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
- if !encoding.blank? && encoding != UTF_8
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(@email.project_file(source_file), mode.join(':')).read
354
+ source = File.open(source_file, mode.join(':')).read
356
355
 
357
356
  # Run the content through Erubis
358
- filtered = self.eval_erb(source, source_file)
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 filtered, self
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.
@@ -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
@@ -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}'
@@ -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="http://placehold.it/100x50.jpg?text=missing.jpg%0A(100%C3%9750)" style="display:block" width=100>')
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="http://placehold.it/100x50.jpg?text=F%20P%20O%0A(100%C3%9750)" style="display:block" width=100>')
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
@@ -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
- Inkcite::Renderer.render('{a id="litmus" href="http://litmus.com"}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>')
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("http://placehold.it/300x100.jpg?text=inkcite-mobile.jpg%0A(300%C3%97100)");height:100px;width:300px }')
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.9.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: 2015-11-24 00:00:00.000000000 Z
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