bootstrap-email 0.3.4 → 1.0.0.alpha2.1

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 (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 +15 -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 -14
  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