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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/bin/bootstrap-email +2 -0
- data/core/bootstrap-email.scss +29 -18
- data/core/bootstrap-head.scss +143 -0
- data/core/layout.html.erb +11 -0
- data/core/scss/_colors.scss +161 -0
- data/core/scss/_functions.scss +29 -0
- data/core/scss/_helper_groups.scss +21 -0
- data/core/{sass → scss}/_reboot_email.scss +7 -22
- data/core/scss/_selectors_for_utils.scss +124 -0
- data/core/scss/_utilities.scss +145 -0
- data/core/scss/_variables.scss +52 -0
- data/core/{sass → scss/components}/_alert.scss +0 -1
- data/core/{sass → scss/components}/_badge.scss +8 -15
- data/core/{sass → scss/components}/_button.scss +21 -17
- data/core/{sass → scss/components}/_card.scss +3 -2
- data/core/{sass → scss/components}/_container.scss +0 -0
- data/core/scss/components/_grid.scss +38 -0
- data/core/scss/components/_hr.scss +8 -0
- data/core/{sass → scss/components}/_image.scss +2 -1
- data/core/{sass → scss/components}/_preview.scss +0 -0
- data/core/scss/components/_stack.scss +33 -0
- data/core/{sass → scss/components}/_table.scss +0 -0
- data/core/scss/utilities/_background.scss +5 -0
- data/core/scss/utilities/_border-radius.scss +21 -0
- data/core/scss/utilities/_border.scss +13 -0
- data/core/scss/utilities/_color.scss +9 -0
- data/core/scss/utilities/_display.scss +7 -0
- data/core/scss/utilities/_sizing.scss +31 -0
- data/core/scss/utilities/_spacing.scss +18 -0
- data/core/scss/utilities/_text-decoration.scss +9 -0
- data/core/scss/utilities/_typography.scss +48 -0
- data/core/scss/utilities/_valign.scss +5 -0
- data/core/templates/body.html.erb +1 -1
- data/core/templates/container.html.erb +3 -3
- data/core/templates/table-left.html.erb +1 -1
- data/core/templates/table-to-tbody.html.erb +5 -0
- data/core/templates/table-to-tr.html.erb +7 -0
- data/core/templates/table.html.erb +1 -1
- data/core/templates/td.html.erb +3 -0
- data/core/templates/tr.html.erb +5 -0
- data/lib/bootstrap-email.rb +20 -222
- data/lib/bootstrap-email/bootstrap_email_cli.rb +92 -0
- data/lib/bootstrap-email/compiler.rb +98 -0
- data/lib/bootstrap-email/components/alert.rb +11 -0
- data/lib/bootstrap-email/components/align.rb +22 -0
- data/lib/bootstrap-email/components/badge.rb +11 -0
- data/lib/bootstrap-email/components/base.rb +54 -0
- data/lib/bootstrap-email/components/block.rb +13 -0
- data/lib/bootstrap-email/components/body.rb +10 -0
- data/lib/bootstrap-email/components/button.rb +11 -0
- data/lib/bootstrap-email/components/card.rb +14 -0
- data/lib/bootstrap-email/components/color.rb +13 -0
- data/lib/bootstrap-email/components/container.rb +14 -0
- data/lib/bootstrap-email/components/force_encoding.rb +16 -0
- data/lib/bootstrap-email/components/grid.rb +14 -0
- data/lib/bootstrap-email/components/head_style.rb +33 -0
- data/lib/bootstrap-email/components/hr.rb +12 -0
- data/lib/bootstrap-email/components/margin.rb +22 -0
- data/lib/bootstrap-email/components/padding.rb +16 -0
- data/lib/bootstrap-email/components/paragraph.rb +13 -0
- data/lib/bootstrap-email/components/preview_text.rb +18 -0
- data/lib/bootstrap-email/components/spacer.rb +11 -0
- data/lib/bootstrap-email/components/spacing.rb +17 -0
- data/lib/bootstrap-email/components/stack.rb +30 -0
- data/lib/bootstrap-email/components/table.rb +13 -0
- data/lib/bootstrap-email/components/version_comment.rb +15 -0
- data/lib/bootstrap-email/erb.rb +9 -0
- data/lib/bootstrap-email/initialize.rb +1 -0
- data/lib/bootstrap-email/rails/action_mailer.rb +11 -0
- data/lib/bootstrap-email/{engine.rb → rails/engine.rb} +0 -0
- data/lib/bootstrap-email/sass_cache.rb +67 -0
- data/lib/bootstrap-email/version.rb +3 -5
- metadata +82 -59
- data/core/head.scss +0 -269
- data/core/sass/_border.scss +0 -73
- data/core/sass/_color.scss +0 -33
- data/core/sass/_display.scss +0 -7
- data/core/sass/_functions.scss +0 -14
- data/core/sass/_grid.scss +0 -131
- data/core/sass/_hr.scss +0 -13
- data/core/sass/_spacing.scss +0 -100
- data/core/sass/_typography.scss +0 -50
- data/core/sass/_variables.scss +0 -150
- data/core/template.html.erb +0 -11
- data/core/templates/align-center.html.erb +0 -9
- data/core/templates/align-left.html.erb +0 -9
- data/core/templates/align-right.html.erb +0 -9
- data/core/templates/col.html.erb +0 -3
- data/core/templates/hr.html.erb +0 -9
- data/core/templates/row.html.erb +0 -7
- data/lib/assets/stylesheets/bootstrap-email.scss +0 -1
- data/lib/bootstrap-email/action_mailer.rb +0 -12
- 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,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,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,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;">➿</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
|