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,168 @@
1
+ module Inkcite
2
+ module Renderer
3
+ class Button < Base
4
+
5
+ # Convenience class which makes it easy to retrieve the attributes
6
+ # for a button.
7
+ class Config
8
+
9
+ def initialize ctx, opt={}
10
+ @opt = opt
11
+ @ctx = ctx
12
+ end
13
+
14
+ def bgcolor
15
+ hex(@opt[:bgcolor] || @ctx[BUTTON_BACKGROUND_COLOR] || @ctx[Base::LINK_COLOR])
16
+ end
17
+
18
+ def border
19
+ @opt[:border] || @ctx[BUTTON_BORDER]
20
+ end
21
+
22
+ def border_bottom
23
+ "#{Renderer.px(bevel)} solid #{bevel_color}" if bevel > 0
24
+ end
25
+
26
+ def bevel
27
+ (@opt[:bevel] || @ctx[BUTTON_BEVEL]).to_i
28
+ end
29
+
30
+ def bevel_color
31
+ bc = @opt[BEVEL_COLOR] || @ctx[BUTTON_BEVEL_COLOR]
32
+ !bc.blank?? hex(bc) : text_shadow
33
+ end
34
+
35
+ def border_radius
36
+ (@opt[Base::BORDER_RADIUS] || @ctx[BUTTON_BORDER_RADIUS]).to_i
37
+ end
38
+
39
+ def color
40
+ hex(@opt[:color] || @ctx[BUTTON_COLOR] || Util::contrasting_text_color(bgcolor))
41
+ end
42
+
43
+ def float
44
+ @opt[:align] || @opt[:float] || @ctx[BUTTON_FLOAT]
45
+ end
46
+
47
+ def font
48
+ @opt[:font] || @ctx[BUTTON_FONT]
49
+ end
50
+
51
+ def font_size
52
+ (@opt[Base::FONT_SIZE] || @ctx[BUTTON_FONT_SIZE]).to_i
53
+ end
54
+
55
+ def font_weight
56
+ @opt[Base::FONT_WEIGHT] || @ctx[BUTTON_FONT_WEIGHT]
57
+ end
58
+
59
+ def height
60
+ (@opt[:height] || @ctx[BUTTON_HEIGHT]).to_i
61
+ end
62
+
63
+ def line_height
64
+ @opt[Base::LINE_HEIGHT] || @ctx[BUTTON_LINE_HEIGHT]
65
+ end
66
+
67
+ def margin_top
68
+ (@opt[Base::MARGIN_TOP] || @ctx[BUTTON_MARGIN_TOP]).to_i
69
+ end
70
+
71
+ def padding
72
+ (@opt[:padding] || @ctx[BUTTON_PADDING]).to_i
73
+ end
74
+
75
+ def text_shadow
76
+ ts = @opt[Base::TEXT_SHADOW] || @ctx[BUTTON_TEXT_SHADOW]
77
+ unless ts
78
+ ts = Util::brightness_value(bgcolor) > 382.5 ? Util::lighten(bgcolor, 0.25) : Util::darken(bgcolor)
79
+ end
80
+ hex(ts)
81
+ end
82
+
83
+ def width
84
+ (@opt[:width] || @ctx[BUTTON_WIDTH]).to_i
85
+ end
86
+
87
+ private
88
+
89
+ BEVEL_COLOR = :'bevel-color'
90
+
91
+ BUTTON_BACKGROUND_COLOR = :'button-background-color'
92
+ BUTTON_BEVEL = :'button-bevel'
93
+ BUTTON_BEVEL_COLOR = :'button-bevel-color'
94
+ BUTTON_BORDER = :'button-border'
95
+ BUTTON_BORDER_RADIUS = :'button-border-radius'
96
+ BUTTON_COLOR = :'button-color'
97
+ BUTTON_FLOAT = :'button-float'
98
+ BUTTON_FONT = :'button-font'
99
+ BUTTON_FONT_SIZE = :'button-font-size'
100
+ BUTTON_FONT_WEIGHT = :'button-font-weight'
101
+ BUTTON_HEIGHT = :'button-height'
102
+ BUTTON_LINE_HEIGHT = :'button-line-height'
103
+ BUTTON_MARGIN_TOP = :'button-margin-top'
104
+ BUTTON_PADDING = :'button-padding'
105
+ BUTTON_TEXT_SHADOW = :'button-text-shadow'
106
+ BUTTON_WIDTH = :'button-width'
107
+
108
+ # Convenient
109
+ def hex color
110
+ Renderer.hex(color)
111
+ end
112
+
113
+ end
114
+
115
+ def render tag, opt, ctx
116
+
117
+ html = ''
118
+
119
+ if tag == 'button'
120
+
121
+ id = opt[:id]
122
+ href = opt[:href]
123
+
124
+ cfg = Config.new(ctx, opt)
125
+
126
+ # Wrap the table in a link to make the whole thing clickable. This works
127
+ # in most email clients but doesn't work in Outlook (for a change).
128
+ html << "{a id=\"#{id}\" href=\"#{href}\" color=\"none\"}"
129
+
130
+ # Responsive button is just a highly styled table/td combination with optional
131
+ # curved corners and a lower bevel (border).
132
+ html << "{table bgcolor=#{cfg.bgcolor}"
133
+ html << " padding=#{cfg.padding}" if cfg.padding > 0
134
+ html << " border=#{cfg.border}" if cfg.border
135
+ html << " border-radius=#{cfg.border_radius}" if cfg.border_radius > 0
136
+
137
+ # Need to separate borders that are collapsed by default - otherwise, the bevel
138
+ # renders incorrectly.
139
+ html << " border-bottom=\"#{cfg.border_bottom}\" border-collapse=separate" if cfg.bevel > 0
140
+
141
+ html << " margin-top=#{cfg.margin_top}" if cfg.margin_top > 0
142
+ html << " width=#{cfg.width}" if cfg.width > 0
143
+ html << " float=#{cfg.float}" if cfg.float
144
+ html << " mobile=\"fill\"}\n"
145
+ html << "{td align=center"
146
+ html << " height=#{cfg.height} valign=middle" if cfg.height > 0
147
+ html << " font=\"#{cfg.font}\""
148
+ html << " line-height=#{cfg.line_height}" unless cfg.line_height.blank?
149
+ html << " font-size=\"#{cfg.font_size}\"" if cfg.font_size > 0
150
+ html << " font-weight=\"#{cfg.font_weight}\"" unless cfg.font_weight.blank?
151
+ html << " shadow=\"#{cfg.text_shadow}\" shadow-offset=-1}"
152
+
153
+ # Second, internal link for Outlook users that makes the inside of the button
154
+ # clickable.
155
+ html << "{a id=\"#{id}\" href=\"#{href}\" color=\"#{cfg.color}\"}"
156
+
157
+ else
158
+
159
+ html << "{/a}{/td}\n{/table}{/a}"
160
+
161
+ end
162
+
163
+ html
164
+ end
165
+
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,29 @@
1
+ module Inkcite
2
+ module Renderer
3
+ class Div < Responsive
4
+
5
+ def render tag, opt, ctx
6
+
7
+ return '</div>' if tag == '/div'
8
+
9
+ div = Element.new('div')
10
+
11
+ height = opt[:height].to_i
12
+ div.style[:height] = px(height) if height > 0
13
+
14
+ # Text alignment - left, right, center.
15
+ align = opt[:align]
16
+ div.style[TEXT_ALIGN] = align unless none?(align)
17
+
18
+ mix_font div, opt, ctx
19
+
20
+ mix_background div, opt
21
+
22
+ mix_responsive div, opt, ctx
23
+
24
+ div.to_s
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,82 @@
1
+ module Inkcite
2
+ module Renderer
3
+ class Element
4
+
5
+ attr_reader :tag
6
+
7
+ def initialize tag, att={}
8
+
9
+ # The tag, attribute and in-line CSS styles.
10
+ @tag = tag
11
+ @att = att
12
+
13
+ # True if the tag self-closes as in "<img .../>"
14
+ @self_close = att.delete(:self_close) == true
15
+
16
+ end
17
+
18
+ def [] key
19
+ @att[key]
20
+ end
21
+
22
+ def []= key, val
23
+ @att[key] = val
24
+ end
25
+
26
+ def add_rule rule
27
+
28
+ # Mark the rule as active in case it was one of the pre-defined rules
29
+ # that can be activated on first use.
30
+ rule.activate!
31
+
32
+ # Add the rule to those that will affect this element
33
+ responsive_styles << rule
34
+
35
+ # Add the rule's klass to those that will be rendered in the
36
+ # element's HTML.
37
+ classes << rule.klass
38
+
39
+ rule
40
+ end
41
+
42
+ def classes
43
+ @classes ||= Set.new
44
+ end
45
+
46
+ def responsive_styles
47
+ @responsive_rules ||= []
48
+ end
49
+
50
+ def self_close?
51
+ @self_close
52
+ end
53
+
54
+ def style
55
+ @styles ||= {}
56
+ end
57
+
58
+ def to_s
59
+
60
+ # Convert the style hash into CSS style attribute.
61
+ @att[:style] = Renderer.quote(Renderer.render_styles(@styles)) unless @styles.blank?
62
+
63
+ # Convert the list of CSS classes assigned to this element into an attribute
64
+ self[:class] = Renderer.quote(@classes.to_a.sort.join(' ')) unless @classes.blank?
65
+
66
+ html = '<'
67
+ html << @tag
68
+
69
+ unless @att.empty?
70
+ html << ' '
71
+ html << Renderer.join_hash(@att)
72
+ end
73
+
74
+ html << ' /' if self_close?
75
+ html << '>'
76
+
77
+ html
78
+ end
79
+
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,132 @@
1
+ module Inkcite
2
+ module Renderer
3
+ class Footnote < Base
4
+
5
+ class Instance
6
+
7
+ # Optional, unique ID assigned by the designer for this footnote
8
+ # so that a numeric footnote can be referenced repeatedly,
9
+ # non-linearly throughout the email.
10
+ attr_reader :id
11
+
12
+ # Symbol associated with the footnote. Typically going to be
13
+ # numeric but could be a user-specified symbol - e.g. †.
14
+ attr_reader :symbol
15
+
16
+ # The message associated with the footnote that will be displayed
17
+ # when the {footnotes} tag is rendered.
18
+ attr_reader :text
19
+
20
+ def initialize id, symbol, text
21
+ @id = id
22
+ @symbol = symbol.to_s
23
+ @text = text
24
+ end
25
+
26
+ def number
27
+ @symbol.to_i
28
+ end
29
+
30
+ # Returns true if this footnote is numeric rather than
31
+ # a symbol - e.g. †
32
+ def numeric?
33
+ @symbol == @symbol.to_i.to_s
34
+ end
35
+
36
+ def to_s
37
+ "#{symbol} #{text}"
38
+ end
39
+
40
+ end
41
+
42
+ def render tag, opt, ctx
43
+
44
+ # Grab the optional id for this footnote. This would only be
45
+ # populated if the designer intends on referencing this footnote
46
+ # in multiple spots.
47
+ id = opt[:id] || opt[:name]
48
+
49
+ # If an id was specified, check to see if an existing footnote has
50
+ # already been associated with this.
51
+ instance = ctx.footnotes.detect { |f| f.id == id } unless id.blank?
52
+ unless instance
53
+
54
+ # Grab the optional symbol that was specified by the designer. If
55
+ # this isn't specified count the number of existing numeric footnotes
56
+ # and increment it for this new footnote's symbol.
57
+ symbol = opt[:symbol]
58
+ if symbol.blank?
59
+
60
+ # Grab the last numeric footnote that was specified and, assuming
61
+ # there is one, increment the count. Otherwise, start the count
62
+ # off at one.
63
+ last_instance = ctx.footnotes.select(&:numeric?).last
64
+ symbol = last_instance.nil?? 1 : last_instance.symbol.to_i + 1
65
+
66
+ end
67
+
68
+ # Grab the text associated with this footnote.
69
+ text = opt[:text]
70
+ ctx.error("Footnote requires text attribute", { :id => id, :symbol => symbol }) if text.blank?
71
+
72
+ # Create a new Footnote instance
73
+ instance = Instance.new(id, symbol, text)
74
+
75
+ # Push the new footnote onto the stack.
76
+ ctx.footnotes << instance
77
+
78
+ end
79
+
80
+ # Allow footnotes to be defined without showing a symbol
81
+ hidden = opt[:hidden].to_i == 1
82
+ "#{instance.symbol}" unless hidden
83
+ end
84
+
85
+ end
86
+
87
+ class Footnotes < Base
88
+ def render tag, opt, ctx
89
+
90
+ # Nothing to do if footnotes are blank.
91
+ return if ctx.footnotes.blank?
92
+
93
+ # Check to see if a template has been provided. Otherwise use a default one based
94
+ # on the format of the email.
95
+ tmpl = opt[:tmpl] || opt[:template]
96
+ if tmpl.blank?
97
+ tmpl = ctx.text?? "($symbol$) $text$\n\n" : "<sup>$symbol$</sup> $text$<br><br>"
98
+
99
+ elsif ctx.text?
100
+
101
+ # If there are new-lines encoded in the custom template, make sure
102
+ # they get converted to real new lines.
103
+ tmpl.gsub!('\\n', "\n")
104
+
105
+ end
106
+
107
+
108
+ # Symbolic footnotes (e.g. daggers) always go ahead of
109
+ # the numeric footnotes - cause that's the way I like it
110
+ # uh huh, uh huh.
111
+ ctx.footnotes.sort! do |f1, f2|
112
+
113
+ next 1 if f1.numeric? && !f2.numeric?
114
+ next -1 if !f1.numeric? && f2.numeric?
115
+ f1.number <=> f2.number
116
+
117
+ end
118
+
119
+ html = ''
120
+
121
+ # Iterate through each of the footnotes and render them based on the
122
+ # template that was provided.
123
+ ctx.footnotes.collect do |f|
124
+ html << tmpl.gsub('$symbol$', f.symbol).gsub('$text$', f.text)
125
+ end
126
+
127
+ html
128
+ end
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,35 @@
1
+ module Inkcite
2
+ module Renderer
3
+ class GoogleAnalytics < Base
4
+
5
+ def render tag, opt, ctx
6
+
7
+ # Google analytics only possible in a browser version of the email.
8
+ return nil unless ctx.browser?
9
+
10
+ tracking_code = ctx[:code] || ctx[:id] || ctx[GOOGLE_ANALYTICS]
11
+ return nil if tracking_code.blank?
12
+
13
+ # Push the google analytics code onto the context's inline scripts.
14
+ script = <<-EOS
15
+ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
16
+ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
17
+ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
18
+ })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
19
+ ga('create', '#{tracking_code}');
20
+ ga('set', 'campaignName', '#{ctx.project}|#{ctx.issue.name}');
21
+ ga('send', 'pageview');
22
+ EOS
23
+
24
+ ctx.scripts << script
25
+
26
+ nil
27
+ end
28
+
29
+ private
30
+
31
+ GOOGLE_ANALYTICS = :'google-analytics'
32
+
33
+ end
34
+ end
35
+ end