inkcite 1.0.0
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 +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}
|