inkcite 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +110 -0
- data/Rakefile +8 -0
- data/assets/facebook-like.css +62 -0
- data/assets/facebook-like.js +59 -0
- data/assets/init/config.yml +97 -0
- data/assets/init/helpers.tsv +31 -0
- data/assets/init/source.html +60 -0
- data/assets/init/source.txt +6 -0
- data/bin/inkcite +6 -0
- data/inkcite.gemspec +42 -0
- data/lib/inkcite.rb +32 -0
- data/lib/inkcite/cli/base.rb +128 -0
- data/lib/inkcite/cli/build.rb +130 -0
- data/lib/inkcite/cli/init.rb +58 -0
- data/lib/inkcite/cli/preview.rb +30 -0
- data/lib/inkcite/cli/server.rb +123 -0
- data/lib/inkcite/cli/test.rb +61 -0
- data/lib/inkcite/email.rb +219 -0
- data/lib/inkcite/mailer.rb +140 -0
- data/lib/inkcite/minifier.rb +151 -0
- data/lib/inkcite/parser.rb +111 -0
- data/lib/inkcite/renderer.rb +177 -0
- data/lib/inkcite/renderer/base.rb +186 -0
- data/lib/inkcite/renderer/button.rb +168 -0
- data/lib/inkcite/renderer/div.rb +29 -0
- data/lib/inkcite/renderer/element.rb +82 -0
- data/lib/inkcite/renderer/footnote.rb +132 -0
- data/lib/inkcite/renderer/google_analytics.rb +35 -0
- data/lib/inkcite/renderer/image.rb +95 -0
- data/lib/inkcite/renderer/image_base.rb +82 -0
- data/lib/inkcite/renderer/in_browser.rb +38 -0
- data/lib/inkcite/renderer/like.rb +73 -0
- data/lib/inkcite/renderer/link.rb +243 -0
- data/lib/inkcite/renderer/litmus.rb +33 -0
- data/lib/inkcite/renderer/lorem.rb +39 -0
- data/lib/inkcite/renderer/mobile_image.rb +67 -0
- data/lib/inkcite/renderer/mobile_style.rb +40 -0
- data/lib/inkcite/renderer/mobile_toggle.rb +27 -0
- data/lib/inkcite/renderer/outlook_background.rb +48 -0
- data/lib/inkcite/renderer/partial.rb +31 -0
- data/lib/inkcite/renderer/preheader.rb +22 -0
- data/lib/inkcite/renderer/property.rb +39 -0
- data/lib/inkcite/renderer/responsive.rb +334 -0
- data/lib/inkcite/renderer/span.rb +21 -0
- data/lib/inkcite/renderer/table.rb +67 -0
- data/lib/inkcite/renderer/table_base.rb +149 -0
- data/lib/inkcite/renderer/td.rb +92 -0
- data/lib/inkcite/uploader.rb +173 -0
- data/lib/inkcite/util.rb +85 -0
- data/lib/inkcite/version.rb +3 -0
- data/lib/inkcite/view.rb +745 -0
- data/lib/inkcite/view/context.rb +38 -0
- data/lib/inkcite/view/media_query.rb +60 -0
- data/lib/inkcite/view/tag_stack.rb +38 -0
- data/test/email_spec.rb +16 -0
- data/test/parser_spec.rb +72 -0
- data/test/project/config.yml +98 -0
- data/test/project/helpers.tsv +56 -0
- data/test/project/images/inkcite.jpg +0 -0
- data/test/project/source.html +58 -0
- data/test/project/source.txt +6 -0
- data/test/renderer/button_spec.rb +45 -0
- data/test/renderer/div_spec.rb +101 -0
- data/test/renderer/element_spec.rb +31 -0
- data/test/renderer/footnote_spec.rb +57 -0
- data/test/renderer/image_spec.rb +82 -0
- data/test/renderer/link_spec.rb +84 -0
- data/test/renderer/mobile_image_spec.rb +27 -0
- data/test/renderer/mobile_style_spec.rb +37 -0
- data/test/renderer/td_spec.rb +126 -0
- data/test/renderer_spec.rb +28 -0
- data/test/view_spec.rb +15 -0
- metadata +333 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
module Inkcite
|
2
|
+
class View
|
3
|
+
|
4
|
+
# Private class used to convey view attributes to the Erubis rendering
|
5
|
+
# engine without exposing all of the view's attributes.
|
6
|
+
class Context
|
7
|
+
|
8
|
+
delegate :browser?, :development?, :email?, :environment, :format, :production?, :preview?, :version, :to => :view
|
9
|
+
|
10
|
+
def initialize view
|
11
|
+
@view = view
|
12
|
+
end
|
13
|
+
|
14
|
+
def method_missing(m, *args, &block)
|
15
|
+
if m[-1] == QUESTION_MARK
|
16
|
+
start_at = m[0] == UNDERSCORE ? 1 : 0
|
17
|
+
symbol = m[start_at, m.length - (start_at + 1)].to_sym
|
18
|
+
|
19
|
+
@view.version == symbol
|
20
|
+
else
|
21
|
+
super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def view
|
28
|
+
@view
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
UNDERSCORE = '_'
|
34
|
+
QUESTION_MARK = '?'
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Inkcite
|
2
|
+
class View
|
3
|
+
class MediaQuery
|
4
|
+
|
5
|
+
def initialize view, max_width
|
6
|
+
|
7
|
+
@view = view
|
8
|
+
@max_width = max_width
|
9
|
+
|
10
|
+
# Initialize the responsive styles used in this email. This will hold
|
11
|
+
# an array of Responsive::Rule objects.
|
12
|
+
@responsive_styles = Renderer::Responsive.presets(view)
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
def << rule
|
17
|
+
|
18
|
+
# Rules only get added once
|
19
|
+
@responsive_styles << rule unless @responsive_styles.include?(rule)
|
20
|
+
|
21
|
+
rule
|
22
|
+
end
|
23
|
+
|
24
|
+
def active_styles
|
25
|
+
@responsive_styles.select(&:active?)
|
26
|
+
end
|
27
|
+
|
28
|
+
def blank?
|
29
|
+
@responsive_styles.none?(&:active?)
|
30
|
+
end
|
31
|
+
|
32
|
+
def find_by_declaration declarations
|
33
|
+
@responsive_styles.detect { |r| r.declarations == declarations }
|
34
|
+
end
|
35
|
+
|
36
|
+
def find_by_klass klass
|
37
|
+
@responsive_styles.detect { |r| r.klass == klass }
|
38
|
+
end
|
39
|
+
|
40
|
+
def find_by_tag_and_klass tag, klass
|
41
|
+
@responsive_styles.detect { |r| r.klass == klass && r.include?(tag) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_a
|
45
|
+
|
46
|
+
css = []
|
47
|
+
css << "@media only screen and (max-width: #{Inkcite::Renderer::px(@max_width)}) {"
|
48
|
+
css += active_styles.collect(&:to_css)
|
49
|
+
css << "}"
|
50
|
+
|
51
|
+
css
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_css
|
55
|
+
to_a.join("\n")
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Inkcite
|
2
|
+
class View
|
3
|
+
class TagStack
|
4
|
+
|
5
|
+
attr_reader :tag
|
6
|
+
|
7
|
+
delegate :empty?, :length, :to => :opts
|
8
|
+
|
9
|
+
def initialize tag, ctx
|
10
|
+
@tag = tag
|
11
|
+
@ctx = ctx
|
12
|
+
@opts = []
|
13
|
+
end
|
14
|
+
|
15
|
+
# Pushes a new set of options onto the stack for this tag.
|
16
|
+
def << opt
|
17
|
+
@opts << opt
|
18
|
+
end
|
19
|
+
alias_method :push, :<<
|
20
|
+
|
21
|
+
# Retrieves the most recent set of options for this tag.
|
22
|
+
def opts
|
23
|
+
@opts.last || {}
|
24
|
+
end
|
25
|
+
|
26
|
+
# Pops the most recent tag off of the stack.
|
27
|
+
def pop
|
28
|
+
if @opts.empty?
|
29
|
+
@ctx.error 'Attempt to close an unopened tag', { :tag => tag }
|
30
|
+
nil
|
31
|
+
else
|
32
|
+
@opts.pop
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/test/email_spec.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'minitest/spec'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'inkcite'
|
4
|
+
|
5
|
+
describe Inkcite::Email do
|
6
|
+
|
7
|
+
before do
|
8
|
+
@email = Inkcite::Email.new('test/project/')
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'supports multi-line property declarations' do
|
12
|
+
@email.properties[:multiline].must_equal("This\n is a\n multiline tag.")
|
13
|
+
@email.properties[:"/multiline"].must_equal("This\n ends the\n multiline tag.")
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
data/test/parser_spec.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'minitest/spec'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'inkcite'
|
4
|
+
|
5
|
+
describe Inkcite::Parser do
|
6
|
+
|
7
|
+
it 'can resolve name=value parameters' do
|
8
|
+
Inkcite::Parser.parameters('border=1').must_equal({ :border => '1' })
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'can resolve parameters with dashes in the name' do
|
12
|
+
Inkcite::Parser.parameters('border-radius=5').must_equal({ :'border-radius' => '5' })
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'can resolve single word values sans double quotes' do
|
16
|
+
Inkcite::Parser.parameters('color=#f90').must_equal({ :'color' => '#f90' })
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'can resolve multi-word values wrapped in double quotes' do
|
20
|
+
Inkcite::Parser.parameters('alt="Click Here!"').must_equal({ :alt => 'Click Here!' })
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'can resolve complex, mixed parameters' do
|
24
|
+
Inkcite::Parser.parameters('src="images/logo.png" height=50 width=100 alt="Generic Logo" mobile-style=hide').must_equal({ :src => 'images/logo.png', :height => '50', :width => '100', :alt => 'Generic Logo', :'mobile-style' => 'hide' })
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'ignores malformed parameters' do
|
28
|
+
Inkcite::Parser.parameters('a=1 b="2 c=3').must_equal({ :a => '1' })
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'can identify an expression and replace it with a value' do
|
32
|
+
|
33
|
+
results = Inkcite::Parser.each('{table border=1}') do |e|
|
34
|
+
e.must_equal('table border=1')
|
35
|
+
'OK'
|
36
|
+
end
|
37
|
+
|
38
|
+
results.must_equal('OK')
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'can parse multiple expressions' do
|
42
|
+
expressions = [ 'table border=1', 'img src=logo.png height=15' ]
|
43
|
+
results = Inkcite::Parser.each('{table border=1}{img src=logo.png height=15}') do |e|
|
44
|
+
expressions.wont_be_empty
|
45
|
+
expressions -= [ e ]
|
46
|
+
'OK'
|
47
|
+
end
|
48
|
+
expressions.must_be_empty
|
49
|
+
results.must_equal('OKOK')
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'can parse nested expression' do
|
53
|
+
expressions = [ '#offwhite', 'table bgcolor=OK' ]
|
54
|
+
results = Inkcite::Parser.each('{table bgcolor={#offwhite}}') do |e|
|
55
|
+
expressions.wont_be_empty
|
56
|
+
expressions -= [ e ]
|
57
|
+
'OK'
|
58
|
+
end
|
59
|
+
expressions.must_be_empty
|
60
|
+
results.must_equal('OK')
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'does not loop forever' do
|
64
|
+
begin
|
65
|
+
Inkcite::Parser.each('{ok}') { |e| '{ok}' }
|
66
|
+
false.must_equal(true) # Intentional, should never be thrown.
|
67
|
+
rescue Exception => e
|
68
|
+
e.message.must_equal("Infinite replacement detected: 1000 {ok}")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# When true, appends a cache-busting timestamp to the images referenced
|
2
|
+
# in the email. This ensures the client always retrieves the latest
|
3
|
+
# version of the image and helpful during client previews. Generally
|
4
|
+
# this should be disabled in production.
|
5
|
+
cache-bust: false
|
6
|
+
|
7
|
+
# When true copies image alt-text to the title property. Populating both
|
8
|
+
# presents a more consistent image tooltip experience.
|
9
|
+
copy-alt-to-title: false
|
10
|
+
|
11
|
+
# When true minifies the HTML and CSS of the email. Should usually be
|
12
|
+
# disabled in development to make debugging easier.
|
13
|
+
minify: true
|
14
|
+
|
15
|
+
# When empty links are found in content, this is the URL that will be
|
16
|
+
# included instead - so that clients understand this link is missing
|
17
|
+
# and needs to be provided.
|
18
|
+
missing-link-url: 'https://github.com/404'
|
19
|
+
|
20
|
+
# No placeholders during image testing unless the spec turns this
|
21
|
+
# back on itself.
|
22
|
+
image-placeholders: false
|
23
|
+
|
24
|
+
# Inkcite can generate multiple versions of an email from a single source
|
25
|
+
# file which is useful for targeted mailings and a/b testing. Specify a
|
26
|
+
# unique, single-word identifier for each version.
|
27
|
+
#versions:
|
28
|
+
# - new_customer
|
29
|
+
# - past_customer
|
30
|
+
|
31
|
+
# SMTP settings for sending previews to the small list of internal and client
|
32
|
+
# addresses specified below. Most importantly, specify the address your test
|
33
|
+
# emails will be sent 'from:'
|
34
|
+
smtp:
|
35
|
+
host: 'smtp.gmail.com'
|
36
|
+
port: 587
|
37
|
+
domain: 'yourdomain.com'
|
38
|
+
username: ''
|
39
|
+
password: ''
|
40
|
+
from: 'Your Name <email@domain.com>'
|
41
|
+
|
42
|
+
# Specify the distribution lists for preview versions of the email.
|
43
|
+
recipients:
|
44
|
+
clients:
|
45
|
+
- 'Awesome Customer <awesome.customer@domain.com>'
|
46
|
+
internal:
|
47
|
+
- 'Creative Director <creative.director@domain.com>'
|
48
|
+
- 'Proofreader <proof.reader@domain.com>'
|
49
|
+
|
50
|
+
# Easy Litmus integration for compatibility testing.
|
51
|
+
# http://litmusapp.com
|
52
|
+
litmus:
|
53
|
+
subdomain: ''
|
54
|
+
username: ''
|
55
|
+
password: ''
|
56
|
+
|
57
|
+
# Easy deployment of static assets to a preview server.
|
58
|
+
sftp:
|
59
|
+
host: ''
|
60
|
+
path: ''
|
61
|
+
username: ''
|
62
|
+
password: ''
|
63
|
+
|
64
|
+
# Link tagging ensures that every link in the email includes a
|
65
|
+
# name-value pair. This is useful if you harvest data from your
|
66
|
+
# website analytics. {id} will be replaced with the unique ID
|
67
|
+
# from the link if you're concerned about which link the
|
68
|
+
# recipient clicked to get to your website.
|
69
|
+
#tag-links: "tag=inkcite|{id}"
|
70
|
+
|
71
|
+
# Optionally, if your email newsletter links to multiple websites
|
72
|
+
# and you only want to tag links to a specific domain, include
|
73
|
+
# that domain in this setting.
|
74
|
+
#tag-links-domain: 'inkceptional.com'
|
75
|
+
|
76
|
+
# Environment-specific overrides allow you to change any setting
|
77
|
+
# for each environment (e.g local development vs. client preview).
|
78
|
+
|
79
|
+
# These overrides apply to your local development environment when
|
80
|
+
# you are viewing the email in your browser via Inkcite's server.
|
81
|
+
development:
|
82
|
+
minify: false
|
83
|
+
|
84
|
+
# These overrides apply to previews both internally and to external
|
85
|
+
# clients and sent with Inkcite's preview function.
|
86
|
+
preview:
|
87
|
+
email:
|
88
|
+
view-in-browser-url: 'http://preview.contenthost.com/path/{filename}'
|
89
|
+
image-host: 'http://preview.imagehost.com/emails/'
|
90
|
+
|
91
|
+
# These overrides apply to the final, ready-to-send files.
|
92
|
+
production:
|
93
|
+
cache-bust: false
|
94
|
+
image-host: "http://production.imagehost.com/emails/myemail"
|
95
|
+
|
96
|
+
email:
|
97
|
+
view-in-browser-url: 'http://production.contenthost.com/path/{filename}'
|
98
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
// This file helps you keep your email code DRY (don't repeat yourself)
|
2
|
+
// by allowing you to easily define constants and custom tags.
|
3
|
+
//
|
4
|
+
// The keys and values in this file are tab-delimited.
|
5
|
+
|
6
|
+
// Palette
|
7
|
+
#background #ffffff
|
8
|
+
#text #000000
|
9
|
+
#link #0099cc
|
10
|
+
|
11
|
+
font-family sans-serif
|
12
|
+
font-size 15
|
13
|
+
line-height 19
|
14
|
+
|
15
|
+
default-color {#text}
|
16
|
+
default-font-size {font-size}
|
17
|
+
default-font-weight normal
|
18
|
+
default-line-height {line-height}
|
19
|
+
|
20
|
+
responsive-font-size 20
|
21
|
+
responsive-mobile-font-size 40
|
22
|
+
|
23
|
+
large-color #f00
|
24
|
+
large-font-family serif
|
25
|
+
large-font-size 24
|
26
|
+
large-font-weight bold
|
27
|
+
large-line-height 24
|
28
|
+
|
29
|
+
small-color #ccc
|
30
|
+
small-font-size 11
|
31
|
+
|
32
|
+
// This is an example of a custom tag. Tabs delimit the tag name, its open and
|
33
|
+
// close values. Inkcite will replace instances of {big} and {/big} with these
|
34
|
+
// values, respectively. Notice that it allows its color, which defaults to
|
35
|
+
// #444444, to be configured in your HTML as in {big color=#ff0000}.
|
36
|
+
big <div style="font-size: 18px; font-weight: bold; color: $color=#444444$"> </div>
|
37
|
+
|
38
|
+
// Bullet-proof buttons
|
39
|
+
button-border-radius 5
|
40
|
+
button-float center
|
41
|
+
button-padding 8
|
42
|
+
button-width 175
|
43
|
+
|
44
|
+
// Dimensions
|
45
|
+
width 500
|
46
|
+
|
47
|
+
// Test for multiline tag declaration
|
48
|
+
multiline <<-START
|
49
|
+
This
|
50
|
+
is a
|
51
|
+
multiline tag.
|
52
|
+
END->> <<-START
|
53
|
+
This
|
54
|
+
ends the
|
55
|
+
multiline tag.
|
56
|
+
END->>
|
Binary file
|
@@ -0,0 +1,58 @@
|
|
1
|
+
{table padding=10 width=100% mobile="hide"}
|
2
|
+
{td font=default align=center}This preheader will disappear on a mobile device.{/td}
|
3
|
+
{/table}
|
4
|
+
|
5
|
+
{table padding=10 width={width} float=center mobile="fill"}
|
6
|
+
{td align=left font=default}
|
7
|
+
|
8
|
+
{img src=logo.gif height=50 width=200 alt="Company Logo"}<br>
|
9
|
+
|
10
|
+
{lorem sentences=3}<br><br>
|
11
|
+
|
12
|
+
{img src=billboard.jpg height=180 width={width} mobile="fill"}<br>
|
13
|
+
|
14
|
+
{lorem sentences=10}<br><br>
|
15
|
+
|
16
|
+
{button id="call-to-action" href="http://inkceptional.com"}Call To Action{/button}<br>
|
17
|
+
|
18
|
+
{/td}
|
19
|
+
{/table}
|
20
|
+
|
21
|
+
{table width={width} padding=10 float=center valign=top mobile="stack"}
|
22
|
+
{td width=50%}
|
23
|
+
|
24
|
+
{img src=kittens.jpg width=250 height=150 mobile="fill"}<br>
|
25
|
+
|
26
|
+
{big}{lorem type=headline}{/big}
|
27
|
+
|
28
|
+
{lorem sentences=8}<br><br>
|
29
|
+
|
30
|
+
{button id="call-to-action2" href="http://inkceptional.com"}Create & Send{/button}
|
31
|
+
|
32
|
+
{/td}
|
33
|
+
{td width=50% bgcolor=#eeeeee font=default}
|
34
|
+
|
35
|
+
{big}{lorem type=headline}{/big}
|
36
|
+
{lorem sentences=3}<br><br>
|
37
|
+
|
38
|
+
{big}{lorem type=headline}{/big}
|
39
|
+
{lorem sentences=3}<br><br>
|
40
|
+
|
41
|
+
{big color=#990000}{lorem type=headline}{/big}
|
42
|
+
{lorem sentences=3}
|
43
|
+
|
44
|
+
{/td}
|
45
|
+
{/table}
|
46
|
+
|
47
|
+
{table padding=10 width={width} float=center mobile="fill"}
|
48
|
+
{td align=left font=small}
|
49
|
+
|
50
|
+
{img src=footer.jpg height=100 width={width} mobile="hide"}<br>
|
51
|
+
|
52
|
+
<% if email? %>
|
53
|
+
This email was sent to [email]. {a id="unsubscribe" href=#}Click here to unsubscribe{/a}.
|
54
|
+
Using ERB, this unsubscribe notice will only appear in the 'email' format of this project.
|
55
|
+
<% end %>
|
56
|
+
|
57
|
+
{/td}
|
58
|
+
{/table}
|