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