inkcite 1.14.0 → 1.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/assets/init/config.yml +2 -0
  3. data/assets/social/facebook.png +0 -0
  4. data/assets/social/instagram.png +0 -0
  5. data/assets/social/pintrest.png +0 -0
  6. data/assets/social/twitter.png +0 -0
  7. data/inkcite.gemspec +2 -2
  8. data/lib/inkcite.rb +1 -0
  9. data/lib/inkcite/cli/base.rb +32 -6
  10. data/lib/inkcite/cli/preview.rb +24 -28
  11. data/lib/inkcite/cli/server.rb +22 -19
  12. data/lib/inkcite/cli/test.rb +1 -1
  13. data/lib/inkcite/email.rb +8 -4
  14. data/lib/inkcite/facade/animation.rb +4 -0
  15. data/lib/inkcite/facade/keyframe.rb +26 -3
  16. data/lib/inkcite/image/base.rb +38 -0
  17. data/lib/inkcite/image/guetzli_minifier.rb +62 -0
  18. data/lib/inkcite/image/image_minifier.rb +143 -0
  19. data/lib/inkcite/image/image_optim_minifier.rb +90 -0
  20. data/lib/inkcite/image/mozjpeg_minifier.rb +92 -0
  21. data/lib/inkcite/mailer.rb +201 -112
  22. data/lib/inkcite/minifier.rb +2 -146
  23. data/lib/inkcite/post_processor.rb +13 -0
  24. data/lib/inkcite/renderer.rb +19 -0
  25. data/lib/inkcite/renderer/background.rb +53 -14
  26. data/lib/inkcite/renderer/base.rb +29 -15
  27. data/lib/inkcite/renderer/button.rb +1 -1
  28. data/lib/inkcite/renderer/carousel.rb +245 -0
  29. data/lib/inkcite/renderer/container_base.rb +10 -0
  30. data/lib/inkcite/renderer/div.rb +1 -3
  31. data/lib/inkcite/renderer/fireworks.rb +54 -40
  32. data/lib/inkcite/renderer/footnote.rb +22 -2
  33. data/lib/inkcite/renderer/image.rb +11 -0
  34. data/lib/inkcite/renderer/image_base.rb +3 -6
  35. data/lib/inkcite/renderer/in_browser.rb +4 -0
  36. data/lib/inkcite/renderer/link.rb +39 -12
  37. data/lib/inkcite/renderer/mobile_image.rb +1 -1
  38. data/lib/inkcite/renderer/responsive.rb +9 -1
  39. data/lib/inkcite/renderer/social.rb +31 -3
  40. data/lib/inkcite/renderer/special_effect.rb +22 -13
  41. data/lib/inkcite/renderer/sup.rb +32 -0
  42. data/lib/inkcite/renderer/table_base.rb +3 -0
  43. data/lib/inkcite/renderer/topic.rb +76 -0
  44. data/lib/inkcite/renderer/trademark.rb +47 -0
  45. data/lib/inkcite/renderer/video_preview.rb +3 -2
  46. data/lib/inkcite/uploader.rb +2 -3
  47. data/lib/inkcite/util.rb +51 -0
  48. data/lib/inkcite/version.rb +1 -1
  49. data/lib/inkcite/view.rb +140 -54
  50. data/lib/inkcite/view/context.rb +1 -31
  51. data/lib/inkcite/view/media_query.rb +6 -0
  52. data/test/animation_spec.rb +7 -0
  53. data/test/parser_spec.rb +1 -1
  54. data/test/renderer/background_spec.rb +16 -12
  55. data/test/renderer/div_spec.rb +11 -0
  56. data/test/renderer/footnote_spec.rb +5 -1
  57. data/test/renderer/image_spec.rb +51 -28
  58. data/test/renderer/link_spec.rb +20 -8
  59. data/test/renderer/lorem_spec.rb +2 -2
  60. data/test/renderer/mobile_image_spec.rb +6 -0
  61. data/test/renderer/mobile_style_spec.rb +3 -3
  62. data/test/renderer/redacted_spec.rb +2 -2
  63. data/test/renderer/social_spec.rb +6 -6
  64. data/test/renderer/table_spec.rb +4 -0
  65. data/test/renderer/topic_spec.rb +28 -0
  66. data/test/renderer/trademark_spec.rb +40 -0
  67. data/test/renderer/video_preview_spec.rb +1 -1
  68. data/test/test_helper.rb +14 -0
  69. data/test/view_spec.rb +4 -0
  70. metadata +26 -12
  71. data/assets/init/image_optim.yml +0 -37
@@ -0,0 +1,47 @@
1
+ module Inkcite
2
+ module Renderer
3
+ class Trademark < Base
4
+
5
+ def initialize sym
6
+ @sym = sym
7
+ end
8
+
9
+ def render tag, opt, ctx
10
+
11
+ # Check to see if there is an ID associated with this symbol.
12
+ # If so, it only needs to appear once.
13
+ id = opt[:id]
14
+
15
+ if id.blank?
16
+ ctx.error('Missing id on trademark/registered symbol')
17
+ id = "tm#{ctx.unique_id(:trademark)}"
18
+ end
19
+
20
+ return '' unless ctx.once?("#{id}-trademark")
21
+
22
+ no_sup = opt[:'no-sup']
23
+
24
+ html = ''
25
+ html << '{sup}' unless no_sup
26
+ html << @sym
27
+
28
+ # If the trademark symbol should be followed immediately with a footnote
29
+ # render the {footnote} Helper with the once flag and the no-sup attribute
30
+ # to ensure unnecessary superscripts don't get rendered.
31
+ footnote = opt[:footnote]
32
+ unless footnote.blank?
33
+ html << %Q({footnote id="#{id}")
34
+
35
+ # Only include the footnote text if the attribute is not boolean
36
+ html << %Q( text="#{footnote}") if footnote != true
37
+ html << %q( once}) unless footnote.blank?
38
+ end
39
+
40
+ html << '{/sup}' unless no_sup
41
+
42
+ html
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -242,9 +242,10 @@ module Inkcite
242
242
  frames.each do |f|
243
243
  this_frame_url = "url(#{f})"
244
244
 
245
- animation.add_keyframe(percent, { BACKGROUND_IMAGE => this_frame_url })
245
+ keyframe = animation.add_keyframe(percent.round(0), { BACKGROUND_IMAGE => this_frame_url })
246
246
  percent += percent_per_frame
247
- animation.add_keyframe(percent, { BACKGROUND_IMAGE => this_frame_url })
247
+ keyframe.end_percent = percent.round(0)
248
+
248
249
  percent += percent_per_transition
249
250
 
250
251
  end
@@ -8,9 +8,8 @@ module Inkcite
8
8
 
9
9
  times = []
10
10
 
11
- ['source.html', 'source.txt', 'helpers.tsv'].each do |file|
12
- file = email.project_file(file)
13
- times << File.mtime(file).to_i if File.exist?(file)
11
+ Dir.glob(File.join(email.path, '*.{html,tsv,txt,yml}')).each do |file|
12
+ times << File.mtime(file).to_i
14
13
  end
15
14
 
16
15
  local_images = email.image_dir
@@ -38,6 +38,10 @@ module Inkcite
38
38
  opts.detect { |o| !o.blank? }
39
39
  end
40
40
 
41
+ def self.dir_size path
42
+ Dir.glob(File.join(path, '*.*')).inject(0) { |size, file| size + File.size(file) }
43
+ end
44
+
41
45
  # Centralizing the URL/CGI encoding for all HREF processing because
42
46
  # URI.escape/encode is obsolete.
43
47
  def self.encode *arg
@@ -52,6 +56,15 @@ module Inkcite
52
56
  end
53
57
  end
54
58
 
59
+ def self.exec command
60
+ command = command.join(' ') if command.is_a?(Array)
61
+ `#{command} 2>&1`
62
+ end
63
+
64
+ def self.file_extension path_to_file
65
+ File.extname(path_to_file).delete('.').downcase
66
+ end
67
+
55
68
  # Conversion of HSL to RGB color courtesy of
56
69
  # http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
57
70
  def self.hsl_to_color h, s, l
@@ -102,6 +115,40 @@ module Inkcite
102
115
  p
103
116
  end
104
117
 
118
+ def self.log message, meta=nil
119
+
120
+ msg = "#{Time.now.strftime(DATE_FORMAT)} - INFO - #{message}"
121
+ unless meta.blank?
122
+ msg << '['
123
+ msg << Renderer.join_hash(meta, '=', ', ')
124
+ msg << ']'
125
+ end
126
+
127
+ puts msg
128
+ end
129
+
130
+ def self.pretty_file_size size
131
+
132
+ size = size.to_f
133
+ decimals = 2
134
+ ext = 'b'
135
+
136
+ if size > KB
137
+ size = size / KB
138
+ if size > KB
139
+ size = size / KB
140
+ ext = 'MB'
141
+ else
142
+ ext = 'Kb'
143
+ end
144
+ else
145
+ round = 0
146
+ "#{size}b"
147
+ end
148
+
149
+ "#{size.round(decimals)}#{ext}"
150
+ end
151
+
105
152
  # RGB to hex courtesy of
106
153
  # http://blog.lebrijo.com/converting-rgb-colors-to-hexadecimal-with-ruby/
107
154
  def self.rgb_to_hex val
@@ -166,6 +213,10 @@ module Inkcite
166
213
  BLACK = '#000000'
167
214
  WHITE = '#111111'
168
215
 
216
+ DATE_FORMAT = '%H:%M:%S'
217
+
218
+ KB = 1024
219
+
169
220
  # Recursive key symbolization for the provided Hash.
170
221
  def self.symbolize_keys hash
171
222
  unless hash.nil?
@@ -1,3 +1,3 @@
1
1
  module Inkcite
2
- VERSION = "1.14.0"
2
+ VERSION = "1.15.0"
3
3
  end
@@ -23,9 +23,6 @@ module Inkcite
23
23
  # Manages the Responsive::Rules applied to this email view.
24
24
  attr_reader :media_query
25
25
 
26
- # Line number of the email file being processed
27
- attr_accessor :line_number
28
-
29
26
  # The configuration hash for the view
30
27
  attr_accessor :config
31
28
 
@@ -37,6 +34,14 @@ module Inkcite
37
34
  # after a rendering is complete.
38
35
  attr_accessor :js_compressor
39
36
 
37
+ # Array of post-processors that can modify the HTML of the email
38
+ # after it is rendered but before it is saved.
39
+ attr_accessor :post_processors
40
+
41
+ # The most recently processed tag populated from the renderer and
42
+ # used when reporting errors.
43
+ attr_accessor :last_rendered_markup
44
+
40
45
  def initialize email, environment, format, version
41
46
  @email = email
42
47
  @environment = environment
@@ -77,10 +82,6 @@ module Inkcite
77
82
  # version in the list of those defined.
78
83
  @config[:'version-index'] = (email.versions.index(version) + 1).to_s
79
84
 
80
- # Tracks the line number and is recorded when errors are encountered
81
- # while rendering said line.
82
- @line_number = 0
83
-
84
85
  # True if VML is used during the preparation of this email.
85
86
  @vml_used = false
86
87
 
@@ -88,6 +89,9 @@ module Inkcite
88
89
  @footnotes = nil
89
90
  @substitutions = nil
90
91
 
92
+ # Will hold any post processors installed during rendering.
93
+ @post_processors = []
94
+
91
95
  end
92
96
 
93
97
  def [] key
@@ -149,9 +153,10 @@ module Inkcite
149
153
  end
150
154
 
151
155
  # Records an error message on the currently processing line of the source.
152
- def error message, obj=nil
156
+ def error message, obj={}
157
+
158
+ obj[:markup] = %Q({#{self.last_rendered_markup}})
153
159
 
154
- message << " (line #{self.line_number.to_i})"
155
160
  unless obj.blank?
156
161
  message << ' ['
157
162
  message << obj.collect { |k, v| "#{k}=#{v}" }.join(', ')
@@ -206,12 +211,12 @@ module Inkcite
206
211
  # Default naming based on the number of versions - only the format if there is
207
212
  # a single version or version and format when there are multiple versions.
208
213
  fn = if email.versions.length > 1
209
- '{version}-{format}'
210
- elsif text?
211
- 'email'
212
- else
213
- '{format}'
214
- end
214
+ '{version}-{format}'
215
+ elsif text?
216
+ 'email'
217
+ else
218
+ '{format}'
219
+ end
215
220
 
216
221
  end
217
222
 
@@ -221,12 +226,27 @@ module Inkcite
221
226
 
222
227
  # Sanity check to ensure there is an appropriate extension on the
223
228
  # file name.
224
- ext ||= (text?? TXT_EXTENSION : HTML_EXTENSION)
229
+ ext ||= (text? ? TXT_EXTENSION : HTML_EXTENSION)
225
230
  fn << ext unless File.extname(fn) == ext
226
231
 
227
232
  fn
228
233
  end
229
234
 
235
+ # Defines a new helper, which allows designers to keep helper
236
+ # markup alongside the usage of it inside of partial. Helps keep
237
+ # code clean and prevents helper.tsv pollution for one-offs
238
+ def helper tag, open, close=nil
239
+
240
+ tag = tag.to_sym
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
+ @config[tag] = open.to_s
246
+ @config[:"/#{tag}"] = close.to_s
247
+
248
+ end
249
+
230
250
  # Returns the fully-qualified URL to the designated image (e.g. logo.gif)
231
251
  # appropriate for the current rendering environment. In development
232
252
  # mode, local will have either images/ or images-optim/ prepended on them
@@ -250,7 +270,7 @@ module Inkcite
250
270
  # Prepend the image host onto the src if one is specified in the properties.
251
271
  # During local development, images are always expected in an images/ subdirectory.
252
272
  image_host = if development?
253
- (@email.optimize_images?? Minifier::IMAGE_CACHE : Email::IMAGES) + '/'
273
+ (@email.optimize_images? ? Image::ImageMinifier::IMAGE_CACHE : Email::IMAGES) + '/'
254
274
  else
255
275
 
256
276
  # Use the image host defined in config.yml or, out-of-the-box refer to images/
@@ -327,7 +347,23 @@ module Inkcite
327
347
 
328
348
  def meta key
329
349
  md = meta_data
330
- md.nil?? nil : md[key]
350
+ md.nil? ? nil : md[key]
351
+ end
352
+
353
+ # Returns true if the specified key has been queried once and
354
+ # only once. Any additional queries will return false
355
+ def once? key
356
+
357
+ # Initialize the 'once' data hash which maps
358
+ data[:once] ||= {}
359
+
360
+ key = key.to_sym
361
+
362
+ # True if this is the first time we've encountered this key.
363
+ first_time = data[:once][key].nil?
364
+ data[:once][key] = true if first_time
365
+
366
+ first_time
331
367
  end
332
368
 
333
369
  # Returns the opts for the parent matching the designated
@@ -339,7 +375,7 @@ module Inkcite
339
375
  # Returns the array of browser prefixes that need to be included in
340
376
  # CSS styles based on which version of the email this is.
341
377
  def prefixes
342
- _prefixes = [ '' ]
378
+ _prefixes = ['']
343
379
 
344
380
  # In development mode, include all prefixes for maximum compatibility.
345
381
  _prefixes += %w(-moz- -ms- -o-) if development?
@@ -365,7 +401,7 @@ module Inkcite
365
401
 
366
402
  # Will be used to assemble the parameters passed to File.open.
367
403
  # First, always open the file in read mode.
368
- mode = [ 'r' ]
404
+ mode = ['r']
369
405
 
370
406
  # Detect abnormal file encoding and construct the string to
371
407
  # convert such encoding to UTF-8 if specified.
@@ -398,7 +434,7 @@ module Inkcite
398
434
  raise "Already rendered" unless @content.blank?
399
435
 
400
436
  source_file = 'source'
401
- source_file << (text?? TXT_EXTENSION : HTML_EXTENSION)
437
+ source_file << (text? ? TXT_EXTENSION : HTML_EXTENSION)
402
438
 
403
439
  # Read the original source which may include embedded Ruby.
404
440
  filtered = read_source(@email.project_file(source_file))
@@ -451,21 +487,12 @@ module Inkcite
451
487
  html += external_styles
452
488
 
453
489
  html << inline_styles
454
- html << '</head>'
455
490
 
456
- # Intentionally not setting the link colors because those should be entirely
457
- # controlled by the styles and attributes of the links themselves. By not
458
- # setting it, links created sans-helper should be visually distinct.
459
- html << '<body style="width: 100% !important; min-width: 100% !important; margin: 0 !important; padding: 0; -webkit-text-size-adjust: none; -ms-text-size-adjust: none;'
491
+ html << outlook_styles
460
492
 
461
- # A pleasing but obvious background exposed in development mode to alert
462
- # the designer that they have exposed the body background - which means
463
- # unpredictable results if sent.
464
- if development?
465
- html << " background: #ccc url('data:image/png;base64,#{Inkcite.blueprint_image64}');"
466
- end
493
+ html << '</head>'
467
494
 
468
- html << %q(">)
495
+ html << body_declaration
469
496
 
470
497
  html << minified
471
498
 
@@ -483,6 +510,10 @@ module Inkcite
483
510
 
484
511
  end
485
512
 
513
+ # Provide each post processor (if any were installed during the rendering
514
+ # process) with a chance to operate on the final HTML.
515
+ @content = PostProcessor.run_all(@content, self)
516
+
486
517
  # Ensure that all failsafes pass
487
518
  assert_failsafes
488
519
 
@@ -619,17 +650,17 @@ module Inkcite
619
650
 
620
651
  private
621
652
 
622
- ASSETS = 'assets'
623
- FILE_SCHEME = 'file'
624
- FILE_NAME = :'file-name'
625
- HTML_EXTENSION = '.html'
653
+ ASSETS = 'assets'
654
+ FILE_SCHEME = 'file'
655
+ FILE_NAME = :'file-name'
656
+ HTML_EXTENSION = '.html'
626
657
  LINKS_EXTENSION = '-links.csv'
627
- NEW_LINE = "\n"
628
- REGEX_SLASH = '/'
658
+ NEW_LINE = "\n"
659
+ REGEX_SLASH = '/'
629
660
  SOURCE_ENCODING = :'source-encoding'
630
- TAB = "\t"
631
- TXT_EXTENSION = '.txt'
632
- UTF_8 = 'utf-8'
661
+ TAB = "\t"
662
+ TXT_EXTENSION = '.txt'
663
+ UTF_8 = 'utf-8'
633
664
 
634
665
  # Empty hash used when there is no environment or format-specific configuration
635
666
  EMPTY_HASH = {}
@@ -696,6 +727,31 @@ module Inkcite
696
727
  passes
697
728
  end
698
729
 
730
+ def body_declaration
731
+
732
+ # Intentionally not setting the link colors because those should be entirely
733
+ # controlled by the styles and attributes of the links themselves. By not
734
+ # setting it, links created sans-helper should be visually distinct.
735
+ body = '<body'
736
+
737
+ # The body class is used to fix the width problem in new Gmail iOS.
738
+ # https://litmus.com/community/discussions/5913-new-gmail-app-not-respect-full-width
739
+ body << ' class="body"' if email?
740
+
741
+ body << ' style="margin: 0; padding: 0; -webkit-text-size-adjust: none; -ms-text-size-adjust: none;'
742
+
743
+ # A pleasing but obvious background exposed in development mode to alert
744
+ # the designer that they have exposed the body background - which means
745
+ # unpredictable results if sent.
746
+ if development?
747
+ body << %Q( background: #ccc url('data:image/png;base64,#{Inkcite.blueprint_image64}');)
748
+ end
749
+
750
+ body << '">'
751
+
752
+ body
753
+ end
754
+
699
755
  # Returns true if the content in this email either matches the
700
756
  # regular expression provided or if it includes the exact string
701
757
  # that is provided.
@@ -865,12 +921,6 @@ module Inkcite
865
921
  # Reset the font on every cell to the default family.
866
922
  reset << "td { font-family: #{self[Renderer::Base::FONT_FAMILY]}; }"
867
923
 
868
- # VML-specific CSS needed only if VML was used in the email.
869
- if vml_used?
870
- reset << 'v\:* { behavior: url(#default#VML); display: inline-block; }'
871
- reset << 'o\:* { behavior: url(#default#VML); display: inline-block; }'
872
- end
873
-
874
924
  reset.join(NEW_LINE)
875
925
  end
876
926
 
@@ -907,7 +957,7 @@ module Inkcite
907
957
 
908
958
  # Join all of the style blocks into a single block if this
909
959
  # is not an email - otherwise, keep them separate.
910
- style_blocks = [ style_blocks.join(NEW_LINE) ] unless email?
960
+ style_blocks = [style_blocks.join(NEW_LINE)] unless email?
911
961
 
912
962
  html = []
913
963
 
@@ -921,6 +971,24 @@ module Inkcite
921
971
  html.join(NEW_LINE)
922
972
  end
923
973
 
974
+ def outlook_styles
975
+
976
+ html = []
977
+
978
+ # VML-specific CSS needed only if VML was used in the email.
979
+ if vml_used?
980
+ html << '<style>'
981
+
982
+ %w(v o).each do |l|
983
+ html << Inkcite::Renderer::Style.new("#{l}\:*", self, { :behavior => 'url(#default#VML)', :display => 'inline-block' }).to_s
984
+ end
985
+
986
+ html << '</style>'
987
+ end
988
+
989
+ html.join(NEW_LINE)
990
+ end
991
+
924
992
  def load_helper_file file, into, abort_on_fail=true
925
993
 
926
994
  unless File.exist?(file)
@@ -973,7 +1041,7 @@ module Inkcite
973
1041
  def load_helpers
974
1042
 
975
1043
  _helpers = {
976
- :n => NEW_LINE
1044
+ :n => NEW_LINE
977
1045
  }
978
1046
 
979
1047
  # Get the project path from which most helpers will be loaded.
@@ -995,9 +1063,30 @@ module Inkcite
995
1063
  # on its version - e.g. difference colors in the "no orders for 30 days" email.
996
1064
  load_helper_file File.join(path, "helpers-#{@version}.tsv"), _helpers, false
997
1065
 
1066
+ project_path = File.basename(@email.path)
1067
+
1068
+ # If the project directory name is all numbers and is either six or eight
1069
+ # characters in length, assume it is a date - e.g. 201706 or 20170513.
1070
+ # Automatically extract the year, month and day (optional).
1071
+ if project_path.to_i.to_s == project_path
1072
+ path_length = project_path.length
1073
+ if path_length >= 4 && path_length <= 8
1074
+
1075
+ # These substrings need to remain strings so that properties can be
1076
+ # cloned when a view is instanted.
1077
+ _helpers[:yyyy] = project_path[0..3]
1078
+ _helpers[:mm] = project_path[4..5] if path_length >= 6
1079
+ _helpers[:dd] = project_path[6..7] if path_length == 8
1080
+
1081
+ end
1082
+ end
1083
+
998
1084
  # As a convenience pre-populate the month name of the email.
999
1085
  mm = _helpers[:mm].to_i
1000
- _helpers[:month] = Date::MONTHNAMES[mm] if mm > 0
1086
+ if mm > 0
1087
+ _helpers[:month] = Date::MONTHNAMES[mm]
1088
+ _helpers[:MONTH] = _helpers[:month].upcase
1089
+ end
1001
1090
 
1002
1091
  _helpers
1003
1092
  end
@@ -1036,9 +1125,6 @@ module Inkcite
1036
1125
 
1037
1126
  filtered.split("\n").each do |line|
1038
1127
 
1039
- # Increment the line number as we read the file.
1040
- @line_number += 1
1041
-
1042
1128
  begin
1043
1129
  line = Renderer.render(line, self)
1044
1130
  rescue Exception => e