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.
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}