sevk 0.1.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/README.md +75 -0
- data/Rakefile +42 -0
- data/lib/sevk/client.rb +164 -0
- data/lib/sevk/error.rb +40 -0
- data/lib/sevk/markup/renderer.rb +516 -0
- data/lib/sevk/resources/audiences.rb +41 -0
- data/lib/sevk/resources/base.rb +22 -0
- data/lib/sevk/resources/broadcasts.rb +18 -0
- data/lib/sevk/resources/contacts.rb +40 -0
- data/lib/sevk/resources/domains.rb +18 -0
- data/lib/sevk/resources/emails.rb +24 -0
- data/lib/sevk/resources/segments.rb +35 -0
- data/lib/sevk/resources/subscriptions.rb +23 -0
- data/lib/sevk/resources/templates.rb +37 -0
- data/lib/sevk/resources/topics.rb +35 -0
- data/lib/sevk/version.rb +5 -0
- data/lib/sevk.rb +213 -0
- data/sevk.gemspec +40 -0
- metadata +174 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevk
|
|
4
|
+
module Markup
|
|
5
|
+
# Font configuration
|
|
6
|
+
class FontConfig
|
|
7
|
+
attr_accessor :id, :name, :url
|
|
8
|
+
|
|
9
|
+
def initialize(id: "", name: "", url: "")
|
|
10
|
+
@id = id
|
|
11
|
+
@name = name
|
|
12
|
+
@url = url
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Head settings for email generation
|
|
17
|
+
class EmailHeadSettings
|
|
18
|
+
attr_accessor :title, :preview_text, :styles, :fonts
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@title = ""
|
|
22
|
+
@preview_text = ""
|
|
23
|
+
@styles = ""
|
|
24
|
+
@fonts = []
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Parsed email content
|
|
29
|
+
class ParsedEmailContent
|
|
30
|
+
attr_accessor :body, :head_settings
|
|
31
|
+
|
|
32
|
+
def initialize
|
|
33
|
+
@body = ""
|
|
34
|
+
@head_settings = EmailHeadSettings.new
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Sevk Markup Renderer
|
|
39
|
+
# Converts Sevk markup to email-compatible HTML using regex-based parsing (like Node.js)
|
|
40
|
+
class Renderer
|
|
41
|
+
def initialize
|
|
42
|
+
@head_settings = EmailHeadSettings.new
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def render(markup)
|
|
46
|
+
# Parse head settings from markup
|
|
47
|
+
parse_head_settings(markup)
|
|
48
|
+
|
|
49
|
+
# Normalize markup
|
|
50
|
+
markup = normalize_markup(markup)
|
|
51
|
+
|
|
52
|
+
# Process markup using regex
|
|
53
|
+
processed = process_markup(markup)
|
|
54
|
+
|
|
55
|
+
generate_html(processed)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def normalize_markup(content)
|
|
61
|
+
result = content
|
|
62
|
+
|
|
63
|
+
# Replace <link> with <sevk-link>
|
|
64
|
+
if result.include?("<link")
|
|
65
|
+
result = result.gsub(/<link\s+href=/i, "<sevk-link href=")
|
|
66
|
+
result = result.gsub("</link>", "</sevk-link>")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
unless result.include?("<sevk-email") || result.include?("<email") || result.include?("<mail")
|
|
70
|
+
result = "<mail><body>#{result}</body></mail>"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
result
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def parse_head_settings(markup)
|
|
77
|
+
# Extract title
|
|
78
|
+
if (match = markup.match(/<title[^>]*>([\s\S]*?)<\/title>/i))
|
|
79
|
+
@head_settings.title = match[1].strip
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Extract preview
|
|
83
|
+
if (match = markup.match(/<preview[^>]*>([\s\S]*?)<\/preview>/i))
|
|
84
|
+
@head_settings.preview_text = match[1].strip
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Extract styles
|
|
88
|
+
if (match = markup.match(/<style[^>]*>([\s\S]*?)<\/style>/i))
|
|
89
|
+
@head_settings.styles = match[1].strip
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Extract fonts
|
|
93
|
+
markup.scan(/<font[^>]*name=["']([^"']*)["'][^>]*url=["']([^"']*)["'][^>]*\/?>/i).each_with_index do |(name, url), i|
|
|
94
|
+
@head_settings.fonts << FontConfig.new(id: "font-#{i}", name: name, url: url)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def process_markup(content)
|
|
99
|
+
result = content
|
|
100
|
+
|
|
101
|
+
# Process section tags
|
|
102
|
+
result = process_tag(result, "section") do |attrs, inner|
|
|
103
|
+
style = extract_all_style_attributes(attrs)
|
|
104
|
+
style_str = style_to_string(style)
|
|
105
|
+
%(<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="#{style_str}">
|
|
106
|
+
<tbody>
|
|
107
|
+
<tr>
|
|
108
|
+
<td>#{inner}</td>
|
|
109
|
+
</tr>
|
|
110
|
+
</tbody>
|
|
111
|
+
</table>)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Process row tags
|
|
115
|
+
result = process_tag(result, "row") do |attrs, inner|
|
|
116
|
+
style = extract_all_style_attributes(attrs)
|
|
117
|
+
style_str = style_to_string(style)
|
|
118
|
+
%(<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="#{style_str}">
|
|
119
|
+
<tbody style="width:100%">
|
|
120
|
+
<tr style="width:100%">#{inner}</tr>
|
|
121
|
+
</tbody>
|
|
122
|
+
</table>)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Process column tags
|
|
126
|
+
result = process_tag(result, "column") do |attrs, inner|
|
|
127
|
+
style = extract_all_style_attributes(attrs)
|
|
128
|
+
style_str = style_to_string(style)
|
|
129
|
+
%(<td style="#{style_str}">#{inner}</td>)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Process container tags
|
|
133
|
+
result = process_tag(result, "container") do |attrs, inner|
|
|
134
|
+
style = extract_all_style_attributes(attrs)
|
|
135
|
+
style_str = style_to_string(style)
|
|
136
|
+
%(<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="#{style_str}">
|
|
137
|
+
<tbody>
|
|
138
|
+
<tr style="width:100%">
|
|
139
|
+
<td>#{inner}</td>
|
|
140
|
+
</tr>
|
|
141
|
+
</tbody>
|
|
142
|
+
</table>)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Process heading tags
|
|
146
|
+
result = process_tag(result, "heading") do |attrs, inner|
|
|
147
|
+
level = attrs["level"] || "1"
|
|
148
|
+
style = extract_all_style_attributes(attrs)
|
|
149
|
+
style_str = style_to_string(style)
|
|
150
|
+
%(<h#{level} style="#{style_str}">#{inner}</h#{level}>)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Process paragraph tags
|
|
154
|
+
result = process_tag(result, "paragraph") do |attrs, inner|
|
|
155
|
+
style = extract_all_style_attributes(attrs)
|
|
156
|
+
style_str = style_to_string(style)
|
|
157
|
+
%(<p style="#{style_str}">#{inner}</p>)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Process text tags
|
|
161
|
+
result = process_tag(result, "text") do |attrs, inner|
|
|
162
|
+
style = extract_all_style_attributes(attrs)
|
|
163
|
+
style_str = style_to_string(style)
|
|
164
|
+
%(<span style="#{style_str}">#{inner}</span>)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Process button tags with MSO compatibility
|
|
168
|
+
result = process_tag(result, "button") do |attrs, inner|
|
|
169
|
+
process_button(attrs, inner)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Process image tags
|
|
173
|
+
result = result.gsub(/<image([^>]*)\/?>/i) do
|
|
174
|
+
attrs = parse_attributes(Regexp.last_match(1) || "")
|
|
175
|
+
src = attrs["src"] || ""
|
|
176
|
+
alt = attrs["alt"] || ""
|
|
177
|
+
width = attrs["width"]
|
|
178
|
+
height = attrs["height"]
|
|
179
|
+
|
|
180
|
+
style = extract_all_style_attributes(attrs)
|
|
181
|
+
style["outline"] ||= "none"
|
|
182
|
+
style["border"] ||= "none"
|
|
183
|
+
style["text-decoration"] ||= "none"
|
|
184
|
+
|
|
185
|
+
style_str = style_to_string(style)
|
|
186
|
+
width_attr = width ? %( width="#{width}") : ""
|
|
187
|
+
height_attr = height ? %( height="#{height}") : ""
|
|
188
|
+
|
|
189
|
+
%(<img src="#{src}" alt="#{alt}"#{width_attr}#{height_attr} style="#{style_str}" />)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Process divider tags
|
|
193
|
+
result = result.gsub(/<divider([^>]*)\/?>/i) do
|
|
194
|
+
attrs = parse_attributes(Regexp.last_match(1) || "")
|
|
195
|
+
style = extract_all_style_attributes(attrs)
|
|
196
|
+
style_str = style_to_string(style)
|
|
197
|
+
class_attr = attrs["class"] || attrs["className"]
|
|
198
|
+
class_str = class_attr ? %( class="#{class_attr}") : ""
|
|
199
|
+
%(<hr style="#{style_str}"#{class_str} />)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Process link tags
|
|
203
|
+
result = process_tag(result, "sevk-link") do |attrs, inner|
|
|
204
|
+
href = attrs["href"] || "#"
|
|
205
|
+
target = attrs["target"] || "_blank"
|
|
206
|
+
style = extract_all_style_attributes(attrs)
|
|
207
|
+
style_str = style_to_string(style)
|
|
208
|
+
%(<a href="#{href}" target="#{target}" style="#{style_str}">#{inner}</a>)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Process list tags
|
|
212
|
+
result = process_tag(result, "list") do |attrs, inner|
|
|
213
|
+
list_type = attrs["type"] || "unordered"
|
|
214
|
+
tag = list_type == "ordered" ? "ol" : "ul"
|
|
215
|
+
style = extract_all_style_attributes(attrs)
|
|
216
|
+
style["list-style-type"] = attrs["list-style-type"] if attrs["list-style-type"]
|
|
217
|
+
style_str = style_to_string(style)
|
|
218
|
+
class_attr = attrs["class"] || attrs["className"]
|
|
219
|
+
class_str = class_attr ? %( class="#{class_attr}") : ""
|
|
220
|
+
%(<#{tag} style="#{style_str}"#{class_str}>#{inner}</#{tag}>)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Process list item tags
|
|
224
|
+
result = process_tag(result, "li") do |attrs, inner|
|
|
225
|
+
style = extract_all_style_attributes(attrs)
|
|
226
|
+
style_str = style_to_string(style)
|
|
227
|
+
class_attr = attrs["class"] || attrs["className"]
|
|
228
|
+
class_str = class_attr ? %( class="#{class_attr}") : ""
|
|
229
|
+
%(<li style="#{style_str}"#{class_str}>#{inner}</li>)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Process codeblock tags
|
|
233
|
+
result = process_tag(result, "codeblock") do |attrs, inner|
|
|
234
|
+
style = extract_all_style_attributes(attrs)
|
|
235
|
+
style["width"] ||= "100%"
|
|
236
|
+
style["box-sizing"] ||= "border-box"
|
|
237
|
+
style_str = style_to_string(style)
|
|
238
|
+
escaped = inner.gsub("<", "<").gsub(">", ">")
|
|
239
|
+
%(<pre style="#{style_str}"><code>#{escaped}</code></pre>)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Clean up wrapper tags
|
|
243
|
+
wrapper_patterns = [
|
|
244
|
+
/<sevk-email[^>]*>/i, /<\/sevk-email>/i,
|
|
245
|
+
/<sevk-body[^>]*>/i, /<\/sevk-body>/i,
|
|
246
|
+
/<email[^>]*>/i, /<\/email>/i,
|
|
247
|
+
/<mail[^>]*>/i, /<\/mail>/i,
|
|
248
|
+
/<body[^>]*>/i, /<\/body>/i
|
|
249
|
+
]
|
|
250
|
+
wrapper_patterns.each do |pattern|
|
|
251
|
+
result = result.gsub(pattern, "")
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
result.strip
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def process_button(attrs, inner)
|
|
258
|
+
href = attrs["href"] || "#"
|
|
259
|
+
style = extract_all_style_attributes(attrs)
|
|
260
|
+
|
|
261
|
+
# Parse padding
|
|
262
|
+
padding_top, padding_right, padding_bottom, padding_left = parse_padding(style)
|
|
263
|
+
|
|
264
|
+
y = padding_top + padding_bottom
|
|
265
|
+
text_raise = px_to_pt(y)
|
|
266
|
+
|
|
267
|
+
pl_font_width, pl_space_count = compute_font_width_and_space_count(padding_left)
|
|
268
|
+
pr_font_width, pr_space_count = compute_font_width_and_space_count(padding_right)
|
|
269
|
+
|
|
270
|
+
button_style = {
|
|
271
|
+
"line-height" => "100%",
|
|
272
|
+
"text-decoration" => "none",
|
|
273
|
+
"display" => "inline-block",
|
|
274
|
+
"max-width" => "100%",
|
|
275
|
+
"mso-padding-alt" => "0px"
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# Merge with extracted styles
|
|
279
|
+
button_style.merge!(style)
|
|
280
|
+
|
|
281
|
+
# Override padding with parsed values
|
|
282
|
+
button_style["padding-top"] = "#{padding_top}px"
|
|
283
|
+
button_style["padding-right"] = "#{padding_right}px"
|
|
284
|
+
button_style["padding-bottom"] = "#{padding_bottom}px"
|
|
285
|
+
button_style["padding-left"] = "#{padding_left}px"
|
|
286
|
+
|
|
287
|
+
style_str = style_to_string(button_style)
|
|
288
|
+
|
|
289
|
+
left_mso_spaces = " " * pl_space_count
|
|
290
|
+
right_mso_spaces = " " * pr_space_count
|
|
291
|
+
|
|
292
|
+
%(<a href="#{href}" target="_blank" style="#{style_str}"><!--[if mso]><i style="mso-font-width:#{(pl_font_width * 100).round}%;mso-text-raise:#{text_raise}" hidden>#{left_mso_spaces}</i><![endif]--><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:#{px_to_pt(padding_bottom)}">#{inner}</span><!--[if mso]><i style="mso-font-width:#{(pr_font_width * 100).round}%" hidden>#{right_mso_spaces}​</i><![endif]--></a>)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def parse_padding(style)
|
|
296
|
+
if style["padding"]
|
|
297
|
+
parts = style["padding"].split
|
|
298
|
+
case parts.length
|
|
299
|
+
when 1
|
|
300
|
+
val = parse_px(parts[0])
|
|
301
|
+
[val, val, val, val]
|
|
302
|
+
when 2
|
|
303
|
+
vertical = parse_px(parts[0])
|
|
304
|
+
horizontal = parse_px(parts[1])
|
|
305
|
+
[vertical, horizontal, vertical, horizontal]
|
|
306
|
+
when 4
|
|
307
|
+
[parse_px(parts[0]), parse_px(parts[1]), parse_px(parts[2]), parse_px(parts[3])]
|
|
308
|
+
else
|
|
309
|
+
[0, 0, 0, 0]
|
|
310
|
+
end
|
|
311
|
+
else
|
|
312
|
+
[
|
|
313
|
+
parse_px(style["padding-top"] || "0"),
|
|
314
|
+
parse_px(style["padding-right"] || "0"),
|
|
315
|
+
parse_px(style["padding-bottom"] || "0"),
|
|
316
|
+
parse_px(style["padding-left"] || "0")
|
|
317
|
+
]
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def parse_px(s)
|
|
322
|
+
s.to_s.gsub("px", "").to_i
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def px_to_pt(px)
|
|
326
|
+
(px * 3) / 4
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def compute_font_width_and_space_count(expected_width)
|
|
330
|
+
return [0, 0] if expected_width == 0
|
|
331
|
+
|
|
332
|
+
smallest_space_count = 0
|
|
333
|
+
max_font_width = 5.0
|
|
334
|
+
|
|
335
|
+
loop do
|
|
336
|
+
required_font_width = if smallest_space_count > 0
|
|
337
|
+
expected_width.to_f / smallest_space_count / 2.0
|
|
338
|
+
else
|
|
339
|
+
Float::INFINITY
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
return [required_font_width, smallest_space_count] if required_font_width <= max_font_width
|
|
343
|
+
|
|
344
|
+
smallest_space_count += 1
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def process_tag(content, tag_name)
|
|
349
|
+
result = content
|
|
350
|
+
open_pattern = /<#{tag_name}([^>]*)>/i
|
|
351
|
+
close_tag = "</#{tag_name}>"
|
|
352
|
+
open_tag_start = "<#{tag_name}"
|
|
353
|
+
|
|
354
|
+
max_iterations = 10_000
|
|
355
|
+
iterations = 0
|
|
356
|
+
|
|
357
|
+
while iterations < max_iterations
|
|
358
|
+
iterations += 1
|
|
359
|
+
|
|
360
|
+
# Find all opening tags
|
|
361
|
+
matches = result.to_enum(:scan, open_pattern).map { Regexp.last_match }
|
|
362
|
+
break if matches.empty?
|
|
363
|
+
|
|
364
|
+
processed = false
|
|
365
|
+
|
|
366
|
+
# Find the innermost tag (one that has no nested same tags)
|
|
367
|
+
matches.reverse_each do |match|
|
|
368
|
+
start = match.begin(0)
|
|
369
|
+
inner_start = match.end(0)
|
|
370
|
+
attrs_str = match[1]
|
|
371
|
+
|
|
372
|
+
# Find the next close tag after this opening tag
|
|
373
|
+
close_pos = result.downcase.index(close_tag.downcase, inner_start)
|
|
374
|
+
next unless close_pos
|
|
375
|
+
|
|
376
|
+
inner = result[inner_start...close_pos]
|
|
377
|
+
|
|
378
|
+
# Check if there's another opening tag inside
|
|
379
|
+
next if inner.downcase.include?(open_tag_start.downcase)
|
|
380
|
+
|
|
381
|
+
# This is an innermost tag, process it
|
|
382
|
+
attrs = parse_attributes(attrs_str)
|
|
383
|
+
replacement = yield(attrs, inner)
|
|
384
|
+
end_pos = close_pos + close_tag.length
|
|
385
|
+
|
|
386
|
+
result = result[0...start] + replacement + result[end_pos..]
|
|
387
|
+
processed = true
|
|
388
|
+
break
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
break unless processed
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
result
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def parse_attributes(attrs_str)
|
|
398
|
+
attrs = {}
|
|
399
|
+
attrs_str.scan(/([\w-]+)=["']([^"']*)["']/) do |key, value|
|
|
400
|
+
attrs[key] = value
|
|
401
|
+
end
|
|
402
|
+
attrs
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def extract_all_style_attributes(attrs)
|
|
406
|
+
style = {}
|
|
407
|
+
|
|
408
|
+
# Typography attributes
|
|
409
|
+
if attrs["text-color"]
|
|
410
|
+
style["color"] = attrs["text-color"]
|
|
411
|
+
elsif attrs["color"]
|
|
412
|
+
style["color"] = attrs["color"]
|
|
413
|
+
end
|
|
414
|
+
style["background-color"] = attrs["background-color"] if attrs["background-color"]
|
|
415
|
+
style["font-size"] = attrs["font-size"] if attrs["font-size"]
|
|
416
|
+
style["font-family"] = attrs["font-family"] if attrs["font-family"]
|
|
417
|
+
style["font-weight"] = attrs["font-weight"] if attrs["font-weight"]
|
|
418
|
+
style["line-height"] = attrs["line-height"] if attrs["line-height"]
|
|
419
|
+
style["text-align"] = attrs["text-align"] if attrs["text-align"]
|
|
420
|
+
style["text-decoration"] = attrs["text-decoration"] if attrs["text-decoration"]
|
|
421
|
+
|
|
422
|
+
# Dimensions
|
|
423
|
+
style["width"] = attrs["width"] if attrs["width"]
|
|
424
|
+
style["height"] = attrs["height"] if attrs["height"]
|
|
425
|
+
style["max-width"] = attrs["max-width"] if attrs["max-width"]
|
|
426
|
+
style["min-height"] = attrs["min-height"] if attrs["min-height"]
|
|
427
|
+
|
|
428
|
+
# Spacing - Padding
|
|
429
|
+
if attrs["padding"]
|
|
430
|
+
style["padding"] = attrs["padding"]
|
|
431
|
+
else
|
|
432
|
+
style["padding-top"] = attrs["padding-top"] if attrs["padding-top"]
|
|
433
|
+
style["padding-right"] = attrs["padding-right"] if attrs["padding-right"]
|
|
434
|
+
style["padding-bottom"] = attrs["padding-bottom"] if attrs["padding-bottom"]
|
|
435
|
+
style["padding-left"] = attrs["padding-left"] if attrs["padding-left"]
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Spacing - Margin
|
|
439
|
+
if attrs["margin"]
|
|
440
|
+
style["margin"] = attrs["margin"]
|
|
441
|
+
else
|
|
442
|
+
style["margin-top"] = attrs["margin-top"] if attrs["margin-top"]
|
|
443
|
+
style["margin-right"] = attrs["margin-right"] if attrs["margin-right"]
|
|
444
|
+
style["margin-bottom"] = attrs["margin-bottom"] if attrs["margin-bottom"]
|
|
445
|
+
style["margin-left"] = attrs["margin-left"] if attrs["margin-left"]
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Borders
|
|
449
|
+
if attrs["border"]
|
|
450
|
+
style["border"] = attrs["border"]
|
|
451
|
+
else
|
|
452
|
+
style["border-top"] = attrs["border-top"] if attrs["border-top"]
|
|
453
|
+
style["border-right"] = attrs["border-right"] if attrs["border-right"]
|
|
454
|
+
style["border-bottom"] = attrs["border-bottom"] if attrs["border-bottom"]
|
|
455
|
+
style["border-left"] = attrs["border-left"] if attrs["border-left"]
|
|
456
|
+
style["border-color"] = attrs["border-color"] if attrs["border-color"]
|
|
457
|
+
style["border-width"] = attrs["border-width"] if attrs["border-width"]
|
|
458
|
+
style["border-style"] = attrs["border-style"] if attrs["border-style"]
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# Border Radius
|
|
462
|
+
if attrs["border-radius"]
|
|
463
|
+
style["border-radius"] = attrs["border-radius"]
|
|
464
|
+
else
|
|
465
|
+
style["border-top-left-radius"] = attrs["border-top-left-radius"] if attrs["border-top-left-radius"]
|
|
466
|
+
style["border-top-right-radius"] = attrs["border-top-right-radius"] if attrs["border-top-right-radius"]
|
|
467
|
+
style["border-bottom-left-radius"] = attrs["border-bottom-left-radius"] if attrs["border-bottom-left-radius"]
|
|
468
|
+
style["border-bottom-right-radius"] = attrs["border-bottom-right-radius"] if attrs["border-bottom-right-radius"]
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
style
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def style_to_string(style)
|
|
475
|
+
style.map { |k, v| "#{k}:#{v}" }.join(";")
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def generate_html(content)
|
|
479
|
+
title = @head_settings.title.empty? ? "" : "<title>#{@head_settings.title}</title>"
|
|
480
|
+
|
|
481
|
+
font_links = @head_settings.fonts.map do |font|
|
|
482
|
+
%(<link href="#{CGI.escapeHTML(font.url)}" rel="stylesheet" type="text/css" />)
|
|
483
|
+
end.join("\n")
|
|
484
|
+
|
|
485
|
+
styles = @head_settings.styles.empty? ? "" : %(<style type="text/css">#{@head_settings.styles}</style>)
|
|
486
|
+
|
|
487
|
+
preview_text = ""
|
|
488
|
+
unless @head_settings.preview_text.empty?
|
|
489
|
+
preview_text = %(<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">#{CGI.escapeHTML(@head_settings.preview_text)}</div>)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
<<~HTML
|
|
493
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
494
|
+
<html lang="en" dir="ltr">
|
|
495
|
+
<head>
|
|
496
|
+
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
|
|
497
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
498
|
+
#{title}
|
|
499
|
+
#{font_links}
|
|
500
|
+
#{styles}
|
|
501
|
+
</head>
|
|
502
|
+
<body style="margin:0;padding:0;font-family:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;background-color:#ffffff">
|
|
503
|
+
#{preview_text}
|
|
504
|
+
#{content}
|
|
505
|
+
</body>
|
|
506
|
+
</html>
|
|
507
|
+
HTML
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# Module-level render method
|
|
512
|
+
def self.render(markup)
|
|
513
|
+
Renderer.new.render(markup)
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Sevk
|
|
6
|
+
module Resources
|
|
7
|
+
class Audiences < Base
|
|
8
|
+
def list(page: nil, limit: nil)
|
|
9
|
+
params = { page: page, limit: limit }
|
|
10
|
+
client.get("/audiences#{build_query_string(params)}")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def get(id)
|
|
14
|
+
client.get("/audiences/#{id}")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create(name:, description: nil, users_can_see: nil)
|
|
18
|
+
body = { name: name }
|
|
19
|
+
body[:description] = description if description
|
|
20
|
+
body[:usersCanSee] = users_can_see if users_can_see
|
|
21
|
+
client.post("/audiences", body)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def update(id, name: nil, description: nil, users_can_see: nil)
|
|
25
|
+
body = {}
|
|
26
|
+
body[:name] = name if name
|
|
27
|
+
body[:description] = description if description
|
|
28
|
+
body[:usersCanSee] = users_can_see if users_can_see
|
|
29
|
+
client.put("/audiences/#{id}", body)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def delete(id)
|
|
33
|
+
client.delete("/audiences/#{id}")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def add_contacts(audience_id, contact_ids)
|
|
37
|
+
client.post("/audiences/#{audience_id}/contacts", { contactIds: contact_ids })
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevk
|
|
4
|
+
module Resources
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :client
|
|
7
|
+
|
|
8
|
+
def initialize(client)
|
|
9
|
+
@client = client
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def build_query_string(params)
|
|
15
|
+
return "" if params.nil? || params.empty?
|
|
16
|
+
|
|
17
|
+
query = params.compact.map { |k, v| "#{k}=#{v}" }.join("&")
|
|
18
|
+
query.empty? ? "" : "?#{query}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Sevk
|
|
6
|
+
module Resources
|
|
7
|
+
class Broadcasts < Base
|
|
8
|
+
def list(page: nil, limit: nil, search: nil, status: nil)
|
|
9
|
+
params = { page: page, limit: limit, search: search, status: status }
|
|
10
|
+
client.get("/broadcasts#{build_query_string(params)}")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def get(id)
|
|
14
|
+
client.get("/broadcasts/#{id}")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Sevk
|
|
6
|
+
module Resources
|
|
7
|
+
class Contacts < Base
|
|
8
|
+
def list(page: nil, limit: nil, search: nil)
|
|
9
|
+
params = { page: page, limit: limit, search: search }
|
|
10
|
+
client.get("/contacts#{build_query_string(params)}")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def get(id)
|
|
14
|
+
client.get("/contacts/#{id}")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create(email:, first_name: nil, last_name: nil, subscribed: nil, metadata: nil)
|
|
18
|
+
body = { email: email }
|
|
19
|
+
body[:firstName] = first_name if first_name
|
|
20
|
+
body[:lastName] = last_name if last_name
|
|
21
|
+
body[:subscribed] = subscribed unless subscribed.nil?
|
|
22
|
+
body[:metadata] = metadata if metadata
|
|
23
|
+
client.post("/contacts", body)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update(id, first_name: nil, last_name: nil, subscribed: nil, metadata: nil)
|
|
27
|
+
body = {}
|
|
28
|
+
body[:firstName] = first_name if first_name
|
|
29
|
+
body[:lastName] = last_name if last_name
|
|
30
|
+
body[:subscribed] = subscribed unless subscribed.nil?
|
|
31
|
+
body[:metadata] = metadata if metadata
|
|
32
|
+
client.put("/contacts/#{id}", body)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def delete(id)
|
|
36
|
+
client.delete("/contacts/#{id}")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Sevk
|
|
6
|
+
module Resources
|
|
7
|
+
class Domains < Base
|
|
8
|
+
def list(verified: nil)
|
|
9
|
+
params = { verified: verified }
|
|
10
|
+
client.get("/domains#{build_query_string(params)}")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def get(id)
|
|
14
|
+
client.get("/domains/#{id}")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Sevk
|
|
6
|
+
module Resources
|
|
7
|
+
class Emails < Base
|
|
8
|
+
def send(to:, subject:, html:, from:, from_name: nil, reply_to: nil, text: nil, headers: nil, tags: nil)
|
|
9
|
+
body = {
|
|
10
|
+
to: to,
|
|
11
|
+
subject: subject,
|
|
12
|
+
html: html,
|
|
13
|
+
from: from
|
|
14
|
+
}
|
|
15
|
+
body[:fromName] = from_name if from_name
|
|
16
|
+
body[:replyTo] = reply_to if reply_to
|
|
17
|
+
body[:text] = text if text
|
|
18
|
+
body[:headers] = headers if headers
|
|
19
|
+
body[:tags] = tags if tags
|
|
20
|
+
client.post("emails", body)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|