inkcite 1.15.0 → 1.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -1
  3. data/inkcite.gemspec +0 -3
  4. data/lib/inkcite.rb +0 -1
  5. data/lib/inkcite/cli/base.rb +12 -8
  6. data/lib/inkcite/cli/preview.rb +0 -10
  7. data/lib/inkcite/cli/server.rb +9 -1
  8. data/lib/inkcite/email.rb +5 -10
  9. data/lib/inkcite/facade/animation.rb +4 -1
  10. data/lib/inkcite/image_minifier.rb +96 -0
  11. data/lib/inkcite/mailer.rb +2 -2
  12. data/lib/inkcite/minifier.rb +1 -1
  13. data/lib/inkcite/renderer.rb +10 -0
  14. data/lib/inkcite/renderer/base.rb +47 -2
  15. data/lib/inkcite/renderer/button.rb +15 -5
  16. data/lib/inkcite/renderer/container_base.rb +15 -1
  17. data/lib/inkcite/renderer/image.rb +1 -1
  18. data/lib/inkcite/renderer/image_base.rb +8 -0
  19. data/lib/inkcite/renderer/list.rb +104 -0
  20. data/lib/inkcite/renderer/mobile_image.rb +3 -0
  21. data/lib/inkcite/renderer/responsive.rb +2 -0
  22. data/lib/inkcite/renderer/slant.rb +207 -0
  23. data/lib/inkcite/renderer/snow.rb +15 -1
  24. data/lib/inkcite/renderer/special_effect.rb +7 -0
  25. data/lib/inkcite/renderer/sup.rb +18 -4
  26. data/lib/inkcite/renderer/table.rb +9 -1
  27. data/lib/inkcite/renderer/table_base.rb +20 -2
  28. data/lib/inkcite/renderer/td.rb +7 -2
  29. data/lib/inkcite/renderer/video_preview.rb +1 -1
  30. data/lib/inkcite/uploader.rb +40 -27
  31. data/lib/inkcite/version.rb +1 -1
  32. data/lib/inkcite/view.rb +133 -13
  33. data/lib/inkcite/view/context.rb +6 -1
  34. data/lib/inkcite/view/developer_scripts.rb +56 -0
  35. data/test/renderer/background_spec.rb +4 -4
  36. data/test/renderer/button_spec.rb +14 -8
  37. data/test/renderer/div_spec.rb +26 -3
  38. data/test/renderer/image_spec.rb +9 -4
  39. data/test/renderer/list_spec.rb +36 -0
  40. data/test/renderer/mobile_image_spec.rb +5 -0
  41. data/test/renderer/slant_spec.rb +47 -0
  42. data/test/renderer/span_spec.rb +12 -1
  43. data/test/renderer/table_spec.rb +1 -1
  44. data/test/renderer/td_spec.rb +24 -8
  45. data/test/renderer/trademark_spec.rb +6 -6
  46. metadata +11 -50
  47. data/lib/inkcite/image/base.rb +0 -38
  48. data/lib/inkcite/image/guetzli_minifier.rb +0 -62
  49. data/lib/inkcite/image/image_minifier.rb +0 -143
  50. data/lib/inkcite/image/image_optim_minifier.rb +0 -90
  51. data/lib/inkcite/image/mozjpeg_minifier.rb +0 -92
@@ -258,6 +258,11 @@ module Inkcite
258
258
  styles << Inkcite::Renderer::Style.new(".gmail-fix", sfx.ctx, { FONT_SIZE => '3*px' })
259
259
  end
260
260
 
261
+ # True if we're limiting the rendering of the animation to iOS clients
262
+ only_ios = ctx.email? && !ctx.development?
263
+
264
+ styles << '@media screen and (-webkit-min-device-pixel-ratio:0) {' if only_ios
265
+
261
266
  # Create the <div> that wraps the entire animation.
262
267
  create_wrap_element html, sfx
263
268
 
@@ -276,6 +281,8 @@ module Inkcite
276
281
  # the individual children are configured.
277
282
  sfx.animations.each { |a| styles << a.to_keyframe_css }
278
283
 
284
+ styles << '}' if only_ios
285
+
279
286
  # Push the completed list of styles into the context's stack.
280
287
  ctx.styles << styles.join("\n")
281
288
 
@@ -1,5 +1,8 @@
1
1
  module Inkcite
2
2
  module Renderer
3
+
4
+ # Renders a bulletproof superscript tag.
5
+ # https://litmus.com/community/discussions/488-best-method-for-superscripts
3
6
  class Sup < Base
4
7
 
5
8
  def render tag, opt, ctx
@@ -13,11 +16,14 @@ module Inkcite
13
16
 
14
17
  sup = Element.new('sup', :style => { VERTICAL_ALIGN => :top })
15
18
 
16
- font_size = (opt[FONT_SIZE] || 10).to_i
17
- sup.style[FONT_SIZE] = px(font_size)
19
+ font_size = (opt[FONT_SIZE] || DEFAULT_FONT_SIZE_PERCENT).to_i
20
+ sup.style[FONT_SIZE] = pct(font_size)
21
+
22
+ line_height = (opt[LINE_HEIGHT] || 1).to_i
23
+ sup.style[LINE_HEIGHT] = line_height
18
24
 
19
- line_height = (opt[LINE_HEIGHT] || 10).to_i
20
- sup.style[LINE_HEIGHT] = px(line_height)
25
+ mso_text_raise = (font_size.to_f * (DEFAULT_MSO_TEST_RAISE_PERCENT / DEFAULT_FONT_SIZE_PERCENT)).round(0)
26
+ sup.style[MSO_TEXT_RAISE] = pct(mso_text_raise)
21
27
 
22
28
  html << sup.to_s
23
29
 
@@ -26,6 +32,14 @@ module Inkcite
26
32
  html
27
33
  end
28
34
 
35
+ private
36
+
37
+ # Name of the CSS style used to force
38
+ MSO_TEXT_RAISE = :'mso-text-raise'
39
+
40
+ DEFAULT_FONT_SIZE_PERCENT = 70.0
41
+ DEFAULT_MSO_TEST_RAISE_PERCENT = 60.0
42
+
29
43
  end
30
44
  end
31
45
  end
@@ -67,7 +67,15 @@ module Inkcite
67
67
  # If Fluid-Drop is enabled, padding is always zero at this top-level table
68
68
  # and will be applied in the TD renderer when it creates a new table to
69
69
  # wrap itself in.
70
- table[:cellpadding] = is_fluid_drop ? 0 : get_padding(opt)
70
+ cellpadding = is_fluid_drop ? 0 : get_padding(opt)
71
+ table[:cellpadding] = cellpadding
72
+
73
+ # Need to specify padding as px to ensure that the table plays nicely
74
+ # with high-DPI email clients like Outlook.
75
+ if cellpadding > 0
76
+ cellpaddingpx = px(cellpadding)
77
+ table.style[MSO_PADDING_ALT] = "#{cellpaddingpx} #{cellpaddingpx} #{cellpaddingpx} #{cellpaddingpx}"
78
+ end
71
79
 
72
80
  # Conveniently accept both float and align to mean the same thing.
73
81
  align = opt[:align] || opt[:float]
@@ -73,13 +73,31 @@ module Inkcite
73
73
  # Not taking .to_i because we want to accept both integer values
74
74
  # or percentages - e.g. 50%
75
75
  width = opt[:width]
76
- element[:width] = width unless width.blank?
76
+ unless width.blank?
77
+ element[:width] = width
78
+
79
+ # Need to redefine px-based width as a style to fix rendering
80
+ # problems in high DPI Outlook
81
+ # https://litmus.com/community/discussions/151-mystery-solved-dpi-scaling-in-outlook-2007-2013
82
+ element.style[:width] = px(width) unless width[-1] == '%' || is_fluid?(opt[:mobile])
83
+
84
+ end
85
+
77
86
 
78
87
  mobile_width = opt[MOBILE_WIDTH]
79
88
  element.mobile_style[:width] = px(mobile_width) unless none?(mobile_width)
80
89
 
81
90
  height = opt[:height].to_i
82
- element[:height] = height if height > 0
91
+ if height > 0
92
+ element[:height] = height
93
+
94
+ # Height also needs to be copied into the style to ensure that
95
+ # high DPI Outlook displays the table properly.
96
+ # https://litmus.com/community/discussions/151-mystery-solved-dpi-scaling-in-outlook-2007-2013
97
+ element.style[:height] = px(height)
98
+
99
+ end
100
+
83
101
 
84
102
  mobile_height = opt[MOBILE_HEIGHT]
85
103
  element.mobile_style[:height] = px(mobile_height) unless none?(mobile_height)
@@ -141,11 +141,16 @@ module Inkcite
141
141
  # you know, Outlook.
142
142
  align = opt[:align]
143
143
  unless align.blank?
144
- td[:align] = align
144
+
145
+ # If the alignment is justified, force the text alignment attribute to
146
+ # be left aligned so Outlook doesn't center the text.
147
+ td[:align] = align == JUSTIFY ? LEFT : align
145
148
 
146
149
  # Must use style to reinforce left-align text in certain email clients.
147
150
  # All other alignments are accepted naturally.
148
- td.style[TEXT_ALIGN] = align if align == LEFT
151
+ td.style[TEXT_ALIGN] = align if align == LEFT || align == JUSTIFY
152
+
153
+ mix_text_justify(td, opt, ctx) if align === JUSTIFY
149
154
 
150
155
  end
151
156
 
@@ -94,7 +94,7 @@ module Inkcite
94
94
 
95
95
  # Using an Element to produce the appropriate anchor helper with
96
96
  # the desired
97
- html << Element.new('a', { :id => id, :href => quote(href), :class => hover_klass, :bgcolor => bgcolor, :bggradient => gradient, :block => true }).to_helper
97
+ html << Element.new('a', { :id => id, :href => quote(href), :class => hover_klass, :bgcolor => bgcolor, :bggradient => gradient, BACKGROUND_GRADIENT_SHAPE => CIRCLE, :block => true }).to_helper
98
98
 
99
99
  table = Element.new('table', {
100
100
  :width => '100%', :background => frame_srcs[0], BACKGROUND_SIZE => 'cover',
@@ -4,45 +4,57 @@ require 'stringio'
4
4
  module Inkcite
5
5
  class Uploader
6
6
 
7
- def self.upload email
7
+ def self.upload email, opts
8
8
 
9
- times = []
9
+ # True if we're only uploading images.
10
+ images_only = !!opts[:images]
10
11
 
11
- Dir.glob(File.join(email.path, '*.{html,tsv,txt,yml}')).each do |file|
12
- times << File.mtime(file).to_i
13
- end
12
+ # Check to see if forcing the upload is specified. If not, check to see
13
+ # when the most recent file was updated and
14
+ force = !!opts[:force]
15
+ unless force
16
+
17
+ times = []
14
18
 
15
- local_images = email.image_dir
16
- if File.exist?(local_images)
17
- Dir.foreach(local_images) do |file|
18
- times << File.mtime(File.join(local_images, file)).to_i unless file.starts_with?('.')
19
+ unless images_only
20
+ Dir.glob(File.join(email.path, '*.{html,tsv,txt,yml}')).each do |file|
21
+ times << File.mtime(file).to_i
22
+ end
19
23
  end
20
- end
21
24
 
22
- # Get the most recently updated file.
23
- last_update = times.max
25
+ local_images = email.image_dir
26
+ if File.exist?(local_images)
27
+ Dir.foreach(local_images) do |file|
28
+ times << File.mtime(File.join(local_images, file)).to_i unless file.starts_with?('.')
29
+ end
30
+ end
24
31
 
25
- # Determine when the last upload was completed.
26
- last_upload = email.meta(:last_upload).to_i
32
+ # Get the most recently updated file.
33
+ last_update = times.max
27
34
 
28
- self.do_upload(email, false) if last_update > last_upload
35
+ # Determine when the last upload was completed.
36
+ last_upload = email.meta(:last_upload).to_i
29
37
 
30
- end
38
+ return unless last_update > last_upload
39
+
40
+ end
41
+
42
+ self.do_upload(email, force, images_only)
31
43
 
32
- def self.upload! email
33
- self.do_upload(email, true)
34
44
  end
35
45
 
36
46
  private
37
47
 
38
48
  IMAGE_PATH = :'image-path'
39
49
 
40
- def self.copy! sftp, local, remote, force=true
50
+ def self.copy! email, sftp, local, remote, force=true
41
51
 
42
52
  # Nothing to copy unless the local directory exists (e.g. some emails don't
43
53
  # have an images directory.)
44
54
  return unless File.exist?(local)
45
55
 
56
+ last_upload = email.meta(:last_upload).to_i
57
+
46
58
  Dir.foreach(local) do |file|
47
59
  next if file.starts_with?('.')
48
60
 
@@ -52,11 +64,7 @@ module Inkcite
52
64
  remote_file = File.join(remote, file)
53
65
 
54
66
  unless force
55
- next unless begin
56
- File.stat(local_file).mtime > Time.at(sftp.stat!(remote_file).mtime)
57
- rescue Net::SFTP::StatusException
58
- true # File doesn't exist, so assume it's changed.
59
- end
67
+ next unless File.mtime(local_file).to_i > last_upload
60
68
  end
61
69
 
62
70
  puts "Uploading #{local_file} -> #{remote_file} ..."
@@ -69,7 +77,7 @@ module Inkcite
69
77
 
70
78
  # Internal method responsive for doing the actual upload and
71
79
  # forcing (if necessary) the update of the graphics.
72
- def self.do_upload email, force
80
+ def self.do_upload email, force, images_only
73
81
 
74
82
  # The preview version defines the configuration for the server to which
75
83
  # the files will be sftp'd.
@@ -122,8 +130,13 @@ module Inkcite
122
130
  # to ensure that we're not repeatedly uploading the same images over and
123
131
  # over when force is enabled -- but will re-upload images to distinct
124
132
  # remote roots.
125
- copy! sftp, local_images, remote_root, force && last_remote_root != remote_root
126
- last_remote_root = remote_root
133
+ if last_remote_root != remote_root
134
+ copy! email, sftp, local_images, remote_root, force
135
+ last_remote_root = remote_root
136
+ end
137
+
138
+ # Nothing left to do if this is an image-only upload
139
+ next if images_only
127
140
 
128
141
  # Check to see if we're creating an in-browser version of the email.
129
142
  next unless email.formats.include?(:browser)
@@ -1,3 +1,3 @@
1
1
  module Inkcite
2
- VERSION = "1.15.0"
2
+ VERSION = "1.16.0"
3
3
  end
@@ -239,9 +239,6 @@ module Inkcite
239
239
 
240
240
  tag = tag.to_sym
241
241
 
242
- # Warn the user if the helper is already defined.
243
- view.error("Helper '#{tag}' already defined", :open => open, :close => close) unless @config[tag].nil?
244
-
245
242
  @config[tag] = open.to_s
246
243
  @config[:"/#{tag}"] = close.to_s
247
244
 
@@ -262,7 +259,11 @@ module Inkcite
262
259
 
263
260
  src_url = ''
264
261
 
265
- if Util.is_fully_qualified?(src)
262
+ # Check to see if images are disabled - if so, return a broken image.
263
+ if self[IMAGES_OFF]
264
+ src_url = "__#{src}"
265
+
266
+ elsif Util.is_fully_qualified?(src)
266
267
  src_url << src
267
268
 
268
269
  else
@@ -270,7 +271,7 @@ module Inkcite
270
271
  # Prepend the image host onto the src if one is specified in the properties.
271
272
  # During local development, images are always expected in an images/ subdirectory.
272
273
  image_host = if development?
273
- (@email.optimize_images? ? Image::ImageMinifier::IMAGE_CACHE : Email::IMAGES) + '/'
274
+ (@email.optimize_images? ? ImageMinifier::IMAGE_CACHE : Email::IMAGES) + '/'
274
275
  else
275
276
 
276
277
  # Use the image host defined in config.yml or, out-of-the-box refer to images/
@@ -293,6 +294,14 @@ module Inkcite
293
294
  Renderer.render(src_url, self)
294
295
  end
295
296
 
297
+ def images_off?
298
+ is_enabled?(IMAGES_OFF)
299
+ end
300
+
301
+ def images_off= off
302
+ @config[IMAGES_OFF] = !!off
303
+ end
304
+
296
305
  # Tests if a configuration value has been enabled. This assumes
297
306
  # it is disabled by default but that a value of true, 'true' or 1
298
307
  # for the value indicates it is enabled.
@@ -309,7 +318,6 @@ module Inkcite
309
318
  !val.nil? && (val == false || val == false.to_s)
310
319
  end
311
320
 
312
-
313
321
  def links_file_name
314
322
 
315
323
  # There is nothing to return if trackable links aren't enabled.
@@ -463,14 +471,20 @@ module Inkcite
463
471
 
464
472
  # Resolve the HTML declaration for this email based on whether or not VML was used.
465
473
  html_declaration = '<html xmlns="http://www.w3.org/1999/xhtml"'
466
- html_declaration << ' xmlns:v="urn:schemas-microsoft-com:vml" lang="en" xml:lang="en"' if vml_used?
474
+ html_declaration << ' xmlns:v="urn:schemas-microsoft-com:vml"' if vml_used?
475
+ html_declaration << ' xmlns:o="urn:schemas-microsoft-com:office:office"' if email?
467
476
  html_declaration << '>'
468
477
  html << html_declaration
469
478
 
470
479
  html << '<head>'
471
480
  html << '<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>'
481
+
482
+ # iOS 10 auto-scale fix
483
+ # https://emails.hteumeuleu.com/what-you-need-to-know-about-apple-mail-in-ios-10-c7031f6d704d
484
+ html << '<meta name="x-apple-disable-message-reformatting"/>'
472
485
  html << '<meta name="viewport" content="width=device-width"/>'
473
- html << "<meta name=\"generator\" content=\"Inkcite #{Inkcite::VERSION}\"/>"
486
+
487
+ html << %Q(<meta name="generator" content="Inkcite #{Inkcite::VERSION}"/>)
474
488
 
475
489
  # Enable responsive media queries on Windows phones courtesy of @jamesmacwhite
476
490
  # https://blog.jmwhite.co.uk/2014/03/01/windows-phone-does-support-css3-media-queries-in-html-email/
@@ -490,6 +504,8 @@ module Inkcite
490
504
 
491
505
  html << outlook_styles
492
506
 
507
+ html << outlook_high_dpi_fix if email?
508
+
493
509
  html << '</head>'
494
510
 
495
511
  html << body_declaration
@@ -514,6 +530,13 @@ module Inkcite
514
530
  # process) with a chance to operate on the final HTML.
515
531
  @content = PostProcessor.run_all(@content, self)
516
532
 
533
+ # Some ESPs have link tracking/opt-out code that uses greater-than and
534
+ # less-than symbols. Encode them as their HTML escape code equivalents
535
+ # in the source and replace them with their original characters in the
536
+ # final output.
537
+ @content.gsub!('&#62;', '>')
538
+ @content.gsub!('&#60;', '<')
539
+
517
540
  # Ensure that all failsafes pass
518
541
  assert_failsafes
519
542
 
@@ -695,6 +718,10 @@ module Inkcite
695
718
  # Tab-separated file containing global substitution values.
696
719
  SUBSTITUTIONS_TSV_FILE = 'substitutions.tsv'
697
720
 
721
+ # Config value which disables images (e.g. shows their size, position
722
+ # background color and alt text).
723
+ IMAGES_OFF = :'images-off'
724
+
698
725
  def assert_in_browser msg
699
726
  raise msg if email? && !development?
700
727
  end
@@ -907,7 +934,7 @@ module Inkcite
907
934
  # http://www.ianhoar.com/2008/04/29/outlook-2007-borders-and-1px-padding-on-table-cells
908
935
  # http://www.campaignmonitor.com/blog/post/3392/1px-borders-padding-on-table-cells-in-outlook-07
909
936
  reset << 'table { border-spacing: 0; }'
910
- reset << 'table, td { border-collapse: collapse; }'
937
+ reset << 'table, td { border-collapse: collapse; mso-table-lspace: 0pt !important; mso-table-rspace: 0pt !important; }'
911
938
 
912
939
  # Ensure that telephone numbers are displayed using the same style as links.
913
940
  reset << "a[href^=tel] { color: #{self[Renderer::Base::LINK_COLOR]}; text-decoration:none;}"
@@ -921,6 +948,9 @@ module Inkcite
921
948
  # Reset the font on every cell to the default family.
922
949
  reset << "td { font-family: #{self[Renderer::Base::FONT_FAMILY]}; }"
923
950
 
951
+ # Allow smoother rendering of resized image in Internet Explorer
952
+ reset << "img { -ms-interpolation-mode:bicubic !important; }"
953
+
924
954
  reset.join(NEW_LINE)
925
955
  end
926
956
 
@@ -971,24 +1001,108 @@ module Inkcite
971
1001
  html.join(NEW_LINE)
972
1002
  end
973
1003
 
1004
+ def outlook_high_dpi_fix
1005
+
1006
+ # This is the XML required to ensure high-DPI Outlook plays nicely with the
1007
+ # email when it is displayed.
1008
+ # https://www.emailonacid.com/blog/article/email-development/dpi_scaling_in_outlook_2007-2013
1009
+ html = []
1010
+ html << '<!--[if gte mso 9]>'
1011
+ html << '<xml>'
1012
+ html << '<o:OfficeDocumentSettings>'
1013
+ html << '<o:AllowPNG/>'
1014
+ html << '<o:PixelsPerInch>96</o:PixelsPerInch>'
1015
+ html << '</o:OfficeDocumentSettings>'
1016
+ html << '</xml>'
1017
+ html << '<![endif]-->'
1018
+
1019
+ return html.join(NEW_LINE)
1020
+ end
1021
+
974
1022
  def outlook_styles
975
1023
 
976
1024
  html = []
1025
+ html << '<!--[if mso]>' if email?
1026
+ html << '<style>'
1027
+
1028
+ if email?
1029
+
1030
+ # These are the Outlook-specific styles necessary to block the
1031
+ # visited link from changing color in Outlook 2007-2013.
1032
+ # https://litmus.com/community/discussions/4164-outlook-07-13-visited-link-color-fix
1033
+ vlink_styles = { :'mso-style-priority' => 99, :color => 'inherit' }
1034
+ %w(MsoHyperlink MsoHyperlinkFollowed).each do |l|
1035
+ html << Inkcite::Renderer::Style.new("span.#{l}", self, vlink_styles).to_s
1036
+ end
1037
+
1038
+ end
977
1039
 
978
1040
  # VML-specific CSS needed only if VML was used in the email.
979
1041
  if vml_used?
980
- html << '<style>'
981
-
982
1042
  %w(v o).each do |l|
983
1043
  html << Inkcite::Renderer::Style.new("#{l}\:*", self, { :behavior => 'url(#default#VML)', :display => 'inline-block' }).to_s
984
1044
  end
1045
+ end
985
1046
 
986
1047
  html << '</style>'
987
- end
1048
+ html << '<![endif]-->' if email?
988
1049
 
989
1050
  html.join(NEW_LINE)
990
1051
  end
991
1052
 
1053
+ def load_google_sheets_helpers url, into
1054
+
1055
+ # Add a timestamp to break google's cache and force the page to
1056
+ # be reloaded each time
1057
+ url = Util::add_query_param(url, Time.now.to_i)
1058
+
1059
+ raw = Net::HTTP.get(URI.parse(url))
1060
+
1061
+ # Consolidate line-breaks for simplicity
1062
+ raw.gsub!(/[\r\f\n]{1,}/, NEW_LINE)
1063
+ raw.gsub!(/ \& /, " &amp; ")
1064
+
1065
+ fields = nil
1066
+
1067
+ raw.split(NEW_LINE).each do |line|
1068
+ columns = line.split(TAB)
1069
+ if fields.nil?
1070
+ fields = columns.collect(&:to_sym)
1071
+
1072
+ else
1073
+
1074
+ version = columns[0]
1075
+
1076
+ # Check to see if this version includes an asterisk
1077
+ is_any_version = if version == '*'
1078
+ true
1079
+ elsif version.include?('*')
1080
+ prefix, suffix = version.split('*')
1081
+ (prefix.blank? || @version.to_s.start_with?(prefix)) && (suffix.blank? || @version.to_s.end_with?(suffix))
1082
+ else
1083
+ false
1084
+ end
1085
+
1086
+ is_version = version.to_sym == @version
1087
+
1088
+ if is_any_version || is_version
1089
+ fields.each_with_index do |field, index|
1090
+ unless index == 0
1091
+ value = columns[index]
1092
+ into[field] = value unless value.blank?
1093
+ end
1094
+ end
1095
+
1096
+ break if is_version
1097
+ end
1098
+
1099
+
1100
+ end
1101
+ end
1102
+
1103
+ true
1104
+ end
1105
+
992
1106
  def load_helper_file file, into, abort_on_fail=true
993
1107
 
994
1108
  unless File.exist?(file)
@@ -1053,7 +1167,13 @@ module Inkcite
1053
1167
  # Check to see if the config.yml includes a "helpers:" array which allows
1054
1168
  # additional out-of-project, shared helper files to be loaded.
1055
1169
  included_helpers = [*@email.config[:helpers]]
1056
- included_helpers.each { |file| load_helper_file(File.join(path, file), _helpers) }
1170
+ included_helpers.each do |file|
1171
+ if file.start_with?('http')
1172
+ load_google_sheets_helpers file, _helpers
1173
+ else
1174
+ load_helper_file(File.join(path, file), _helpers)
1175
+ end
1176
+ end
1057
1177
 
1058
1178
  # Load the project's properties, which may include references to additional
1059
1179
  # properties in other directories.