inkcite 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,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
|