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 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