bootstrap-email 1.0.0.alpha1 → 1.0.0.alpha3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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