bootstrap-email 1.0.0.alpha1 → 1.0.0.alpha3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/core/bootstrap-email.scss +2 -0
  4. data/core/bootstrap-head.scss +1 -1
  5. data/core/scss/_selectors_for_utils.scss +24 -0
  6. data/core/scss/_utilities.scss +24 -4
  7. data/core/scss/components/_grid.scss +12 -9
  8. data/core/scss/components/_stack.scss +33 -0
  9. data/core/scss/utilities/_sizing.scss +21 -2
  10. data/core/scss/utilities/_typography.scss +4 -10
  11. data/core/scss/utilities/_valign.scss +5 -0
  12. data/core/templates/body.html +9 -0
  13. data/core/templates/{container.html.erb → container.html} +4 -4
  14. data/core/templates/div.html +3 -0
  15. data/core/templates/table-left.html +9 -0
  16. data/core/templates/table-to-tbody.html +5 -0
  17. data/core/templates/table-to-tr.html +7 -0
  18. data/core/templates/table.html +9 -0
  19. data/core/templates/td.html +3 -0
  20. data/core/templates/tr.html +5 -0
  21. data/lib/{bootstrap_email.rb → bootstrap-email.rb} +6 -1
  22. data/lib/bootstrap-email/bootstrap_email_cli.rb +9 -9
  23. data/lib/bootstrap-email/compiler.rb +49 -65
  24. data/lib/bootstrap-email/config.rb +50 -0
  25. data/lib/bootstrap-email/{components → converters}/alert.rb +1 -1
  26. data/lib/bootstrap-email/converters/align.rb +22 -0
  27. data/lib/bootstrap-email/{components → converters}/badge.rb +1 -1
  28. data/lib/bootstrap-email/converters/base.rb +62 -0
  29. data/lib/bootstrap-email/converters/block.rb +13 -0
  30. data/lib/bootstrap-email/converters/body.rb +10 -0
  31. data/lib/bootstrap-email/{components → converters}/button.rb +1 -1
  32. data/lib/bootstrap-email/{components → converters}/card.rb +3 -3
  33. data/lib/bootstrap-email/{components → converters}/color.rb +1 -1
  34. data/lib/bootstrap-email/{components → converters}/container.rb +1 -1
  35. data/lib/bootstrap-email/converters/force_encoding.rb +16 -0
  36. data/lib/bootstrap-email/converters/grid.rb +14 -0
  37. data/lib/bootstrap-email/converters/head_style.rb +33 -0
  38. data/lib/bootstrap-email/{components → converters}/hr.rb +2 -2
  39. data/lib/bootstrap-email/{components → converters}/margin.rb +1 -1
  40. data/lib/bootstrap-email/{components → converters}/padding.rb +1 -1
  41. data/lib/bootstrap-email/converters/paragraph.rb +13 -0
  42. data/lib/bootstrap-email/converters/preview_text.rb +18 -0
  43. data/lib/bootstrap-email/{components → converters}/spacer.rb +1 -1
  44. data/lib/bootstrap-email/{components → converters}/spacing.rb +5 -6
  45. data/lib/bootstrap-email/converters/stack.rb +30 -0
  46. data/lib/bootstrap-email/{components → converters}/table.rb +1 -1
  47. data/lib/bootstrap-email/converters/version_comment.rb +15 -0
  48. data/lib/bootstrap-email/erb.rb +9 -0
  49. data/lib/bootstrap-email/rails/action_mailer.rb +7 -2
  50. data/lib/bootstrap-email/sass_cache.rb +43 -26
  51. data/lib/bootstrap-email/setup.rb +27 -0
  52. metadata +44 -31
  53. data/core/templates/body.html.erb +0 -9
  54. data/core/templates/col.html.erb +0 -3
  55. data/core/templates/div.html.erb +0 -3
  56. data/core/templates/row.html.erb +0 -7
  57. data/core/templates/table-left.html.erb +0 -9
  58. data/core/templates/table.html.erb +0 -9
  59. data/lib/bootstrap-email/components/align.rb +0 -21
  60. data/lib/bootstrap-email/components/base.rb +0 -26
  61. data/lib/bootstrap-email/components/body.rb +0 -22
  62. data/lib/bootstrap-email/components/grid.rb +0 -14
  63. data/lib/bootstrap-email/components/paragraph.rb +0 -24
@@ -0,0 +1,50 @@
1
+ module BootstrapEmail
2
+ class Config
3
+ attr_writer :sass_email_location # path to main sass file
4
+ attr_writer :sass_head_location # path to head sass file
5
+ attr_writer :sass_load_paths # array of directories for loading sass imports
6
+ attr_writer :sass_cache_location # path to tmp folder for sass cache
7
+ attr_writer :sass_log_enabled # turn on or off sass log when caching new sass
8
+
9
+ def load_options(options)
10
+ file = File.expand_path('bootstrap-email.config.rb', Dir.pwd)
11
+ if options[:config_path]
12
+ require_relative options[:config_path]
13
+ elsif File.exist?(file)
14
+ require_relative file
15
+ end
16
+ options.each { |name, value| instance_variable_set("@#{name}", value) }
17
+ end
18
+
19
+ def sass_location_for(type:)
20
+ ivar = instance_variable_get("@sass_#{type.sub('bootstrap-', '')}_location")
21
+ return ivar if ivar
22
+
23
+ lookup_locations = ["#{type}.scss", "app/assets/stylesheets/#{type}.scss"]
24
+ locations = lookup_locations.map { |location| File.expand_path(location, Dir.pwd) }.select { |location| File.exist?(location) }
25
+ locations.first if locations.any?
26
+ end
27
+
28
+ def sass_load_paths
29
+ paths_array = [SassCache::SASS_DIR]
30
+ @sass_load_paths ||= []
31
+ paths_array.concat(@sass_load_paths)
32
+ end
33
+
34
+ def sass_cache_location
35
+ @sass_cache_location ||= begin
36
+ if defined?(::Rails) && ::Rails.root
37
+ ::Rails.root.join('tmp', 'cache', 'bootstrap-email', '.sass-cache')
38
+ elsif File.writable?(Dir.pwd)
39
+ File.join(Dir.pwd, '.sass-cache', 'bootstrap-email')
40
+ else
41
+ File.join(Dir.tmpdir, '.sass-cache', 'bootstrap-email')
42
+ end
43
+ end
44
+ end
45
+
46
+ def sass_log_enabled?
47
+ defined?(@sass_log_enabled) ? @sass_log_enabled : true
48
+ end
49
+ end
50
+ end
@@ -1,5 +1,5 @@
1
1
  module BootstrapEmail
2
- module Component
2
+ module Converter
3
3
  class Alert < Base
4
4
  def build
5
5
  each_node('.alert') do |node|
@@ -0,0 +1,22 @@
1
+ module BootstrapEmail
2
+ module Converter
3
+ class Align < Base
4
+ def build
5
+ ['left', 'center', 'right'].each do |type|
6
+ full_type = "align-#{type}"
7
+ each_node(".#{full_type}") do |node|
8
+ align_helper(node, full_type, type)
9
+ end
10
+ end
11
+ end
12
+
13
+ def align_helper(node, full_type, type)
14
+ unless table?(node) || td?(node)
15
+ node['class'] = node['class'].sub(full_type, '')
16
+ node = node.replace(template('table', classes: full_type, contents: node.to_html))[0]
17
+ end
18
+ node['align'] = type
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,5 +1,5 @@
1
1
  module BootstrapEmail
2
- module Component
2
+ module Converter
3
3
  class Badge < Base
4
4
  def build
5
5
  each_node('.badge') do |node|
@@ -0,0 +1,62 @@
1
+ module BootstrapEmail
2
+ module Converter
3
+ class Base
4
+ attr_reader :doc
5
+ def initialize(doc)
6
+ @doc = doc
7
+ @cached_templates = {}
8
+ end
9
+
10
+ def self.build(doc)
11
+ new(doc).build
12
+ end
13
+
14
+ private
15
+
16
+ def template(file, locals_hash = {})
17
+ locals_hash[:classes] = locals_hash[:classes].split.join(' ') if locals_hash[:classes]
18
+ if @cached_templates[file]
19
+ string = @cached_templates[file]
20
+ else
21
+ path = File.expand_path("../../../core/templates/#{file}.html", __dir__)
22
+ string = File.read(path).chop # read and remove trailing newline
23
+ @cached_templates[file] = string
24
+ end
25
+ locals_hash.each do |key, value|
26
+ string = string.sub("{{ #{key} }}", value.to_s)
27
+ end
28
+ string
29
+ end
30
+
31
+ def each_node(css_lookup, &blk)
32
+ # sort by youngest child and traverse backwards up the tree
33
+ doc.css(css_lookup).sort_by { |n| n.ancestors.size }.reverse!.each(&blk)
34
+ end
35
+
36
+ def add_class(node, class_name)
37
+ node['class'] ||= ''
38
+ node['class'] += class_name
39
+ end
40
+
41
+ def margin?(node)
42
+ margin_top?(node) || margin_bottom?(node)
43
+ end
44
+
45
+ def margin_top?(node)
46
+ node['class'].to_s.match?(/m[ty]{1}-(lg-)?\d+/)
47
+ end
48
+
49
+ def margin_bottom?(node)
50
+ node['class'].to_s.match?(/m[by]{1}-(lg-)?\d+/)
51
+ end
52
+
53
+ def table?(node)
54
+ node.name == 'table'
55
+ end
56
+
57
+ def td?(node)
58
+ node.name == 'td'
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,13 @@
1
+ module BootstrapEmail
2
+ module Converter
3
+ class Block < Base
4
+ def build
5
+ each_node('block, .to-table') do |node|
6
+ # add .to-table if it's not already there
7
+ class_name = node['class'].to_s.split << 'to-table'
8
+ node.replace(template('table', classes: class_name.uniq.join(' '), contents: node.inner_html))
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+ module BootstrapEmail
2
+ module Converter
3
+ class Body < Base
4
+ def build
5
+ body = doc.at_css('body')
6
+ body.inner_html = template('body', classes: "#{body['class']} body", contents: body.inner_html)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -1,5 +1,5 @@
1
1
  module BootstrapEmail
2
- module Component
2
+ module Converter
3
3
  class Button < Base
4
4
  def build
5
5
  each_node('.btn') do |node|
@@ -1,12 +1,12 @@
1
1
  module BootstrapEmail
2
- module Component
2
+ module Converter
3
3
  class Card < Base
4
4
  def build
5
5
  each_node('.card') do |node|
6
- node.replace(template('table', classes: node['class'], contents: node.delete('class') && node.inner_html))
6
+ node.replace(template('table', classes: node['class'], contents: node.inner_html))
7
7
  end
8
8
  each_node('.card-body') do |node|
9
- node.replace(template('table', classes: node['class'], contents: node.delete('class') && node.inner_html))
9
+ node.replace(template('table', classes: node['class'], contents: node.inner_html))
10
10
  end
11
11
  end
12
12
  end
@@ -1,5 +1,5 @@
1
1
  module BootstrapEmail
2
- module Component
2
+ module Converter
3
3
  class Color < Base
4
4
  def build
5
5
  each_node('*[class*=bg-]') do |node|
@@ -1,5 +1,5 @@
1
1
  module BootstrapEmail
2
- module Component
2
+ module Converter
3
3
  class Container < Base
4
4
  def build
5
5
  each_node('.container') do |node|
@@ -0,0 +1,16 @@
1
+ module BootstrapEmail
2
+ module Converter
3
+ class ForceEncoding < Base
4
+ def build
5
+ body = doc.at_css('body')
6
+ body.add_child('<force-encoding></force-encoding>')
7
+ end
8
+
9
+ def self.replace(html)
10
+ # force utf-8 character encoded in iOS Mail: https://github.com/bootstrap-email/bootstrap-email/issues/50
11
+ # this needs to be done after the document has been outputted to a string so it doesn't get converted
12
+ html.sub('<force-encoding></force-encoding>', '<div id="force-encoding-to-utf-8" style="display: none;">&#10175;</div>')
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ module BootstrapEmail
2
+ module Converter
3
+ class Grid < Base
4
+ def build
5
+ each_node('.row') do |node|
6
+ node.replace(template('table-to-tr', classes: node['class'], contents: node.inner_html))
7
+ end
8
+ each_node('*[class*=col]') do |node|
9
+ node.replace(template('td', classes: node['class'], contents: node.inner_html))
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,33 @@
1
+ module BootstrapEmail
2
+ module Converter
3
+ class HeadStyle < Base
4
+ def build
5
+ doc.at_css('head').add_child(bootstrap_email_head)
6
+ end
7
+
8
+ private
9
+
10
+ def bootstrap_email_head
11
+ html_string = <<-INLINE
12
+ <style type="text/css">
13
+ #{purged_css_from_head}
14
+ </style>
15
+ INLINE
16
+ html_string
17
+ end
18
+
19
+ def purged_css_from_head
20
+ default, custom = BootstrapEmail::SassCache.compile('bootstrap-head').split('/*! allow_purge_after */')
21
+ # get each CSS declaration
22
+ custom.scan(/\w*\.[\w\-]*[\s\S\n]+?(?=})}{1}/).each do |group|
23
+ # get the first class for each comma separated CSS declaration
24
+ exist = group.scan(/(\.[\w\-]*).*?((,+?)|{+?)/).map(&:first).uniq.any? do |selector|
25
+ !doc.at_css(selector).nil?
26
+ end
27
+ custom.sub!(group, '') unless exist
28
+ end
29
+ (default + custom).gsub(/\n\s*\n+/, "\n")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,9 +1,9 @@
1
1
  module BootstrapEmail
2
- module Component
2
+ module Converter
3
3
  class Hr < Base
4
4
  def build
5
5
  each_node('hr') do |node|
6
- default_margin = node['class'].to_s.match?(/m[tby]{1}-(lg-)?\d+/) ? '' : 'my-5'
6
+ default_margin = margin?(node) ? '' : 'my-5'
7
7
  node.replace(template('table', classes: "#{default_margin} hr #{node['class']}", contents: ''))
8
8
  end
9
9
  end
@@ -1,5 +1,5 @@
1
1
  module BootstrapEmail
2
- module Component
2
+ module Converter
3
3
  class Margin < Base
4
4
  def build
5
5
  each_node('*[class*=my-], *[class*=mt-], *[class*=mb-]') do |node|
@@ -1,5 +1,5 @@
1
1
  module BootstrapEmail
2
- module Component
2
+ module Converter
3
3
  class Padding < Base
4
4
  def build
5
5
  each_node('*[class*=p-], *[class*=pt-], *[class*=pr-], *[class*=pb-], *[class*=pl-], *[class*=px-], *[class*=py-]') do |node|
@@ -0,0 +1,13 @@
1
+ module BootstrapEmail
2
+ module Converter
3
+ class Paragraph < Base
4
+ def build
5
+ each_node('p') do |node|
6
+ next if margin?(node)
7
+
8
+ add_class(node, 'mb-4')
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ module BootstrapEmail
2
+ module Converter
3
+ class PreviewText < Base
4
+ def build
5
+ preview_node = doc.at_css('preview')
6
+ return if preview_node.nil?
7
+
8
+ # apply spacing after the text max of 100 characters so it doesn't show body text
9
+ preview_node.inner_html += '&nbsp;' * [(100 - preview_node.content.length.to_i), 0].max
10
+ node = template('div', classes: 'preview', contents: preview_node.content)
11
+ preview_node.remove
12
+
13
+ body = doc.at_css('body')
14
+ body.prepend_child(node)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,5 +1,5 @@
1
1
  module BootstrapEmail
2
- module Component
2
+ module Converter
3
3
  class Spacer < Base
4
4
  def build
5
5
  each_node('*[class*=s-]') do |node|
@@ -1,15 +1,14 @@
1
1
  module BootstrapEmail
2
- module Component
2
+ module Converter
3
3
  class Spacing < Base
4
4
  def build
5
5
  each_node('*[class*=space-y-]') do |node|
6
6
  spacer = node['class'].scan(/space-y-((lg-)?\d+)/)[0][0]
7
7
  # get all direct children except the first
8
- node.xpath('./*[position()>1] | ./tbody/tr/td/*[position()>1]').each do |child|
9
- html = ''
10
- html += template('div', classes: "s-#{spacer}", contents: nil)
11
- html += child.to_html
12
- child.replace(html)
8
+ node.xpath('./*[position() < last()] | ./tbody/tr/td/*[position() < last()]').each do |child|
9
+ next if margin_bottom?(child)
10
+
11
+ add_class(child, "mb-#{spacer}")
13
12
  end
14
13
  end
15
14
  end
@@ -0,0 +1,30 @@
1
+ module BootstrapEmail
2
+ module Converter
3
+ class Stack < Base
4
+ def build
5
+ stack_x
6
+ stack_y
7
+ end
8
+
9
+ def stack_x
10
+ each_node('.stack-x') do |node|
11
+ html = ''
12
+ node.xpath('./*').each do |child|
13
+ html += template('td', classes: 'stack-cell', contents: child.to_html)
14
+ end
15
+ node.replace(template('table-to-tr', classes: node['class'], contents: html))
16
+ end
17
+ end
18
+
19
+ def stack_y
20
+ each_node('.stack-y') do |node|
21
+ html = ''
22
+ node.xpath('./*').each do |child|
23
+ html += template('tr', classes: 'stack-cell', contents: child.to_html)
24
+ end
25
+ node.replace(template('table-to-tbody', classes: node['class'], contents: html))
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,5 +1,5 @@
1
1
  module BootstrapEmail
2
- module Component
2
+ module Converter
3
3
  class Table < Base
4
4
  def build
5
5
  each_node('table') do |node|
@@ -0,0 +1,15 @@
1
+ module BootstrapEmail
2
+ module Converter
3
+ class VersionComment < Base
4
+ def build
5
+ doc.at_css('head').prepend_child(bootstrap_email_comment)
6
+ end
7
+
8
+ private
9
+
10
+ def bootstrap_email_comment
11
+ "\n <!-- Compiled with Bootstrap Email version: #{BootstrapEmail::VERSION} -->"
12
+ end
13
+ end
14
+ end
15
+ end