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