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,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