bootstrap-email 0.3.3 → 1.0.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/bin/bootstrap-email +2 -0
  4. data/core/bootstrap-email.scss +29 -18
  5. data/core/bootstrap-head.scss +143 -0
  6. data/core/layout.html.erb +11 -0
  7. data/core/scss/_colors.scss +161 -0
  8. data/core/scss/_functions.scss +29 -0
  9. data/core/scss/_helper_groups.scss +21 -0
  10. data/core/{sass → scss}/_reboot_email.scss +7 -22
  11. data/core/scss/_selectors_for_utils.scss +124 -0
  12. data/core/scss/_utilities.scss +145 -0
  13. data/core/scss/_variables.scss +52 -0
  14. data/core/{sass → scss/components}/_alert.scss +0 -1
  15. data/core/{sass → scss/components}/_badge.scss +8 -15
  16. data/core/{sass → scss/components}/_button.scss +21 -17
  17. data/core/{sass → scss/components}/_card.scss +3 -2
  18. data/core/{sass → scss/components}/_container.scss +0 -0
  19. data/core/scss/components/_grid.scss +38 -0
  20. data/core/scss/components/_hr.scss +8 -0
  21. data/core/{sass → scss/components}/_image.scss +2 -1
  22. data/core/{sass → scss/components}/_preview.scss +0 -0
  23. data/core/scss/components/_stack.scss +33 -0
  24. data/core/{sass → scss/components}/_table.scss +0 -0
  25. data/core/scss/utilities/_background.scss +5 -0
  26. data/core/scss/utilities/_border-radius.scss +21 -0
  27. data/core/scss/utilities/_border.scss +13 -0
  28. data/core/scss/utilities/_color.scss +9 -0
  29. data/core/scss/utilities/_display.scss +7 -0
  30. data/core/scss/utilities/_sizing.scss +31 -0
  31. data/core/scss/utilities/_spacing.scss +18 -0
  32. data/core/scss/utilities/_text-decoration.scss +9 -0
  33. data/core/scss/utilities/_typography.scss +48 -0
  34. data/core/scss/utilities/_valign.scss +5 -0
  35. data/core/templates/body.html.erb +1 -1
  36. data/core/templates/container.html.erb +3 -3
  37. data/core/templates/table-left.html.erb +1 -1
  38. data/core/templates/table-to-tbody.html.erb +5 -0
  39. data/core/templates/table-to-tr.html.erb +7 -0
  40. data/core/templates/table.html.erb +1 -1
  41. data/core/templates/td.html.erb +3 -0
  42. data/core/templates/tr.html.erb +5 -0
  43. data/lib/bootstrap-email.rb +20 -222
  44. data/lib/bootstrap-email/bootstrap_email_cli.rb +92 -0
  45. data/lib/bootstrap-email/compiler.rb +98 -0
  46. data/lib/bootstrap-email/components/alert.rb +11 -0
  47. data/lib/bootstrap-email/components/align.rb +22 -0
  48. data/lib/bootstrap-email/components/badge.rb +11 -0
  49. data/lib/bootstrap-email/components/base.rb +54 -0
  50. data/lib/bootstrap-email/components/block.rb +13 -0
  51. data/lib/bootstrap-email/components/body.rb +10 -0
  52. data/lib/bootstrap-email/components/button.rb +11 -0
  53. data/lib/bootstrap-email/components/card.rb +14 -0
  54. data/lib/bootstrap-email/components/color.rb +13 -0
  55. data/lib/bootstrap-email/components/container.rb +14 -0
  56. data/lib/bootstrap-email/components/force_encoding.rb +16 -0
  57. data/lib/bootstrap-email/components/grid.rb +14 -0
  58. data/lib/bootstrap-email/components/head_style.rb +33 -0
  59. data/lib/bootstrap-email/components/hr.rb +12 -0
  60. data/lib/bootstrap-email/components/margin.rb +22 -0
  61. data/lib/bootstrap-email/components/padding.rb +16 -0
  62. data/lib/bootstrap-email/components/paragraph.rb +13 -0
  63. data/lib/bootstrap-email/components/preview_text.rb +18 -0
  64. data/lib/bootstrap-email/components/spacer.rb +11 -0
  65. data/lib/bootstrap-email/components/spacing.rb +17 -0
  66. data/lib/bootstrap-email/components/stack.rb +30 -0
  67. data/lib/bootstrap-email/components/table.rb +13 -0
  68. data/lib/bootstrap-email/components/version_comment.rb +15 -0
  69. data/lib/bootstrap-email/erb.rb +9 -0
  70. data/lib/bootstrap-email/initialize.rb +1 -0
  71. data/lib/bootstrap-email/rails/action_mailer.rb +11 -0
  72. data/lib/bootstrap-email/{engine.rb → rails/engine.rb} +0 -0
  73. data/lib/bootstrap-email/sass_cache.rb +67 -0
  74. data/lib/bootstrap-email/version.rb +3 -5
  75. metadata +82 -59
  76. data/core/head.scss +0 -269
  77. data/core/sass/_border.scss +0 -73
  78. data/core/sass/_color.scss +0 -33
  79. data/core/sass/_display.scss +0 -7
  80. data/core/sass/_functions.scss +0 -14
  81. data/core/sass/_grid.scss +0 -131
  82. data/core/sass/_hr.scss +0 -13
  83. data/core/sass/_spacing.scss +0 -100
  84. data/core/sass/_typography.scss +0 -50
  85. data/core/sass/_variables.scss +0 -150
  86. data/core/template.html.erb +0 -11
  87. data/core/templates/align-center.html.erb +0 -9
  88. data/core/templates/align-left.html.erb +0 -9
  89. data/core/templates/align-right.html.erb +0 -9
  90. data/core/templates/col.html.erb +0 -3
  91. data/core/templates/hr.html.erb +0 -9
  92. data/core/templates/row.html.erb +0 -7
  93. data/lib/assets/stylesheets/bootstrap-email.scss +0 -1
  94. data/lib/bootstrap-email/action_mailer.rb +0 -12
  95. data/lib/bootstrap-email/premailer_railtie.rb +0 -5
@@ -0,0 +1,92 @@
1
+ require_relative '../bootstrap-email'
2
+ require 'optparse'
3
+ require 'fileutils'
4
+
5
+ input = nil
6
+ options = {
7
+ destination: 'compiled',
8
+ type: :file
9
+ }
10
+
11
+ parser = OptionParser.new do |opts|
12
+ opts.banner = "Bootstrap 5 stylesheet, compiler, and inliner for responsive and consistent emails with the Bootstrap syntax you know and love.\n\n"
13
+ opts.define_head 'Usage: bootstrap-email <path> [options]'
14
+ opts.separator ''
15
+ opts.separator 'Examples:'
16
+ opts.separator ' bootstrap-email'
17
+ opts.separator ' bootstrap-email email.html > out.html'
18
+ opts.separator ' bootstrap-email ./public/index.html'
19
+ opts.separator ' bootstrap-email -s \'<a href="#" class="btn btn-primary">Some Button</a>\''
20
+ opts.separator ' bootstrap-email -p \'emails/*\' -d emails/compiled'
21
+ opts.separator ' bootstrap-email -p \'views/emails/*\' -d \'views/compiled_emails\''
22
+ opts.separator ' cat input.html | bootstrap-email'
23
+ opts.separator ''
24
+ opts.separator 'Options:'
25
+
26
+ opts.on('-s', '--string STRING', String, 'HTML string to be to be compiled rather than a file.') do |v|
27
+ input = v
28
+ options[:type] = :string
29
+ end
30
+
31
+ opts.on('-p', '--pattern STRING', String, 'Specify a pattern of files to compile and save multiple files at once.') do |v|
32
+ input = v
33
+ options[:type] = :pattern
34
+ end
35
+
36
+ opts.on('-d', '--destination STRING', String, 'Destination for compiled files (used with the --pattern option).') do |v|
37
+ options[:destination] = v
38
+ end
39
+
40
+ opts.on('-c', '--config STRING', String, 'Relative path to SCSS config config file to customize Bootstrap Email.') do |v|
41
+ options[:config] = File.expand_path(v, Dir.pwd)
42
+ end
43
+
44
+ opts.on('-h', '--help', 'Prints this help') do
45
+ puts opts
46
+ exit
47
+ end
48
+
49
+ opts.on('-v', '--version', 'Show version') do
50
+ puts BootstrapEmail::VERSION
51
+ exit
52
+ end
53
+ end
54
+ parser.parse!
55
+
56
+ if input
57
+ # input already set by pattern
58
+ elsif ARGV.any?
59
+ # Executed via command line or shell script
60
+ input = ARGV.shift
61
+ elsif !STDIN.tty?
62
+ # Called in piped command
63
+ input = STDIN.read
64
+ options[:type] = :string
65
+ else
66
+ # Running just the blank command to compile all files in directory containing .html
67
+ input = '*.html*'
68
+ options[:type] = :pattern
69
+ end
70
+
71
+ if input
72
+ case options[:type]
73
+ when :pattern
74
+ Dir.glob(input, base: Dir.pwd).each do |path|
75
+ next unless File.file?(path)
76
+
77
+ puts "Compiling file #{path}"
78
+ compiled = BootstrapEmail::Compiler.new(path, type: :file, options: {config_path: options[:config]}).perform_full_compile
79
+ destination = options[:destination].chomp('/*')
80
+ FileUtils.mkdir_p("#{Dir.pwd}/#{destination}")
81
+ File.write(File.expand_path("#{destination}/#{path.split('/').last}", Dir.pwd), compiled)
82
+ end
83
+ when :file
84
+ path = File.expand_path(input, Dir.pwd)
85
+ puts BootstrapEmail::Compiler.new(path, type: :file, options: {config_path: options[:config]}).perform_full_compile
86
+ when :string
87
+ puts BootstrapEmail::Compiler.new(input, options: {config_path: options[:config]}).perform_full_compile
88
+ end
89
+ else
90
+ puts opts
91
+ exit 1
92
+ end
@@ -0,0 +1,98 @@
1
+ module BootstrapEmail
2
+ class Compiler
3
+ attr_accessor :type, :doc, :premailer
4
+
5
+ def initialize(input, type: :string, options: {})
6
+ self.type = type
7
+ case type
8
+ when :rails
9
+ @mail = input
10
+ html = (@mail.html_part || @mail).body.raw_source
11
+ when :string
12
+ html = input
13
+ when :file
14
+ html = File.read(input)
15
+ end
16
+ html = add_layout!(html)
17
+ build_premailer_doc(html, options)
18
+ end
19
+
20
+ def perform_full_compile
21
+ compile_html!
22
+ inline_css!
23
+ configure_html!
24
+ finalize_document!
25
+ end
26
+
27
+ private
28
+
29
+ def add_layout!(html)
30
+ document = Nokogiri::HTML(html)
31
+ return html unless document.at_css('head').nil?
32
+
33
+ BootstrapEmail::Erb.template(
34
+ File.expand_path('../../core/layout.html.erb', __dir__),
35
+ contents: html
36
+ )
37
+ end
38
+
39
+ def build_premailer_doc(html, options)
40
+ SassC.load_paths << File.expand_path('../../core', __dir__)
41
+ css_string = BootstrapEmail::SassCache.compile('bootstrap-email', config_path: options[:config_path], style: :expanded)
42
+ self.premailer = Premailer.new(
43
+ html,
44
+ with_html_string: true,
45
+ preserve_reset: false,
46
+ css_string: css_string
47
+ )
48
+ self.doc = premailer.doc
49
+ end
50
+
51
+ def compile_html!
52
+ BootstrapEmail::Component::Block.build(doc)
53
+
54
+ BootstrapEmail::Component::Button.build(doc)
55
+ BootstrapEmail::Component::Badge.build(doc)
56
+ BootstrapEmail::Component::Alert.build(doc)
57
+ BootstrapEmail::Component::Card.build(doc)
58
+ # BootstrapEmail::Component::Paragraph.build(doc) this might be too much
59
+ BootstrapEmail::Component::Hr.build(doc)
60
+ BootstrapEmail::Component::Container.build(doc)
61
+ BootstrapEmail::Component::Grid.build(doc)
62
+ BootstrapEmail::Component::Stack.build(doc)
63
+
64
+ BootstrapEmail::Component::Spacing.build(doc)
65
+ BootstrapEmail::Component::Padding.build(doc)
66
+ BootstrapEmail::Component::Margin.build(doc)
67
+ BootstrapEmail::Component::Spacer.build(doc)
68
+
69
+ BootstrapEmail::Component::Table.build(doc)
70
+ BootstrapEmail::Component::Body.build(doc)
71
+ BootstrapEmail::Component::Align.build(doc)
72
+ BootstrapEmail::Component::Color.build(doc)
73
+
74
+ BootstrapEmail::Component::PreviewText.build(doc)
75
+ end
76
+
77
+ def inline_css!
78
+ premailer.to_inline_css
79
+ end
80
+
81
+ def configure_html!
82
+ BootstrapEmail::Component::ForceEncoding.build(doc)
83
+ BootstrapEmail::Component::HeadStyle.build(doc)
84
+ BootstrapEmail::Component::VersionComment.build(doc)
85
+ end
86
+
87
+ def finalize_document!
88
+ html = BootstrapEmail::Component::ForceEncoding.replace(doc.to_html)
89
+ case type
90
+ when :rails
91
+ (@mail.html_part || @mail).body = html
92
+ @mail
93
+ when :string, :file
94
+ html
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,11 @@
1
+ module BootstrapEmail
2
+ module Component
3
+ class Alert < Base
4
+ def build
5
+ each_node('.alert') do |node|
6
+ node.replace(template('table', classes: node['class'], contents: node.delete('class') && node.to_html))
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ module BootstrapEmail
2
+ module Component
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
@@ -0,0 +1,11 @@
1
+ module BootstrapEmail
2
+ module Component
3
+ class Badge < Base
4
+ def build
5
+ each_node('.badge') do |node|
6
+ node.replace(template('table-left', classes: node['class'], contents: node.delete('class') && node.to_html))
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,54 @@
1
+ module BootstrapEmail
2
+ module Component
3
+ class Base
4
+ attr_reader :doc
5
+ def initialize(doc)
6
+ @doc = doc
7
+ end
8
+
9
+ def self.build(doc)
10
+ new(doc).build
11
+ end
12
+
13
+ private
14
+
15
+ def template(file, locals_hash = {})
16
+ locals_hash[:classes] = locals_hash[:classes].split.join(' ') if locals_hash[:classes]
17
+ BootstrapEmail::Erb.template(
18
+ File.expand_path("../../../core/templates/#{file}.html.erb", __dir__),
19
+ locals_hash
20
+ )
21
+ end
22
+
23
+ def each_node(css_lookup, &blk)
24
+ # sort by youngest child and traverse backwards up the tree
25
+ doc.css(css_lookup).sort_by { |n| n.ancestors.size }.reverse!.each(&blk)
26
+ end
27
+
28
+ def add_class(node, class_name)
29
+ node['class'] ||= ''
30
+ node['class'] += class_name
31
+ end
32
+
33
+ def margin?(node)
34
+ margin_top?(node) || margin_bottom?(node)
35
+ end
36
+
37
+ def margin_top?(node)
38
+ node['class'].to_s.match?(/m[ty]{1}-(lg-)?\d+/)
39
+ end
40
+
41
+ def margin_bottom?(node)
42
+ node['class'].to_s.match?(/m[by]{1}-(lg-)?\d+/)
43
+ end
44
+
45
+ def table?(node)
46
+ node.name == 'table'
47
+ end
48
+
49
+ def td?(node)
50
+ node.name == 'td'
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,13 @@
1
+ module BootstrapEmail
2
+ module Component
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 Component
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
@@ -0,0 +1,11 @@
1
+ module BootstrapEmail
2
+ module Component
3
+ class Button < Base
4
+ def build
5
+ each_node('.btn') do |node|
6
+ node.replace(template('table', classes: node['class'], contents: node.delete('class') && node.to_html))
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ module BootstrapEmail
2
+ module Component
3
+ class Card < Base
4
+ def build
5
+ each_node('.card') do |node|
6
+ node.replace(template('table', classes: node['class'], contents: node.inner_html))
7
+ end
8
+ each_node('.card-body') do |node|
9
+ node.replace(template('table', classes: node['class'], contents: node.inner_html))
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module BootstrapEmail
2
+ module Component
3
+ class Color < Base
4
+ def build
5
+ each_node('*[class*=bg-]') do |node|
6
+ next unless ['div'].include?(node.name) # only do automatic thing for div
7
+
8
+ node.replace(template('table', classes: "#{node['class']} w-full", contents: node.delete('class') && node.inner_html))
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module BootstrapEmail
2
+ module Component
3
+ class Container < Base
4
+ def build
5
+ each_node('.container') do |node|
6
+ node.replace(template('container', classes: node['class'], contents: node.inner_html))
7
+ end
8
+ each_node('.container-fluid') do |node|
9
+ node.replace(template('table', classes: node['class'], contents: node.inner_html))
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ module BootstrapEmail
2
+ module Component
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 Component
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 Component
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