inkcite 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +110 -0
  4. data/Rakefile +8 -0
  5. data/assets/facebook-like.css +62 -0
  6. data/assets/facebook-like.js +59 -0
  7. data/assets/init/config.yml +97 -0
  8. data/assets/init/helpers.tsv +31 -0
  9. data/assets/init/source.html +60 -0
  10. data/assets/init/source.txt +6 -0
  11. data/bin/inkcite +6 -0
  12. data/inkcite.gemspec +42 -0
  13. data/lib/inkcite.rb +32 -0
  14. data/lib/inkcite/cli/base.rb +128 -0
  15. data/lib/inkcite/cli/build.rb +130 -0
  16. data/lib/inkcite/cli/init.rb +58 -0
  17. data/lib/inkcite/cli/preview.rb +30 -0
  18. data/lib/inkcite/cli/server.rb +123 -0
  19. data/lib/inkcite/cli/test.rb +61 -0
  20. data/lib/inkcite/email.rb +219 -0
  21. data/lib/inkcite/mailer.rb +140 -0
  22. data/lib/inkcite/minifier.rb +151 -0
  23. data/lib/inkcite/parser.rb +111 -0
  24. data/lib/inkcite/renderer.rb +177 -0
  25. data/lib/inkcite/renderer/base.rb +186 -0
  26. data/lib/inkcite/renderer/button.rb +168 -0
  27. data/lib/inkcite/renderer/div.rb +29 -0
  28. data/lib/inkcite/renderer/element.rb +82 -0
  29. data/lib/inkcite/renderer/footnote.rb +132 -0
  30. data/lib/inkcite/renderer/google_analytics.rb +35 -0
  31. data/lib/inkcite/renderer/image.rb +95 -0
  32. data/lib/inkcite/renderer/image_base.rb +82 -0
  33. data/lib/inkcite/renderer/in_browser.rb +38 -0
  34. data/lib/inkcite/renderer/like.rb +73 -0
  35. data/lib/inkcite/renderer/link.rb +243 -0
  36. data/lib/inkcite/renderer/litmus.rb +33 -0
  37. data/lib/inkcite/renderer/lorem.rb +39 -0
  38. data/lib/inkcite/renderer/mobile_image.rb +67 -0
  39. data/lib/inkcite/renderer/mobile_style.rb +40 -0
  40. data/lib/inkcite/renderer/mobile_toggle.rb +27 -0
  41. data/lib/inkcite/renderer/outlook_background.rb +48 -0
  42. data/lib/inkcite/renderer/partial.rb +31 -0
  43. data/lib/inkcite/renderer/preheader.rb +22 -0
  44. data/lib/inkcite/renderer/property.rb +39 -0
  45. data/lib/inkcite/renderer/responsive.rb +334 -0
  46. data/lib/inkcite/renderer/span.rb +21 -0
  47. data/lib/inkcite/renderer/table.rb +67 -0
  48. data/lib/inkcite/renderer/table_base.rb +149 -0
  49. data/lib/inkcite/renderer/td.rb +92 -0
  50. data/lib/inkcite/uploader.rb +173 -0
  51. data/lib/inkcite/util.rb +85 -0
  52. data/lib/inkcite/version.rb +3 -0
  53. data/lib/inkcite/view.rb +745 -0
  54. data/lib/inkcite/view/context.rb +38 -0
  55. data/lib/inkcite/view/media_query.rb +60 -0
  56. data/lib/inkcite/view/tag_stack.rb +38 -0
  57. data/test/email_spec.rb +16 -0
  58. data/test/parser_spec.rb +72 -0
  59. data/test/project/config.yml +98 -0
  60. data/test/project/helpers.tsv +56 -0
  61. data/test/project/images/inkcite.jpg +0 -0
  62. data/test/project/source.html +58 -0
  63. data/test/project/source.txt +6 -0
  64. data/test/renderer/button_spec.rb +45 -0
  65. data/test/renderer/div_spec.rb +101 -0
  66. data/test/renderer/element_spec.rb +31 -0
  67. data/test/renderer/footnote_spec.rb +57 -0
  68. data/test/renderer/image_spec.rb +82 -0
  69. data/test/renderer/link_spec.rb +84 -0
  70. data/test/renderer/mobile_image_spec.rb +27 -0
  71. data/test/renderer/mobile_style_spec.rb +37 -0
  72. data/test/renderer/td_spec.rb +126 -0
  73. data/test/renderer_spec.rb +28 -0
  74. data/test/view_spec.rb +15 -0
  75. 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
@@ -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
@@ -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 &amp; 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}