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,111 @@
|
|
1
|
+
module Inkcite
|
2
|
+
class Parser
|
3
|
+
|
4
|
+
def self.each str, regex=BRACKET_REGEX, &block
|
5
|
+
|
6
|
+
# Make sure we're dealing with a string.
|
7
|
+
str = str.to_s
|
8
|
+
|
9
|
+
# If the string provided is frozen, we need to duplicate it because we
|
10
|
+
# don't want to modify the original.
|
11
|
+
str = str.dup if str.frozen?
|
12
|
+
|
13
|
+
# Counts the number of replacements and prevents an infinite loop.
|
14
|
+
failsafe = 0
|
15
|
+
|
16
|
+
# While there are matches within the string, repeatedly iterate.
|
17
|
+
while match = regex.match(str)
|
18
|
+
|
19
|
+
# Get the position, as an array, of the match within the string.
|
20
|
+
offset = match.offset(0)
|
21
|
+
|
22
|
+
# Provide the block with the area that was matched, sans wrapper brackets.
|
23
|
+
# Replace the brackets and original value with the block's results.
|
24
|
+
result = yield(match[1].to_s) || EMPTY_STRING
|
25
|
+
str[offset.first, offset.last - offset.first] = result
|
26
|
+
|
27
|
+
# Ensure we don't infinite loop.
|
28
|
+
failsafe += 1
|
29
|
+
raise "Infinite replacement detected: #{failsafe} #{str}" if failsafe >= MAX_RECURSION
|
30
|
+
end
|
31
|
+
|
32
|
+
str
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.parameters str
|
36
|
+
|
37
|
+
# Add an extra space to the end to ensure that the last parameter
|
38
|
+
# gets parsed correctly.
|
39
|
+
str = str.to_s + SPACE
|
40
|
+
|
41
|
+
# Will hold the parameters successfully parsed.
|
42
|
+
params = { }
|
43
|
+
|
44
|
+
# True if we're within a quoted value.
|
45
|
+
quote = false
|
46
|
+
|
47
|
+
# Will hold each key and value we find.
|
48
|
+
key = nil
|
49
|
+
|
50
|
+
# This will hold the substring that is assembled - it will either be the
|
51
|
+
# key (when an equals in encountered) or the value (when a space or closing
|
52
|
+
# quote is discovered).
|
53
|
+
value = ''
|
54
|
+
|
55
|
+
length = str.length - 1
|
56
|
+
for i in 0..length
|
57
|
+
|
58
|
+
# Read the next character in the string.
|
59
|
+
chr = str[i]
|
60
|
+
|
61
|
+
if chr == QUOTE
|
62
|
+
|
63
|
+
# Each time a quote is discovered, toggle the flag indicating that we're
|
64
|
+
# inside of a value or (when false) we're assembling a key.
|
65
|
+
quote = !quote
|
66
|
+
|
67
|
+
elsif chr == EQUAL && !quote
|
68
|
+
|
69
|
+
# When an equal sign is encountered and we're not inside of a quote, then
|
70
|
+
# convert the assembled value into a symbolized key.
|
71
|
+
unless value.blank?
|
72
|
+
key = value.to_sym
|
73
|
+
value = ''
|
74
|
+
end
|
75
|
+
|
76
|
+
elsif chr == SPACE && !quote
|
77
|
+
|
78
|
+
# When a space is encountered, if we're not inside of a quote block then
|
79
|
+
# assign the assembled value to the previously discovered key.
|
80
|
+
if key
|
81
|
+
params[key] = value
|
82
|
+
value = ''
|
83
|
+
key = nil
|
84
|
+
end
|
85
|
+
|
86
|
+
else
|
87
|
+
value << chr
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
params
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
# When the handler returns nil
|
99
|
+
EMPTY_STRING = ''
|
100
|
+
|
101
|
+
# We fail if we recurse through a property more than this many times.
|
102
|
+
MAX_RECURSION = 1000
|
103
|
+
|
104
|
+
BRACKET_REGEX = /\{([^\{\}]+)\}/
|
105
|
+
|
106
|
+
SPACE = ' '
|
107
|
+
QUOTE = '"'
|
108
|
+
EQUAL = '='
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
require_relative 'renderer/base'
|
2
|
+
require_relative 'renderer/element'
|
3
|
+
require_relative 'renderer/responsive'
|
4
|
+
require_relative 'renderer/image_base'
|
5
|
+
require_relative 'renderer/table_base'
|
6
|
+
|
7
|
+
require_relative 'renderer/button'
|
8
|
+
require_relative 'renderer/div'
|
9
|
+
require_relative 'renderer/footnote'
|
10
|
+
require_relative 'renderer/google_analytics'
|
11
|
+
require_relative 'renderer/image'
|
12
|
+
require_relative 'renderer/in_browser'
|
13
|
+
require_relative 'renderer/like'
|
14
|
+
require_relative 'renderer/link'
|
15
|
+
require_relative 'renderer/litmus'
|
16
|
+
require_relative 'renderer/lorem'
|
17
|
+
require_relative 'renderer/mobile_image'
|
18
|
+
require_relative 'renderer/mobile_style'
|
19
|
+
require_relative 'renderer/mobile_toggle'
|
20
|
+
require_relative 'renderer/outlook_background'
|
21
|
+
require_relative 'renderer/partial'
|
22
|
+
require_relative 'renderer/preheader'
|
23
|
+
require_relative 'renderer/property'
|
24
|
+
require_relative 'renderer/span'
|
25
|
+
require_relative 'renderer/table'
|
26
|
+
require_relative 'renderer/td'
|
27
|
+
|
28
|
+
module Inkcite
|
29
|
+
module Renderer
|
30
|
+
|
31
|
+
def self.fix_illegal_characters value, context
|
32
|
+
|
33
|
+
# These special characters cause rendering problems in a variety
|
34
|
+
# of email clients. Convert them to the correct unicode characters.
|
35
|
+
# https://www.campaignmonitor.com/blog/post/1810/why-are-all-my-apostrophes-mis
|
36
|
+
|
37
|
+
if context.text?
|
38
|
+
value.gsub!(/[–—]/, '-')
|
39
|
+
value.gsub!(/™/, '(tm)')
|
40
|
+
value.gsub!(/®/, '(r)')
|
41
|
+
value.gsub!(/[‘’`]/, "'")
|
42
|
+
value.gsub!(/[“”]/, '"')
|
43
|
+
value.gsub!(/…/, '...')
|
44
|
+
|
45
|
+
else
|
46
|
+
value.gsub!(/[–—]/, '–')
|
47
|
+
value.gsub!(/\-\-/, '–')
|
48
|
+
value.gsub!(/™/, '™')
|
49
|
+
value.gsub!(/®/, '®')
|
50
|
+
value.gsub!(/[‘’`]/, '’')
|
51
|
+
value.gsub!(/“/, '“')
|
52
|
+
value.gsub!(/”/, '”')
|
53
|
+
value.gsub!(/é/, 'é')
|
54
|
+
value.gsub!(/…/, '…')
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
value
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.hex color
|
62
|
+
|
63
|
+
# Convert #rgb into #rrggbb
|
64
|
+
if !color.blank? && color.length < 7
|
65
|
+
red = color[1]
|
66
|
+
green = color[2]
|
67
|
+
blue = color[3]
|
68
|
+
color = "##{red}#{red}#{green}#{green}#{blue}#{blue}"
|
69
|
+
end
|
70
|
+
|
71
|
+
color
|
72
|
+
end
|
73
|
+
|
74
|
+
# Joins the key-value-pairs of the provided hash into a readable
|
75
|
+
# string destined for HTML or CSS style declarations. For example,
|
76
|
+
# { :bgcolor => '"#fff"' } would become bgcolor="#fff" using the
|
77
|
+
# default equality and space delimiters.
|
78
|
+
def self.join_hash hash, equal=EQUAL, sep=SPACE
|
79
|
+
|
80
|
+
pairs = []
|
81
|
+
|
82
|
+
hash.keys.sort.each do |att|
|
83
|
+
val = hash[att]
|
84
|
+
pairs << "#{att}#{equal}#{val}" unless val.blank?
|
85
|
+
end
|
86
|
+
|
87
|
+
pairs.join(sep)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Applies a "px" extension to unlabeled integer values. If a labeled
|
91
|
+
# value is detected (e.g. 2em) or a non-integer value is provided
|
92
|
+
# (e.g. "normal") then the value is returned directly.
|
93
|
+
def self.px val
|
94
|
+
|
95
|
+
# Quick abort if a non-integer value has been provided. This catches
|
96
|
+
# cases like 3em and normal. When provided, the value is not converted
|
97
|
+
# to pixels and instead is returned directly.
|
98
|
+
return val if val && val.to_i.to_s != val.to_s
|
99
|
+
|
100
|
+
val = val.to_i
|
101
|
+
val = "#{val}px" unless val == 0
|
102
|
+
val
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.quote val
|
106
|
+
"\"#{val}\""
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.render str, context
|
110
|
+
|
111
|
+
Parser.each(str) do |tag|
|
112
|
+
|
113
|
+
# Split the string into the tag and it's attributes.
|
114
|
+
name, opts = tag.split(SPACE, 2)
|
115
|
+
|
116
|
+
# Convert the options string (e.g. color=#ff9900 border=none) into parameters.
|
117
|
+
opts = Parser.parameters(opts)
|
118
|
+
|
119
|
+
# Strip off the leading slash (/) if there is one. Renderers are
|
120
|
+
open_tag = (name.starts_with?(SLASH) ? name[1..-1] : name).to_sym
|
121
|
+
|
122
|
+
# Choose a renderer either from the dynamic set or use the default one that
|
123
|
+
# simply renders from the property values.
|
124
|
+
renderer = renderers[open_tag] || default_renderer
|
125
|
+
|
126
|
+
renderer.render name, opts, context
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.render_styles styles
|
133
|
+
join_hash(styles, COLON, SEMI_COLON)
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
COLON = ':'
|
139
|
+
EQUAL = '='
|
140
|
+
SEMI_COLON = ';'
|
141
|
+
SPACE = ' '
|
142
|
+
SLASH = '/'
|
143
|
+
|
144
|
+
def self.default_renderer
|
145
|
+
@default_renderer ||= Property.new
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.renderers
|
149
|
+
|
150
|
+
# Dynamic renderers for custom behavior and tags.
|
151
|
+
@renderers ||= {
|
152
|
+
:a => Link.new,
|
153
|
+
:button => Button.new,
|
154
|
+
:div => Div.new,
|
155
|
+
:footnote => Footnote.new,
|
156
|
+
:footnotes => Footnotes.new,
|
157
|
+
:google => GoogleAnalytics.new,
|
158
|
+
:img => Image.new,
|
159
|
+
:'in-browser' => InBrowser.new,
|
160
|
+
:include => Partial.new,
|
161
|
+
:like => Like.new,
|
162
|
+
:litmus => Litmus.new,
|
163
|
+
:lorem => Lorem.new,
|
164
|
+
:'mobile-img' => MobileImage.new,
|
165
|
+
:'mobile-style' => MobileStyle.new,
|
166
|
+
:'mobile-toggle-on' => MobileToggleOn.new,
|
167
|
+
:'outlook-bg' => OutlookBackground.new,
|
168
|
+
:preheader => Preheader.new,
|
169
|
+
:span => Span.new,
|
170
|
+
:table => Table.new,
|
171
|
+
:td => Td.new
|
172
|
+
}
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
module Inkcite
|
2
|
+
module Renderer
|
3
|
+
class Base
|
4
|
+
|
5
|
+
# Constants for style and property names with dashes in them.
|
6
|
+
BACKGROUND_COLOR = :'background-color'
|
7
|
+
BACKGROUND_IMAGE = :'background-image'
|
8
|
+
BACKGROUND_REPEAT = :'background-repeat'
|
9
|
+
BACKGROUND_POSITION = :'background-position'
|
10
|
+
BACKGROUND_SIZE = :'background-size'
|
11
|
+
BORDER_BOTTOM = :'border-bottom'
|
12
|
+
BORDER_COLLAPSE = :'border-collapse'
|
13
|
+
BORDER_RADIUS = :'border-radius'
|
14
|
+
BORDER_SPACING = :'border-spacing'
|
15
|
+
BOX_SHADOW = :'box-shadow'
|
16
|
+
FONT_FAMILY = :'font-family'
|
17
|
+
FONT_SIZE = :'font-size'
|
18
|
+
FONT_WEIGHT = :'font-weight'
|
19
|
+
LETTER_SPACING = :'letter-spacing'
|
20
|
+
LINE_HEIGHT = :'line-height'
|
21
|
+
LINK_COLOR = :'#link'
|
22
|
+
MARGIN_TOP = :'margin-top'
|
23
|
+
PADDING_X = :'padding-x'
|
24
|
+
PADDING_Y = :'padding-y'
|
25
|
+
TEXT_ALIGN = :'text-align'
|
26
|
+
TEXT_DECORATION = :'text-decoration'
|
27
|
+
TEXT_SHADOW = :'text-shadow'
|
28
|
+
TEXT_SHADOW_BLUR = :'shadow-blur'
|
29
|
+
TEXT_SHADOW_OFFSET = :'shadow-offset'
|
30
|
+
VERTICAL_ALIGN = :'vertical-align'
|
31
|
+
|
32
|
+
# CSS Directions
|
33
|
+
DIRECTIONS = [ :top, :right, :bottom, :left ]
|
34
|
+
|
35
|
+
# Attribute and CSS dimensions
|
36
|
+
DIMENSIONS = [ :width, :height ]
|
37
|
+
|
38
|
+
# Common value declarations
|
39
|
+
POUND_SIGN = '#'
|
40
|
+
NONE = 'none'
|
41
|
+
|
42
|
+
# Zero-width space character
|
43
|
+
ZERO_WIDTH_SPACE = '​'
|
44
|
+
|
45
|
+
def render tag, opt, ctx
|
46
|
+
raise "Not implemented: #{tag} #{opts}"
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
# Convenience proxy
|
52
|
+
def detect *opts
|
53
|
+
Util.detect(*opts)
|
54
|
+
end
|
55
|
+
|
56
|
+
def detect_font att, font, opt, parent, ctx
|
57
|
+
val = detect(opt[att], ctx["#{font}-#{att}"], parent ? parent[att] : nil)
|
58
|
+
|
59
|
+
# Sometimes font values reference other defined values so we need
|
60
|
+
# to run them through the renderer to resolve them.
|
61
|
+
val = Inkcite::Renderer.render(val, ctx)
|
62
|
+
|
63
|
+
# Convience
|
64
|
+
val = nil if none?(val)
|
65
|
+
|
66
|
+
val
|
67
|
+
end
|
68
|
+
|
69
|
+
# Convenience pass-thru to Renderer's static helper method.
|
70
|
+
def hex color
|
71
|
+
Renderer.hex(color)
|
72
|
+
end
|
73
|
+
|
74
|
+
def none? val
|
75
|
+
val.blank? || val == NONE
|
76
|
+
end
|
77
|
+
|
78
|
+
# Sets the element's in-line bgcolor style if it has been defined
|
79
|
+
# in the provided options.
|
80
|
+
def mix_background element, opt
|
81
|
+
|
82
|
+
# Background color of the image, if populated.
|
83
|
+
bgcolor = detect(opt[:bgcolor], opt[BACKGROUND_COLOR])
|
84
|
+
element.style[BACKGROUND_COLOR] = hex(bgcolor) unless none?(bgcolor)
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
def mix_font element, opt, ctx, parent=nil
|
89
|
+
|
90
|
+
# Always ensure we have a parent to inherit from.
|
91
|
+
parent ||= {}
|
92
|
+
|
93
|
+
# Check for a font in either the element's specified options or inherit a setting from
|
94
|
+
# from the parent if provided.
|
95
|
+
font = detect(opt[:font], parent[:font])
|
96
|
+
|
97
|
+
# Fonts can be disabled on individual cells if the parent table
|
98
|
+
# has set one for the entire table.
|
99
|
+
font = nil if none?(font)
|
100
|
+
|
101
|
+
font_family = detect_font(FONT_FAMILY, font, opt, parent, ctx)
|
102
|
+
element.style[FONT_FAMILY] = font_family unless font_family.blank?
|
103
|
+
|
104
|
+
font_size = detect_font(FONT_SIZE, font, opt, parent, ctx)
|
105
|
+
element.style[FONT_SIZE] = px(font_size) unless font_size.blank?
|
106
|
+
|
107
|
+
color = detect_font(:color, font, opt, parent, ctx)
|
108
|
+
element.style[:color] = hex(color) unless color.blank?
|
109
|
+
|
110
|
+
line_height = detect_font(LINE_HEIGHT, font, opt, parent, ctx)
|
111
|
+
element.style[LINE_HEIGHT] = px(line_height) unless line_height.blank?
|
112
|
+
|
113
|
+
font_weight = detect_font(FONT_WEIGHT, font, opt, parent, ctx)
|
114
|
+
element.style[FONT_WEIGHT] = font_weight unless font_weight.blank?
|
115
|
+
|
116
|
+
letter_spacing = detect_font(LETTER_SPACING, font, opt, parent, ctx)
|
117
|
+
element.style[LETTER_SPACING] = px(letter_spacing) unless none?(letter_spacing)
|
118
|
+
|
119
|
+
# With font support comes text shadow support.
|
120
|
+
mix_text_shadow element, opt, ctx
|
121
|
+
|
122
|
+
font
|
123
|
+
end
|
124
|
+
|
125
|
+
def mix_text_shadow element, opt, ctx
|
126
|
+
|
127
|
+
shadow = detect(opt[:shadow], opt[TEXT_SHADOW])
|
128
|
+
return if shadow.blank?
|
129
|
+
|
130
|
+
# Allow shadows to be disabled because sometimes a child element (like an
|
131
|
+
# image within a cell or an entire cell within a table) wants to disable
|
132
|
+
# the shadowing forced by a parent.
|
133
|
+
if none?(shadow)
|
134
|
+
element.style[TEXT_SHADOW] = shadow
|
135
|
+
|
136
|
+
else
|
137
|
+
|
138
|
+
shadow_offset = detect(opt[TEXT_SHADOW_OFFSET], ctx[TEXT_SHADOW_OFFSET], 1)
|
139
|
+
shadow_blur = detect(opt[TEXT_SHADOW_BLUR], ctx[TEXT_SHADOW_BLUR], 0)
|
140
|
+
|
141
|
+
element.style[TEXT_SHADOW] = "0 #{px(shadow_offset)} #{px(shadow_blur)} #{hex(shadow)}"
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
def px val
|
148
|
+
Renderer.px(val)
|
149
|
+
end
|
150
|
+
|
151
|
+
def quote val
|
152
|
+
Renderer.quote(val)
|
153
|
+
end
|
154
|
+
|
155
|
+
def render_tag tag, attributes=nil, styles=nil
|
156
|
+
|
157
|
+
# Convert the style hash into CSS style attribute.
|
158
|
+
unless styles.blank?
|
159
|
+
attributes ||= {}
|
160
|
+
attributes[:style] = quote(Renderer.render_styles(styles))
|
161
|
+
end
|
162
|
+
|
163
|
+
# Check to see if this is a self-closing tag.
|
164
|
+
self_close = attributes && attributes.delete(:self_close) == true
|
165
|
+
|
166
|
+
html = "<#{tag}"
|
167
|
+
|
168
|
+
unless attributes.blank?
|
169
|
+
|
170
|
+
# Make sure multiple classes are handled properly.
|
171
|
+
classes = attributes[:class]
|
172
|
+
attributes[:class] = quote([*classes].join(' ')) unless classes.blank?
|
173
|
+
|
174
|
+
html << SPACE + Renderer.join_hash(attributes)
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
html << '/' if self_close
|
179
|
+
html << '>'
|
180
|
+
|
181
|
+
html
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|