inkcite 1.7.0 → 1.8.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: 9d4ff329598b10063f608482495b1067089c890f
4
- data.tar.gz: 382ad87478df473ad76bda20a7fb306e125c65b5
3
+ metadata.gz: d4d3044cbc8db45ebd662ab451d759ef1f0aa25d
4
+ data.tar.gz: e87b26d745c307427fd53c7f1e55c19e242f3aaa
5
5
  SHA512:
6
- metadata.gz: 15a0cb8480b9e0abf92c31e2304b3f65baa64b450af06bd66061e3ca18f217645239bf5bedac1a74b0b5bea43a28e6a115de350c62ea30818e9a9b58bb0bddee
7
- data.tar.gz: 7313d13e0033675536c3255946bde8cd37c5eef54fbd50c9abe8787a97cf77f76fde38f7bd1e2090f967b8ba4f512da4daa16bd132db776cb8d566c2ed643932
6
+ metadata.gz: efb49479b68f7745f54902ea4d488c80532345baf772f74f6c939c22a77247c8f9635f4ac73cbf765cab9b3b96319c5af832f06f53c072e5ffd3541cda5a7460
7
+ data.tar.gz: 40b7fcc6f1bf820be7bfe5b890730a831b08f48fdd390061584c6808164b19998f1f038a1ad33740365f38689498e192d61f2f4d8719cb6280a235e9fd68b66a
data/README.md CHANGED
@@ -5,12 +5,13 @@ 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
- * Easy, flexible templates, variables and helpers
9
- * ERB for easy A/B testing and versioning
8
+ * Easy, flexible templates, variables and Helpers
9
+ * ERB for dynamic content and easy A/B Testing and Versioning
10
10
  * Automatic link tagging and tracking
11
- * [Litmus] integration for compatibility
12
- * Preview distribution lists
13
- * Content inclusion/exclusion rules
11
+ * [Litmus]-integrated compatibility testing and analytics
12
+ * Email preview distribution lists
13
+ * Automatic image optimization using ImageOptim
14
+ * Failsafe rules to double-check your work
14
15
 
15
16
  ## Installation
16
17
 
@@ -106,7 +107,7 @@ developer questions in a timely manner.
106
107
 
107
108
  ## License
108
109
 
109
- Copyright (c) 2014 Jeffrey D. Hoffman. MIT Licensed, see [LICENSE] for
110
+ Copyright (c) 2014-2015 Jeffrey D. Hoffman. MIT Licensed, see [LICENSE] for
110
111
  details.
111
112
 
112
113
  [Middleman]: http://middlemanapp.com
data/inkcite.gemspec CHANGED
@@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
29
29
  spec.add_dependency 'faker'
30
30
  spec.add_dependency 'guard'
31
31
  spec.add_dependency 'guard-livereload'
32
+ spec.add_dependency 'htmlbeautifier'
32
33
  spec.add_dependency 'image_optim'
33
34
  spec.add_dependency 'image_optim_pack'
34
35
  spec.add_dependency 'litmus'
@@ -9,6 +9,10 @@ module Inkcite
9
9
  option :archive,
10
10
  :aliases => '-a',
11
11
  :desc => 'The name of the archive to compress final assets into'
12
+ option :environment,
13
+ :aliases => '-e',
14
+ :desc => 'The environment (development, preview or production) to build',
15
+ :default => :production
12
16
  option :force,
13
17
  :aliases => '-f',
14
18
  :desc => 'Build even if there are errors (not recommended)',
@@ -16,10 +20,7 @@ module Inkcite
16
20
 
17
21
  def build
18
22
  require_relative 'build'
19
- Cli::Build.invoke(email, {
20
- :archive => options['archive'],
21
- :force => options['force']
22
- })
23
+ Cli::Build.invoke(email, options)
23
24
  end
24
25
 
25
26
  desc 'init NAME [options]', 'Initialize a new email project in the NAME directory'
@@ -7,24 +7,23 @@ module Inkcite
7
7
  errors = false
8
8
 
9
9
  # Don't allow production files to be built if there are errors.
10
- email.views(:production) do |ev|
10
+ email.views(opts[:environment]) do |ev|
11
11
 
12
12
  ev.render!
13
+ next if ev.errors.blank?
13
14
 
14
- if !ev.errors.blank?
15
- puts "The #{ev.version} version (#{ev.format}) has #{ev.errors.size} errors:"
16
- puts " - #{ev.errors.join("\n - ")}"
17
- errors = true
18
- end
15
+ puts "The #{ev.version} version (#{ev.format}) has #{ev.errors.size} errors:"
16
+ puts " - #{ev.errors.join("\n - ")}"
17
+ errors = true
19
18
 
20
19
  end
21
20
 
22
- abort("Fix errors or use --force to build") if errors && !opts[:force]
21
+ abort('Fix errors or use --force to build') if errors && !opts[:force]
23
22
 
24
23
  # First, compile all assets to the build directory.
25
24
  build_to_dir email, opts
26
25
 
27
- # No archive? Build to files instead.
26
+ # Compress the directory into an archive if so desired.
28
27
  archive = opts[:archive]
29
28
  build_archive(email, opts) unless archive.blank?
30
29
 
@@ -102,7 +101,7 @@ module Inkcite
102
101
  email.optimize_images
103
102
 
104
103
  # For each of the production views, build the HTML and links files.
105
- email.views(:production) do |ev|
104
+ email.views(opts[:environment]) do |ev|
106
105
 
107
106
  File.open(File.join(build_html_to, ev.file_name), 'w') { |f| ev.write(f) }
108
107
 
@@ -113,7 +112,8 @@ module Inkcite
113
112
 
114
113
  # Copy all of the source images into the build directory in preparation
115
114
  # for optimization
116
- FileUtils.cp_r(File.join(email.optimized_image_dir, '.'), build_images_to)
115
+ build_images_from = email.optimized_image_dir
116
+ FileUtils.cp_r(File.join(build_images_from, '.'), build_images_to) if File.exists?(build_images_from)
117
117
 
118
118
  end
119
119
 
@@ -19,7 +19,7 @@ module Inkcite
19
19
  is_empty = opts[:empty]
20
20
 
21
21
  # True if we're initializing a project from the built-in files.
22
- is_new = opts[:from].blank?
22
+ is_new = from_path.blank?
23
23
  if is_new
24
24
 
25
25
  # Use the default, bundled path if a from-path wasn't specified.
@@ -51,7 +51,7 @@ module Inkcite
51
51
 
52
52
  # If the example email is required, switch to the example root and
53
53
  # copy the files within over the existing files.
54
- unless is_empty
54
+ if is_new && !is_empty
55
55
  from_path = File.join(Inkcite.asset_path, 'example')
56
56
  FileUtils.cp_r(File.join(from_path, '.'), full_init_path)
57
57
  puts 'Copied example email files'
@@ -1,5 +1,6 @@
1
1
  require 'guard'
2
2
  require 'guard/commander'
3
+ require 'htmlbeautifier'
3
4
  require 'rack'
4
5
  require 'rack-livereload'
5
6
  require 'webrick'
@@ -123,6 +124,10 @@ module Inkcite
123
124
 
124
125
  html = view.render!
125
126
 
127
+ # If we're rendering the development version of the email, beautify the
128
+ # output so that
129
+ html = HtmlBeautifier.beautify(html) if view.development?
130
+
126
131
  unless view.errors.blank?
127
132
  error_count = view.errors.count
128
133
  puts "#{ts} #{error_count} error#{'s' if error_count > 1} or warning#{'s' if error_count > 1}:"
data/lib/inkcite/email.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  module Inkcite
2
2
  class Email
3
3
 
4
+ BROWSER_VERSION = :'browser-version'
4
5
  CACHE_BUST = :'cache-bust'
5
6
  IMAGE_HOST = :'image-host'
6
7
  IMAGE_PLACEHOLDERS = :'image-placeholders'
@@ -28,7 +29,11 @@ module Inkcite
28
29
 
29
30
  def formats env
30
31
 
31
- f = [ :email, :browser ]
32
+ # Inkcite is always capable of producing an email version of
33
+ # the project.
34
+ f = [ :email ]
35
+
36
+ f << :browser if config[BROWSER_VERSION] == true
32
37
 
33
38
  # Need to make sure a source.txt exists before we can include
34
39
  # it in the list of known formats.
@@ -61,7 +66,7 @@ module Inkcite
61
66
  end
62
67
 
63
68
  def optimize_images?
64
- config[Inkcite::Email::OPTIMIZE_IMAGES] == true
69
+ config[OPTIMIZE_IMAGES] == true
65
70
  end
66
71
 
67
72
  # Returns the directory that optimized, compressed images
@@ -102,7 +107,9 @@ module Inkcite
102
107
  version = (version || versions.first).to_sym
103
108
 
104
109
  raise "Unknown environment \"#{environment}\" - must be one of #{ENVIRONMENTS.join(',')}" unless ENVIRONMENTS.include?(environment)
105
- raise "Unknown format \"#{format}\" - must be one of #{FORMATS.join(',')}" unless FORMATS.include?(format)
110
+
111
+ _formats = formats(environment)
112
+ raise "Unknown format \"#{format}\" - must be one of #{_formats.join(',')}" unless _formats.include?(format)
106
113
  raise "Unknown version: \"#{version}\" - must be one of #{versions.join(',')}" unless versions.include?(version)
107
114
 
108
115
  # Instantiate a new view of this email with the desired view and
@@ -130,9 +137,6 @@ module Inkcite
130
137
 
131
138
  private
132
139
 
133
- # Allowed formats.
134
- FORMATS = [ :browser, :email, :text ].freeze
135
-
136
140
  # Name of the property controlling the meta data file name and
137
141
  # the default file name.
138
142
  META_FILE_NAME = :'meta-file'
@@ -24,6 +24,7 @@ module Inkcite
24
24
  MARGIN_LEFT = :'margin-left'
25
25
  MARGIN_RIGHT = :'margin-right'
26
26
  MARGIN_TOP = :'margin-top'
27
+ MAX_WIDTH = :'max-width'
27
28
  PADDING_X = :'padding-x'
28
29
  PADDING_Y = :'padding-y'
29
30
  TEXT_ALIGN = :'text-align'
@@ -78,6 +79,10 @@ module Inkcite
78
79
  Renderer.hex(color)
79
80
  end
80
81
 
82
+ def if_mso html
83
+ %Q(<!--[if mso]>#{html.to_s}<![endif]-->)
84
+ end
85
+
81
86
  def none? val
82
87
  val.blank? || val == NONE
83
88
  end
@@ -92,6 +97,21 @@ module Inkcite
92
97
 
93
98
  end
94
99
 
100
+ def mix_border element, opt
101
+
102
+ border = opt[:border]
103
+ element.style[:border] = border unless border.blank?
104
+
105
+ # Iterate through each of the possible borders and apply them individually
106
+ # to the style if they are defined.
107
+ DIRECTIONS.each do |dir|
108
+ key = :"border-#{dir}"
109
+ border = opt[key]
110
+ element.style[key] = border unless border.blank? || border == NONE
111
+ end
112
+
113
+ end
114
+
95
115
  def mix_font element, opt, ctx, parent=nil
96
116
 
97
117
  # Always ensure we have a parent to inherit from.
@@ -8,16 +8,24 @@ module Inkcite
8
8
 
9
9
  div = Element.new('div')
10
10
 
11
+ width = opt[:width]
12
+ div.style[:width] = px(width) unless width.blank?
13
+
11
14
  height = opt[:height].to_i
12
15
  div.style[:height] = px(height) if height > 0
13
16
 
17
+ mix_background div, opt
18
+ mix_font div, opt, ctx
19
+
14
20
  # Text alignment - left, right, center.
15
21
  align = opt[:align]
16
22
  div.style[TEXT_ALIGN] = align unless none?(align)
17
23
 
18
- mix_font div, opt, ctx
24
+ valign = opt[:valign]
25
+ div.style[VERTICAL_ALIGN] = valign unless valign.blank?
19
26
 
20
- mix_background div, opt
27
+ display = opt[:display]
28
+ div.style[:display] = display unless display.blank?
21
29
 
22
30
  mix_responsive div, opt, ctx
23
31
 
@@ -13,7 +13,19 @@ module Inkcite
13
13
  # True if the tag self-closes as in "<img .../>"
14
14
  @self_close = att.delete(:self_close) == true
15
15
 
16
+ # For caller convenience, accept a style hash from the attributes
17
+ # or initialize it here.
18
+ @styles = att.delete(:style) || {}
19
+
20
+ end
21
+
22
+ # I found myself doing a lot of Element.new('tag', { }).to_s + 'more html'
23
+ # so this method makes it easier by allowing elements to be added to
24
+ # strings.
25
+ def + html
26
+ to_s << html.to_s
16
27
  end
28
+ alias_method :concat, :+
17
29
 
18
30
  def [] key
19
31
  @att[key]
@@ -52,7 +64,7 @@ module Inkcite
52
64
  end
53
65
 
54
66
  def style
55
- @styles ||= {}
67
+ @styles
56
68
  end
57
69
 
58
70
  def to_s
@@ -21,6 +21,7 @@ module Inkcite
21
21
  # activate but those read from footnotes.tsv are inactive until
22
22
  # referenced in the source.
23
23
  attr_accessor :active
24
+ alias_method :active?, :active
24
25
 
25
26
  def initialize id, symbol, text, active=true
26
27
  @id = id
@@ -94,8 +95,8 @@ module Inkcite
94
95
  # Grab the last numeric footnote that was specified and, assuming
95
96
  # there is one, increment the count. Otherwise, start the count
96
97
  # off at one.
97
- last_instance = ctx.footnotes.select(&:numeric?).last
98
- instance.symbol = last_instance.nil? ? 1 : last_instance.symbol.to_i + 1
98
+ last_instance = ctx.footnotes.select { |fn| fn.numeric? && fn.active? }.collect(&:number).max.to_i
99
+ instance.symbol = last_instance + 1
99
100
 
100
101
  end
101
102
 
@@ -14,6 +14,7 @@ module Inkcite
14
14
  img[:src] = image_url(opt[:src], opt, ctx)
15
15
 
16
16
  mix_background img, opt
17
+ mix_border img, opt
17
18
 
18
19
  # Check to see if there is alt text specified for this image. We are
19
20
  # testing against nil because sometimes the author desires an empty
@@ -36,6 +37,9 @@ module Inkcite
36
37
 
37
38
  mix_font img, opt, ctx
38
39
 
40
+ text_align = opt[TEXT_ALIGN]
41
+ img.style[TEXT_ALIGN] = text_align unless text_align.blank?
42
+
39
43
  end
40
44
 
41
45
  end
@@ -75,9 +79,28 @@ module Inkcite
75
79
 
76
80
  mobile = opt[:mobile]
77
81
 
78
- # Check to see if this image is inside of a mobile-image declaration.
79
- # If so, the image defaults to hide on mobile.
80
- mobile = HIDE if mobile.blank? && !ctx.parent_opts(:mobile_image).blank?
82
+ # Fluid-Hybrid responsive images courtesy of @moonstrips and @CourtFantinato.
83
+ # http://webdesign.tutsplus.com/tutorials/creating-a-future-proof-responsive-email-without-media-queries--cms-23919#comment-2074740905
84
+ if mobile == FLUID
85
+
86
+ # Set the inline styles of the image to scale with aspect ratio
87
+ # intact up to the maximum width of the image itself.
88
+ img.style[MAX_WIDTH] = px(opt[:width])
89
+ img.style[:width] = '100%'
90
+ img.style[:height] = 'auto'
91
+
92
+ # Leave the explicit width attribute set (this prevents Outlook from
93
+ # blowing up) but clear the height attribute as Gmail images will not
94
+ # maintain aspect ratio if present.
95
+ img[:height] = nil
96
+
97
+ else
98
+
99
+ # Check to see if this image is inside of a mobile-image declaration.
100
+ # If so, the image defaults to hide on mobile.
101
+ mobile = HIDE if mobile.blank? && !ctx.parent_opts(:mobile_image).blank?
102
+
103
+ end
81
104
 
82
105
  mix_responsive img, opt, ctx, mobile
83
106
 
@@ -2,15 +2,17 @@ module Inkcite
2
2
  module Renderer
3
3
  class Responsive < Base
4
4
 
5
- BUTTON = 'button'
6
- DROP = 'drop'
7
- FILL = 'fill'
8
- HIDE = 'hide'
9
- IMAGE = 'img'
10
- SHOW = 'show'
11
- SWITCH = 'switch'
12
- SWITCH_UP = 'switch-up'
13
- TOGGLE = 'toggle'
5
+ BUTTON = 'button'
6
+ DROP = 'drop'
7
+ FILL = 'fill'
8
+ FLUID = 'fluid'
9
+ FLUID_DROP = 'fluid-drop'
10
+ HIDE = 'hide'
11
+ IMAGE = 'img'
12
+ SHOW = 'show'
13
+ SWITCH = 'switch'
14
+ SWITCH_UP = 'switch-up'
15
+ TOGGLE = 'toggle'
14
16
 
15
17
  # For elements that take on different background properties
16
18
  # when they go responsive
@@ -180,6 +182,12 @@ module Inkcite
180
182
 
181
183
  protected
182
184
 
185
+ # Returns true if the mobile klass provided matches any of the
186
+ # Fluid-Hybrid classes.
187
+ def is_fluid? mobile
188
+ mobile == FLUID || mobile == FLUID_DROP
189
+ end
190
+
183
191
  def mix_font element, opt, ctx, parent=nil
184
192
 
185
193
  # Let the super class do its thing and grab the name of the font
@@ -215,6 +223,11 @@ module Inkcite
215
223
  # Nothing to do if there is no class specified.s
216
224
  return nil if klass.blank?
217
225
 
226
+ # The Fluid-Hybrid klass is also ignored because it doesn't involve
227
+ # media queries - aborting early to avoid the "missing mobile class"
228
+ # warning normally generated.
229
+ return nil if is_fluid?(klass)
230
+
218
231
  mq = ctx.media_query
219
232
 
220
233
  # The element's tag - e.g. table, td, etc.
@@ -226,7 +239,7 @@ module Inkcite
226
239
 
227
240
  id = opt[:id]
228
241
  if id.blank?
229
- ctx.errors 'Mobile elements with toggle behavior require an ID attribute', { :tag => tag} if id.blank?
242
+ ctx.errors 'Mobile elements with toggle behavior require an ID attribute', { :tag => tag } if id.blank?
230
243
 
231
244
  else
232
245
 
@@ -4,60 +4,144 @@ module Inkcite
4
4
 
5
5
  def render tag, opt, ctx
6
6
 
7
+ html = ''
8
+
9
+ # We're either going to be pushing a newly opened table onto this stack
10
+ # or we're popping the open opts off of it.
7
11
  tag_stack = ctx.tag_stack(:table)
8
12
 
9
13
  if tag == CLOSE_TABLE
10
14
 
11
- # Remove this table from the stack of previously open tags.
12
- tag_stack.pop
15
+ # Pop the opts used when the table was originally opened. Then grab the
16
+ # mobile attribute. If this is a fluid table, we'll need to do close some
17
+ # of the additional tags injected when it was opened.
18
+ open_opt = tag_stack.pop
19
+ open_mobile = open_opt[:mobile]
13
20
 
14
- return '</tr></table>'
21
+ # If the table was declared as Fluid-Hybrid Drop, then there are some additional
22
+ # elements that need to be closed before the regular row-table closure that
23
+ # the Table helper normally produces.
24
+ if open_mobile == FLUID_DROP
15
25
 
16
- end
26
+ # Close the interior conditional table for Outlook that contains the floating blocks.
27
+ html << if_mso('</tr></table>')
17
28
 
18
- # Push this table onto the stack which will make it's declaration
19
- # available to its child TDs.
20
- tag_stack << opt
29
+ # Close what @campaignmonitor calls the "secret weapon" cell that typically aligns
30
+ # the text horizontally and vertically aligns the floating elements.
31
+ html << '</td>' # Styled
32
+ end
21
33
 
22
- table = Element.new(tag, { :border => 0, :cellspacing => 0 })
34
+ # Normal Inkcite Helper close HTML.
35
+ html << '</tr></table>'
23
36
 
24
- # Inherit base cell attributes - border, background color and image, etc.
25
- mix_all table, opt, ctx
37
+ # Close the conditional table for Output that contains the entire fluid layout.
38
+ html << if_mso('</td></tr></table>') if is_fluid?(open_mobile)
26
39
 
27
- # Text shadowing
28
- mix_text_shadow table, opt, ctx
40
+ else
29
41
 
30
- # Conveniently accept padding (easier to type and consistent with CSS)or
31
- # cellpadding which must always be declared.
32
- table[:cellpadding] = (opt[:padding] || opt[:cellpadding]).to_i
42
+ # Push this table onto the stack which will make it's declaration
43
+ # available to its child TDs.
44
+ tag_stack << opt
33
45
 
34
- # Conveniently accept both float and align to mean the same thing.
35
- align = opt[:align] || opt[:float]
36
- table[:align] = align unless align.blank?
46
+ table = Element.new(tag, { :border => 0, :cellspacing => 0 })
37
47
 
38
- border_radius = opt[BORDER_RADIUS].to_i
39
- table.style[BORDER_RADIUS] = px(border_radius) if border_radius > 0
48
+ # Grab the responsive mobile klass that is assigned to this table, if any.
49
+ mobile = opt[:mobile]
40
50
 
41
- border_collapse = opt[BORDER_COLLAPSE]
42
- table.style[BORDER_COLLAPSE] = border_collapse unless border_collapse.blank?
51
+ # Check if fluid-drop has been specified. This will force a lot more HTML to
52
+ # be produced for this table and its child TDs.
53
+ is_fluid_drop = mobile == FLUID_DROP
43
54
 
44
- # Apply margins.
45
- mix_margins table, opt, ctx
55
+ # Inherit base cell attributes - border, background color and image, etc.
56
+ mix_all table, opt, ctx
57
+ mix_margins table, opt, ctx
58
+ mix_text_shadow table, opt, ctx
46
59
 
47
- mobile = opt[:mobile]
60
+ # Conveniently accept padding (easier to type and consistent with CSS) or
61
+ # cellpadding which must always be declared.
62
+ #
63
+ # If Fluid-Drop is enabled, padding is always zero at this top-level table
64
+ # and will be applied in the TD renderer when it creates a new table to
65
+ # wrap itself in.
66
+ table[:cellpadding] = is_fluid_drop ? 0 : get_padding(opt)
48
67
 
49
- # When a Table is configured to have it's cells DROP then it
50
- # actually needs to FILL on mobile and it's child Tds will
51
- # be DROP'd. Override the local mobile klass so the child Tds
52
- # see the parent as DROP.
53
- mobile = FILL if mobile == DROP || mobile == SWITCH
68
+ # Conveniently accept both float and align to mean the same thing.
69
+ align = opt[:align] || opt[:float]
70
+ table[:align] = align unless align.blank?
54
71
 
55
- mix_responsive table, opt, ctx, mobile
72
+ border_radius = opt[BORDER_RADIUS].to_i
73
+ table.style[BORDER_RADIUS] = px(border_radius) if border_radius > 0
56
74
 
57
- table.to_s + '<tr>'
58
- end
75
+ border_collapse = opt[BORDER_COLLAPSE]
76
+ table.style[BORDER_COLLAPSE] = border_collapse unless border_collapse.blank?
77
+
78
+
79
+ # For both fluid and fluid-drop share certain setup which is performed here.
80
+ if is_fluid?(mobile)
81
+
82
+ # Width must always be specified on fluid tables, like it is for images.
83
+ # Warn the designer if a width has not been supplied in pixels.
84
+ width = opt[:width].to_i
85
+ ctx.error("Width is a required attribute when '#{mobile}' is applied to a {table}", opt) unless width > 0
86
+
87
+ # Fluid table method courtesy of @campaignmonitor - assign the max-width in
88
+ # pixels and set the normal width to 100%.
89
+ # https://www.campaignmonitor.com/blog/email-marketing/2014/07/creating-a-centred-responsive-design-without-media-queries/
90
+ table.style[MAX_WIDTH] = px(width)
91
+ table[:width] = '100%'
92
+
93
+ # Outlook (and Lotus Notes, if you can believe it) doesn't support max-width
94
+ # so we need to wrap the entire fluid table in a conditional table that
95
+ # ensures layout displays within the actual maximum pixels width.
96
+ html << if_mso(Element.new('table', {
97
+ :align => opt[:align], :border => 0, :cellspacing => 0, :cellpadding => 0, :width => opt[:width].to_i
98
+ }) + '<tr><td>')
99
+
100
+ elsif mobile == DROP || mobile == SWITCH
101
+
102
+ # When a Table is configured to have it's cells DROP then it
103
+ # actually needs to FILL on mobile and it's child Tds will
104
+ # be DROP'd. Override the local mobile klass so the child Tds
105
+ # see the parent as DROP.
106
+ mobile = FILL
107
+
108
+ end
59
109
 
60
- private
110
+ mix_responsive table, opt, ctx, mobile
111
+
112
+ html << table.to_s
113
+ html << '<tr>'
114
+
115
+ if is_fluid_drop
116
+
117
+ # Fluid-Drop tables need a default alignment specified which is inherited
118
+ # by the child TD elements if not otherwise specified.
119
+ #
120
+ # 11/8/2015: For reasons I don't understand, if the table is not valigned
121
+ # middle by default, then we lose the ability to valign-middle individual
122
+ # TD children. So, if we force 'middle' here, then the TDs can override
123
+ # with 'top' or 'bottom' alignment when desired.
124
+ valign = opt[:valign] ||= 'middle'
125
+
126
+ # According to @campaignmonitor this is the secret weapon of Fluid-Hyrbid
127
+ # drop which wraps the floating elements and centers them appropriately.
128
+ # https://www.campaignmonitor.com/blog/email-marketing/2014/07/creating-a-centred-responsive-design-without-media-queries/
129
+ #
130
+ # The zero-size font addresses a rendering problem in Outlook:
131
+ # https://css-tricks.com/fighting-the-space-between-inline-block-elements/
132
+ html << Element.new('td', :style => { TEXT_ALIGN => :center, VERTICAL_ALIGN => opt[:valign], FONT_SIZE => 0 }).to_s
133
+
134
+ # Lastly, Outlook needs yet another conditional table that will be used
135
+ # to contain the floating blocks. The TD elements are generated by
136
+ # each of the columns within this Fluid-Drop table.
137
+ html << if_mso(Element.new('table', :width => '100%', :align => :center, :cellpadding => 0, :cellspacing => 0, :border => 0) + '<tr>')
138
+
139
+ end
140
+
141
+ end
142
+
143
+ html
144
+ end
61
145
 
62
146
  CLOSE_TABLE = '/table'
63
147
 
@@ -4,10 +4,16 @@ module Inkcite
4
4
 
5
5
  protected
6
6
 
7
+ # Returns cellpadding specified from the provided attributes by
8
+ # Checking both :padding and :cellpadding.
9
+ def get_padding opt
10
+ (opt[:padding] || opt[:cellpadding]).to_i
11
+ end
12
+
7
13
  def mix_all element, opt, ctx
8
14
 
9
15
  mix_background element, opt, ctx
10
- mix_border element, opt, ctx
16
+ mix_border element, opt
11
17
  mix_dimensions element, opt, ctx
12
18
 
13
19
  end
@@ -67,21 +73,6 @@ module Inkcite
67
73
 
68
74
  end
69
75
 
70
- def mix_border element, opt, ctx
71
-
72
- border = opt[:border]
73
- element.style[:border] = border unless border.blank?
74
-
75
- # Iterate through each of the possible borders and apply them individually
76
- # to the style if they are defined.
77
- DIRECTIONS.each do |dir|
78
- key = :"border-#{dir}"
79
- border = opt[key]
80
- element.style[key] = border unless border.blank? || border == NONE
81
- end
82
-
83
- end
84
-
85
76
  def mix_dimensions element, opt, ctx
86
77
 
87
78
  # Not taking .to_i because we want to accept both integer values
@@ -4,79 +4,171 @@ module Inkcite
4
4
 
5
5
  def render tag, opt, ctx
6
6
 
7
+ html = ''
8
+
9
+ # Tracks the depth of currently open TD elements.
7
10
  tag_stack = ctx.tag_stack(:td)
8
11
 
12
+ # Grab the attributes of the parent table so that the TD can inherit
13
+ # specific values like padding, valign, responsiveness, etc.
14
+ table_opt = ctx.parent_opts(:table)
15
+
16
+ # Check to see if the parent table was set to fluid-drop which causes
17
+ # the table cells to be wrapped in <div> elements and floated to
18
+ # cause them to display more responsively on Android Mail and Gmail apps.
19
+ #
20
+ # Fluid-Hybrid TD courtesy of @moonstrips and our friends at Campaign Monitor
21
+ # https://www.campaignmonitor.com/blog/email-marketing/2014/07/creating-a-centred-responsive-design-without-media-queries/
22
+ is_fluid_drop = table_opt[:mobile] == FLUID_DROP
23
+
9
24
  if tag == CLOSE_TD
25
+
26
+ # Retrieve the opts that were used to open this TD. We'll need them to
27
+ # check for the private _fluid_drop attribute.
10
28
  tag_stack.pop
11
- return '</td>'
12
- end
13
29
 
14
- # Push this tag onto the stack so that child elements (e.g. links)
15
- # can have access to its attributes.
16
- tag_stack << opt
30
+ # Normal HTML produced by the Helper to close the cell.
31
+ html << '</td>'
17
32
 
18
- # Grab the attributes of the parent table so that the TD can inherit
19
- # specific values like padding, valign, responsiveness, etc.
20
- parent = ctx.parent_opts(:table)
21
-
22
- td = Element.new('td')
23
-
24
- # Inherit base cell attributes - border, background color and image, etc.
25
- mix_all td, opt, ctx
26
-
27
- # Force the td to collapse to a single pixel to support images that
28
- # are less than 15 pixels.
29
- opt.merge!({
30
- :font => NONE,
31
- :color => NONE,
32
- FONT_SIZE => 1,
33
- LINE_HEIGHT => 1
34
- }) unless opt[:flush].blank?
35
-
36
- # It is a best-practice to declare the same padding on all cells in a
37
- # table. Check to see if padding was declared on the parent.
38
- padding = parent[:padding].to_i
39
- td.style[:padding] = px(padding) if padding > 0
40
-
41
- # Custom handling for text align on TDs rather than Base's mix_text_align
42
- # because if possible, using align= rather than a style keeps emails
43
- # smaller. But for left-aligned text, you gotta use a style because
44
- # you know, Outlook.
45
- align = opt[:align]
46
- unless align.blank?
47
- td[:align] = align
48
-
49
- # Must use style to reinforce left-align text in certain email clients.
50
- # All other alignments are accepted naturally.
51
- td.style[TEXT_ALIGN] = align if align == LEFT
33
+ # If the td was originally opened with fluid-drop, we need to do a fair
34
+ # bit of cleanup...
35
+ if is_fluid_drop
52
36
 
53
- end
37
+ # Close the
38
+ html << '</tr></table>'
54
39
 
55
- valign = detect(opt[:valign], parent[:valign])
56
- td[:valign] = valign unless valign.blank?
40
+ # Close the floating, responsive div.
41
+ html << '{/div}'
57
42
 
58
- rowspan = opt[:rowspan].to_i
59
- td[:rowspan] = rowspan if rowspan > 0
43
+ # Close the conditional cell
44
+ html << if_mso('</td>')
60
45
 
61
- mix_font td, opt, ctx, parent
46
+ end
62
47
 
63
- mobile = opt[:mobile]
64
- if mobile.blank?
48
+ else
65
49
 
66
- # If the cell doesn't define it's own responsive behavior, check to
67
- # see if it inherits from its parent table. DROP and SWITCH declared
68
- # at the table-level descend to their tds.
69
- pm = parent[:mobile]
70
- mobile = pm if pm == DROP || pm == SWITCH
50
+ # Push this tag onto the stack so that child elements (e.g. links)
51
+ # can have access to its attributes.
52
+ tag_stack << opt
71
53
 
72
- end
54
+ td = Element.new('td')
55
+
56
+ # Check to see if a width has been specified for this element. The
57
+ # width is critical to Fluid-Hybrid drop.
58
+ width = opt[:width].to_i
59
+
60
+ # Check for vertical alignment applied to either the TD or to the
61
+ # parent Table.
62
+ valign = detect(opt[:valign], table_opt[:valign])
63
+ td[:valign] = valign unless valign.blank?
64
+
65
+ # It is a best-practice to declare the same padding on all cells in a
66
+ # table. Check to see if padding was declared on the parent.
67
+ padding = get_padding(table_opt)
68
+ td.style[:padding] = px(padding) if padding > 0
69
+
70
+ mobile = opt[:mobile]
71
+
72
+ # Need to handle Fluid-Drop HTML injection here before the rest of the
73
+ # TD is formalized. Fluid-Drop removes the width attribute of the cell
74
+ # as it is wrapped in a 100%-width table.
75
+ if is_fluid_drop
76
+
77
+ # Width must be specified for Fluid-Drop cells. Vertical-alignment is
78
+ # also important but should have been preset by the Table Helper if it
79
+ # was omitted by the designer.
80
+ ctx.error("Width is a required attribute when #{FLUID_DROP} is specified", opt) unless width > 0
81
+ ctx.error("Vertical alignment should be specified when #{FLUID_DROP} is specified", opt) if valign.blank?
82
+
83
+ # Conditional Outlook cell to prevent the 100%-wide table within from
84
+ # stretching beyond the max-width. Also, valign necessary to get float
85
+ # elements to align properly.
86
+ html << if_mso(Element.new('td', :width => width, :valign => valign))
87
+
88
+ # Per @campaignmonitor, the secret to the Fluid-Drop trick is to wrap the
89
+ # floating table in a div with "display: inline-block" - which means that
90
+ # they'll obey the text-align property on the parent cell (text-align affects
91
+ # all inline or inline-block elements in a container).
92
+ # https://www.campaignmonitor.com/blog/email-marketing/2014/07/creating-a-centred-responsive-design-without-media-queries/
93
+
94
+ div_mobile = mobile == HIDE ? HIDE : FILL
95
+ html << %Q({div width=#{width} display=inline-block valign=#{valign} mobile="#{div_mobile}"})
73
96
 
74
- mix_responsive td, opt, ctx, mobile
97
+ # One last wrapper table within the div. This 100%-wide table is also where any
98
+ # padding applied to the elements belongs.
99
+ html << Element.new('table', :cellpadding => padding, :cellspacing => 0, :border => 0, :width => '100%').to_s
100
+ html << '<tr>'
75
101
 
76
- #outlook-bg <!-&#45;&#91;if gte mso 9]>[n]<v:rect style="width:%width%px;height:%height%px;" strokecolor="none"><v:fill type="tile" src="%src%" /></v:fill></v:rect><v:shape id="theText[rnd]" style="position:absolute;width:%width%px;height:%height%px;margin:0;padding:0;%style%">[n]<!&#91;endif]&#45;->
77
- #/outlook-bg <!-&#45;&#91;if gte mso 9]></v:shape><!&#91;endif]&#45;->
102
+ # Remove the width attribute from the TDs declaration.
103
+ opt.delete(:width)
104
+
105
+ # The TD nested within the floating div and additional table will inherit center-aligned
106
+ # text which means fluid-drop cells would have a default layout inconsistent with a regular
107
+ # TD - which will typically be left-aligned. So, unless otherwise specified, presume that
108
+ # the TD should have left-aligned text.
109
+ opt[:align] = 'left' if opt[:align].blank?
110
+
111
+ mobile = ''
112
+
113
+ end
114
+
115
+ # Inherit base cell attributes - border, background color and image, etc.
116
+ mix_all td, opt, ctx
117
+
118
+ # Force the td to collapse to a single pixel to support images that
119
+ # are less than 15 pixels.
120
+ opt.merge!({
121
+ :font => NONE,
122
+ :color => NONE,
123
+ FONT_SIZE => 1,
124
+ LINE_HEIGHT => 1
125
+ }) unless opt[:flush].blank?
126
+
127
+ # Custom handling for text align on TDs rather than Base's mix_text_align
128
+ # because if possible, using align= rather than a style keeps emails
129
+ # smaller. But for left-aligned text, you gotta use a style because
130
+ # you know, Outlook.
131
+ align = opt[:align]
132
+ unless align.blank?
133
+ td[:align] = align
134
+
135
+ # Must use style to reinforce left-align text in certain email clients.
136
+ # All other alignments are accepted naturally.
137
+ td.style[TEXT_ALIGN] = align if align == LEFT
138
+
139
+ end
140
+
141
+ rowspan = opt[:rowspan].to_i
142
+ td[:rowspan] = rowspan if rowspan > 0
143
+
144
+ mix_font td, opt, ctx, table_opt
145
+
146
+ # In Fluid-Drop, the font-size is set to zero to overcome Outlook rendering
147
+ # problems so it is important to warn the designer that they need to set
148
+ # it back to a reasonable size on the TD element.
149
+ # TODO [JDH 11/14/2015] Decide if the warning re: font-size should ever
150
+ # be restored based on whether or not users are finding it confusing.
151
+
152
+ if mobile.blank?
153
+
154
+ # If the cell doesn't define it's own responsive behavior, check to
155
+ # see if it inherits from its parent table. DROP and SWITCH declared
156
+ # at the table-level descend to their tds.
157
+ pm = table_opt[:mobile]
158
+ mobile = pm if pm == DROP || pm == SWITCH
159
+
160
+ end
161
+
162
+ mix_responsive td, opt, ctx, mobile
163
+
164
+ #outlook-bg <!-&#45;&#91;if gte mso 9]>[n]<v:rect style="width:%width%px;height:%height%px;" strokecolor="none"><v:fill type="tile" src="%src%" /></v:fill></v:rect><v:shape id="theText[rnd]" style="position:absolute;width:%width%px;height:%height%px;margin:0;padding:0;%style%">[n]<!&#91;endif]&#45;->
165
+ #/outlook-bg <!-&#45;&#91;if gte mso 9]></v:shape><!&#91;endif]&#45;->
166
+
167
+ html << td.to_s
168
+
169
+ end
78
170
 
79
- td.to_s
171
+ html
80
172
  end
81
173
 
82
174
  private
@@ -8,7 +8,7 @@ module Inkcite
8
8
 
9
9
  times = []
10
10
 
11
- [ 'source.html', 'source.txt', 'helpers.tsv' ].each do |file|
11
+ ['source.html', 'source.txt', 'helpers.tsv'].each do |file|
12
12
  file = email.project_file(file)
13
13
  times << File.mtime(file).to_i if File.exists?(file)
14
14
  end
@@ -81,8 +81,8 @@ module Inkcite
81
81
  end
82
82
 
83
83
  # TODO: Verify SFTP configuration
84
- host = config[:host]
85
- path = config[:path]
84
+ host = config[:host]
85
+ path = config[:path]
86
86
  username = config[:username]
87
87
  password = config[:password]
88
88
 
@@ -107,7 +107,7 @@ module Inkcite
107
107
  # Upload each version of the email.
108
108
  email.versions.each do |version|
109
109
 
110
- view = email.view(:preview, :browser, version)
110
+ view = email.view(:preview, :email, version)
111
111
 
112
112
  # Need to pass the upload path through the renderer to ensure
113
113
  # that embedded tags will be converted into data.
@@ -117,28 +117,34 @@ module Inkcite
117
117
  # the content and images is present.
118
118
  mkdir! sftp, remote_root
119
119
 
120
+ # Upload the images to the remote directory. We use the last_remote_root
121
+ # to ensure that we're not repeatedly uploading the same images over and
122
+ # over when force is enabled -- but will re-upload images to distinct
123
+ # remote roots.
124
+ copy! sftp, local_images, remote_root, force && last_remote_root != remote_root
125
+ last_remote_root = remote_root
126
+
127
+ # Check to see if we're creating an in-browser version of the email.
128
+ next unless email.formats.include?(:browser)
129
+
130
+ browser_view = email.view(:preview, :browser, version)
131
+
120
132
  # Check to see if there is a HTML version of this preview. Some emails
121
133
  # do not have a hosted version and so it is not necessary to upload the
122
134
  # HTML version of the email - but this is a bad practice.
123
135
  file_name = view.file_name
124
- unless file_name.blank?
125
-
126
- remote_file_name = File.join(remote_root, file_name)
127
- puts "Uploading #{remote_file_name}"
128
-
129
- # We need to use StringIO to write the email to a buffer in order to upload
130
- # the email's content in binary so that its encoding is honored. SFTP defaults
131
- # to ASCII-8bit in non-binary mode, so it was blowing up on UTF-8 special
132
- # characters (e.g. "Mäkinen").
133
- # http://stackoverflow.com/questions/9439289/netsftp-transfer-mode-binary-vs-text
134
- io = StringIO.new(view.render!)
135
- sftp.upload!(io, remote_file_name)
136
-
137
- end
138
-
139
- # Upload the images to the remote directory
140
- copy! sftp, local_images, remote_root, force && last_remote_root != remote_root
141
- last_remote_root = remote_root
136
+ next if file_name.blank?
137
+
138
+ remote_file_name = File.join(remote_root, file_name)
139
+ puts "Uploading #{remote_file_name}"
140
+
141
+ # We need to use StringIO to write the email to a buffer in order to upload
142
+ # the email's content in binary so that its encoding is honored. SFTP defaults
143
+ # to ASCII-8bit in non-binary mode, so it was blowing up on UTF-8 special
144
+ # characters (e.g. "Mäkinen").
145
+ # http://stackoverflow.com/questions/9439289/netsftp-transfer-mode-binary-vs-text
146
+ io = StringIO.new(browser_view.render!)
147
+ sftp.upload!(io, remote_file_name)
142
148
 
143
149
  end
144
150
 
@@ -1,3 +1,3 @@
1
1
  module Inkcite
2
- VERSION = "1.7.0"
2
+ VERSION = "1.8.0"
3
3
  end
data/lib/inkcite/view.rb CHANGED
@@ -61,8 +61,15 @@ module Inkcite
61
61
  @config[FILE_NAME] = file_name
62
62
 
63
63
  # The MediaQuery object manages the responsive styles that are applied to
64
- # the email during rendering.
65
- @media_query = MediaQuery.new(self, 480)
64
+ # the email during rendering. Check to see if a breakwidth has been supplied
65
+ # in helpers.tsv so the designer can control the primary breakpoint.
66
+ breakpoint = @config[:'mobile-breakpoint'].to_i
67
+ if breakpoint <= 0
68
+ breakpoint = @config[:width].to_i - 1
69
+ breakpoint = 480 if breakpoint <= 0
70
+ end
71
+
72
+ @media_query = MediaQuery.new(self, breakpoint)
66
73
 
67
74
  # Set the version index based on the position of this
68
75
  # version in the list of those defined.
@@ -218,7 +225,11 @@ module Inkcite
218
225
  image_host = if development?
219
226
  (@email.optimize_images?? Minifier::IMAGE_CACHE : Email::IMAGES) + '/'
220
227
  else
221
- self[Email::IMAGE_HOST]
228
+
229
+ # Use the image host defined in config.yml or, out-of-the-box refer to images/
230
+ # in the build directory.
231
+ self[Email::IMAGE_HOST] || (Email::IMAGES + '/')
232
+
222
233
  end
223
234
 
224
235
  src_url << image_host unless image_host.blank?
@@ -227,7 +238,7 @@ module Inkcite
227
238
  src_url << src
228
239
 
229
240
  # Cache-bust the image if the caller is expecting it to be there.
230
- src_url << "?#{Time.now.to_i}" if is_enabled?(Email::CACHE_BUST)
241
+ src_url << "?#{Time.now.to_i}" if !production? && is_enabled?(Email::CACHE_BUST)
231
242
 
232
243
  # Transpose any embedded tags into actual values.
233
244
  Renderer.render(src_url, self)
@@ -361,7 +372,15 @@ module Inkcite
361
372
  html << '<meta name="viewport" content="width=device-width"/>'
362
373
  html << "<meta name=\"generator\" content=\"Inkcite #{Inkcite::VERSION}\"/>"
363
374
 
364
- html << "<title>#{self.title}</title>"
375
+ # Enable responsive media queries on Windows phones courtesy of @jamesmacwhite
376
+ # https://blog.jmwhite.co.uk/2014/03/01/windows-phone-does-support-css3-media-queries-in-html-email/
377
+ html << '<!--[if !mso]><!-->'
378
+ html << '<meta http-equiv="X-UA-Compatible" content="IE=edge" />'
379
+ html << '<!--<![endif]-->'
380
+
381
+ # Some native Android clients display the title before the preheader so
382
+ # don't include it in non-development or email rendering per @moonstrips
383
+ html << "<title>#{self.title if (development? || browser?)}</title>"
365
384
 
366
385
  # Add external script sources.
367
386
  html += external_scripts
@@ -79,4 +79,8 @@ describe Inkcite::Renderer::Image do
79
79
  @view.media_query.find_by_klass('i01').to_css.must_equal('img[class~="i01"] { content: url("images/inkcite-mobile.jpg") !important; }')
80
80
  end
81
81
 
82
+ it 'supports fluid-hybrid desktop and style' do
83
+ 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>')
84
+ end
85
+
82
86
  end
@@ -31,4 +31,24 @@ describe Inkcite::Renderer::Table do
31
31
  Inkcite::Renderer.render('{table margin=15 margin-left=8}', @view).must_equal('<table border=0 cellpadding=0 cellspacing=0 style="margin-bottom:15px;margin-left:8px;margin-right:15px;margin-top:15px"><tr>')
32
32
  end
33
33
 
34
+ it 'supports fluid-hybrid desktop and style' do
35
+ Inkcite::Renderer.render('{table width=500 mobile=fluid}', @view).must_equal(%Q(<!--[if mso]><table border=0 cellpadding=0 cellspacing=0 width=500><tr><td><![endif]--><table border=0 cellpadding=0 cellspacing=0 style="max-width:500px" width=100%><tr>))
36
+ end
37
+
38
+ it 'carries table alignment into the Outlook wrap table in fluid-hybrid' do
39
+ Inkcite::Renderer.render('{table align=center width=500 mobile=fluid}', @view).must_equal(%Q(<!--[if mso]><table align=center border=0 cellpadding=0 cellspacing=0 width=500><tr><td><![endif]--><table align=center border=0 cellpadding=0 cellspacing=0 style="max-width:500px" width=100%><tr>))
40
+ end
41
+
42
+ it 'supports fluid-drop desktop and style' do
43
+
44
+ markup = ''
45
+ markup << %Q({table font-size=25 bgcolor=#090 border="5px solid #f0f" padding=15 width=600 mobile="fluid-drop"})
46
+ markup << %Q({td width=195 bgcolor=#009 color=#fff valign=top}left{/td})
47
+ markup << %Q({td width=195 align=center}centered two-line{/td})
48
+ markup << %Q({td width=195 bgcolor=#900 color=#fff align=right font-size=30}right<br>three<br>lines{/td})
49
+ markup << %Q({/table})
50
+
51
+ Inkcite::Renderer.render(markup, @view).must_equal(%Q(<!--[if mso]><table border=0 cellpadding=0 cellspacing=0 width=600><tr><td><![endif]--><table bgcolor=#009900 border=0 cellpadding=0 cellspacing=0 style="border:5px solid #f0f;max-width:600px" width=100%><tr><td style="font-size:0;text-align:center;vertical-align:middle"><!--[if mso]><table align=center border=0 cellpadding=0 cellspacing=0 width=100%><tr><![endif]--><!--[if mso]><td valign=top width=195><![endif]--><div class="fill" style="display:inline-block;vertical-align:top;width:195px"><table border=0 cellpadding=15 cellspacing=0 width=100%><tr><td align=left bgcolor=#000099 style="color:#ffffff;font-size:25px;padding:15px;text-align:left" valign=top>left</td></tr></table></div><!--[if mso]></td><![endif]--><!--[if mso]><td valign=middle width=195><![endif]--><div class="fill" style="display:inline-block;vertical-align:middle;width:195px"><table border=0 cellpadding=15 cellspacing=0 width=100%><tr><td align=center style="font-size:25px;padding:15px" valign=middle>centered two-line</td></tr></table></div><!--[if mso]></td><![endif]--><!--[if mso]><td valign=middle width=195><![endif]--><div class="fill" style="display:inline-block;vertical-align:middle;width:195px"><table border=0 cellpadding=15 cellspacing=0 width=100%><tr><td align=right bgcolor=#990000 style="color:#ffffff;font-size:30px;padding:15px" valign=middle>right<br>three<br>lines</td></tr></table></div><!--[if mso]></td><![endif]--><!--[if mso]></tr></table><![endif]--></td></tr></table><!--[if mso]></td></tr></table><![endif]-->))
52
+ end
53
+
34
54
  end
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.7.0
4
+ version: 1.8.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-04 00:00:00.000000000 Z
11
+ date: 2015-11-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - '>='
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: htmlbeautifier
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: image_optim
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -313,9 +327,6 @@ files:
313
327
  - assets/init/image_optim.yml
314
328
  - assets/init/source.html
315
329
  - bin/inkcite
316
- - bin/release-major
317
- - bin/release-minor
318
- - bin/release-patch
319
330
  - inkcite.gemspec
320
331
  - lib/inkcite.rb
321
332
  - lib/inkcite/cli/base.rb
data/bin/release-major DELETED
@@ -1 +0,0 @@
1
- gem bump --version major --push --release
data/bin/release-minor DELETED
@@ -1 +0,0 @@
1
- gem bump --version minor --push --release
data/bin/release-patch DELETED
File without changes