sevk 0.1.0 → 1.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 +4 -4
- data/LICENSE +21 -0
- data/README.md +6 -10
- data/lib/sevk/client.rb +148 -58
- data/lib/sevk/error.rb +38 -5
- data/lib/sevk/markup/document_parser.rb +83 -0
- data/lib/sevk/markup/html_document.rb +82 -0
- data/lib/sevk/markup/number_formatting.rb +40 -0
- data/lib/sevk/markup/render_limits.rb +52 -0
- data/lib/sevk/markup/renderer.rb +282 -351
- data/lib/sevk/markup/style_helpers.rb +131 -0
- data/lib/sevk/markup/syntax_highlighting.rb +227 -0
- data/lib/sevk/markup/tag_scanner.rb +112 -0
- data/lib/sevk/markup/template_engine.rb +247 -0
- data/lib/sevk/resources/activity.rb +23 -0
- data/lib/sevk/resources/audiences.rb +30 -11
- data/lib/sevk/resources/base.rb +15 -3
- data/lib/sevk/resources/broadcasts.rb +91 -3
- data/lib/sevk/resources/contacts.rb +39 -12
- data/lib/sevk/resources/domains.rb +41 -3
- data/lib/sevk/resources/emails.rb +91 -9
- data/lib/sevk/resources/inbound.rb +123 -0
- data/lib/sevk/resources/outbound.rb +55 -0
- data/lib/sevk/resources/segments.rb +37 -15
- data/lib/sevk/resources/subscriptions.rb +18 -11
- data/lib/sevk/resources/templates.rb +20 -8
- data/lib/sevk/resources/topics.rb +37 -14
- data/lib/sevk/resources/usage.rb +15 -0
- data/lib/sevk/resources/webhooks.rb +52 -0
- data/lib/sevk/version.rb +1 -1
- data/lib/sevk/webhook_verifier.rb +91 -0
- data/lib/sevk.rb +24 -192
- metadata +25 -82
- data/Rakefile +0 -42
- data/sevk.gemspec +0 -40
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevk
|
|
4
|
+
module Markup
|
|
5
|
+
module StyleHelpers
|
|
6
|
+
STYLE_ORDER = %w[
|
|
7
|
+
color background-color background-image background-size background-position background-repeat
|
|
8
|
+
font-size font-family font-weight line-height text-align text-decoration
|
|
9
|
+
width height max-width max-height min-width min-height
|
|
10
|
+
padding padding-top padding-right padding-bottom padding-left
|
|
11
|
+
margin margin-top margin-right margin-bottom margin-left
|
|
12
|
+
border border-color border-width border-style
|
|
13
|
+
border-top border-right border-bottom border-left
|
|
14
|
+
border-radius border-top-left-radius border-top-right-radius
|
|
15
|
+
border-bottom-left-radius border-bottom-right-radius
|
|
16
|
+
border-collapse list-style-type vertical-align
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def parse_attributes(attrs_str)
|
|
22
|
+
attrs = {}
|
|
23
|
+
attrs_str.scan(/([\w-]+)=(?:"([^"]*)"|'([^']*)')/) do |key, dq_val, sq_val|
|
|
24
|
+
attrs[key] = dq_val.nil? || dq_val.empty? ? (sq_val || '') : dq_val
|
|
25
|
+
end
|
|
26
|
+
attrs
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def extract_all_style_attributes(attrs)
|
|
30
|
+
attrs = attrs.reject { |_, value| value.to_s.empty? }
|
|
31
|
+
style = {}
|
|
32
|
+
|
|
33
|
+
if attrs['text-color']
|
|
34
|
+
style['color'] = attrs['text-color']
|
|
35
|
+
elsif attrs['color']
|
|
36
|
+
style['color'] = attrs['color']
|
|
37
|
+
end
|
|
38
|
+
style['background-color'] = attrs['background-color'] if attrs['background-color']
|
|
39
|
+
# Background image group lives between background-color and font-size to
|
|
40
|
+
# match the reference extractStyleAttributes insertion order.
|
|
41
|
+
bg_image = attrs['background-image']
|
|
42
|
+
style['background-image'] = "url('#{bg_image}')" if bg_image
|
|
43
|
+
if attrs['background-size']
|
|
44
|
+
style['background-size'] = attrs['background-size']
|
|
45
|
+
elsif bg_image
|
|
46
|
+
style['background-size'] = 'cover'
|
|
47
|
+
end
|
|
48
|
+
if attrs['background-position']
|
|
49
|
+
style['background-position'] = attrs['background-position']
|
|
50
|
+
elsif bg_image
|
|
51
|
+
style['background-position'] = 'center'
|
|
52
|
+
end
|
|
53
|
+
if attrs['background-repeat']
|
|
54
|
+
style['background-repeat'] = attrs['background-repeat']
|
|
55
|
+
elsif bg_image
|
|
56
|
+
style['background-repeat'] = 'no-repeat'
|
|
57
|
+
end
|
|
58
|
+
style['font-size'] = attrs['font-size'] if attrs['font-size']
|
|
59
|
+
style['font-family'] = attrs['font-family'] if attrs['font-family']
|
|
60
|
+
style['font-weight'] = attrs['font-weight'] if attrs['font-weight']
|
|
61
|
+
style['line-height'] = attrs['line-height'] if attrs['line-height']
|
|
62
|
+
style['text-align'] = attrs['text-align'] if attrs['text-align']
|
|
63
|
+
style['text-decoration'] = attrs['text-decoration'] if attrs['text-decoration']
|
|
64
|
+
|
|
65
|
+
style['width'] = attrs['width'] if attrs['width']
|
|
66
|
+
style['height'] = attrs['height'] if attrs['height']
|
|
67
|
+
style['max-width'] = attrs['max-width'] if attrs['max-width']
|
|
68
|
+
style['max-height'] = attrs['max-height'] if attrs['max-height']
|
|
69
|
+
style['min-width'] = attrs['min-width'] if attrs['min-width']
|
|
70
|
+
style['min-height'] = attrs['min-height'] if attrs['min-height']
|
|
71
|
+
|
|
72
|
+
# Spacing - Padding (shorthand + per-side both emit; per-side wins per CSS)
|
|
73
|
+
style['padding'] = attrs['padding'] if attrs['padding']
|
|
74
|
+
style['padding-top'] = attrs['padding-top'] if attrs['padding-top']
|
|
75
|
+
style['padding-right'] = attrs['padding-right'] if attrs['padding-right']
|
|
76
|
+
style['padding-bottom'] = attrs['padding-bottom'] if attrs['padding-bottom']
|
|
77
|
+
style['padding-left'] = attrs['padding-left'] if attrs['padding-left']
|
|
78
|
+
|
|
79
|
+
# Spacing - Margin (shorthand + per-side both emit; per-side wins per CSS)
|
|
80
|
+
style['margin'] = attrs['margin'] if attrs['margin']
|
|
81
|
+
style['margin-top'] = attrs['margin-top'] if attrs['margin-top']
|
|
82
|
+
style['margin-right'] = attrs['margin-right'] if attrs['margin-right']
|
|
83
|
+
style['margin-bottom'] = attrs['margin-bottom'] if attrs['margin-bottom']
|
|
84
|
+
style['margin-left'] = attrs['margin-left'] if attrs['margin-left']
|
|
85
|
+
|
|
86
|
+
# Borders cascade: shorthand -> per-property -> per-side (most specific last)
|
|
87
|
+
style['border'] = attrs['border'] if attrs['border']
|
|
88
|
+
style['border-color'] = attrs['border-color'] if attrs['border-color']
|
|
89
|
+
style['border-width'] = attrs['border-width'] if attrs['border-width']
|
|
90
|
+
style['border-style'] = attrs['border-style'] if attrs['border-style']
|
|
91
|
+
style['border-top'] = attrs['border-top'] if attrs['border-top']
|
|
92
|
+
style['border-right'] = attrs['border-right'] if attrs['border-right']
|
|
93
|
+
style['border-bottom'] = attrs['border-bottom'] if attrs['border-bottom']
|
|
94
|
+
style['border-left'] = attrs['border-left'] if attrs['border-left']
|
|
95
|
+
|
|
96
|
+
# Border Radius (shorthand + per-corner both emit; per-corner wins per CSS)
|
|
97
|
+
style['border-radius'] = attrs['border-radius'] if attrs['border-radius']
|
|
98
|
+
style['border-top-left-radius'] = attrs['border-top-left-radius'] if attrs['border-top-left-radius']
|
|
99
|
+
style['border-top-right-radius'] = attrs['border-top-right-radius'] if attrs['border-top-right-radius']
|
|
100
|
+
style['border-bottom-left-radius'] = attrs['border-bottom-left-radius'] if attrs['border-bottom-left-radius']
|
|
101
|
+
style['border-bottom-right-radius'] = attrs['border-bottom-right-radius'] if attrs['border-bottom-right-radius']
|
|
102
|
+
|
|
103
|
+
style
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def class_attribute(attrs)
|
|
107
|
+
value = attrs['class'].to_s.empty? ? attrs['className'] : attrs['class']
|
|
108
|
+
value.to_s.empty? ? '' : %( class="#{value}")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def style_to_string(style)
|
|
112
|
+
seen = {}
|
|
113
|
+
parts = []
|
|
114
|
+
STYLE_ORDER.each do |key|
|
|
115
|
+
value = style[key]
|
|
116
|
+
next if value.nil? || value == ''
|
|
117
|
+
|
|
118
|
+
parts << "#{key}:#{value}"
|
|
119
|
+
seen[key] = true
|
|
120
|
+
end
|
|
121
|
+
style.each do |key, value|
|
|
122
|
+
next if seen[key]
|
|
123
|
+
next if value.nil? || value == ''
|
|
124
|
+
|
|
125
|
+
parts << "#{key}:#{value}"
|
|
126
|
+
end
|
|
127
|
+
parts.join(';')
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevk
|
|
4
|
+
module Markup
|
|
5
|
+
module SyntaxHighlighting
|
|
6
|
+
MAX_CODEBLOCK_LENGTH = 2500
|
|
7
|
+
MAX_TOTAL_CODEBLOCK_LENGTH = 2500
|
|
8
|
+
|
|
9
|
+
LANGUAGES = %w[
|
|
10
|
+
plain extend plaintext text txt markup html mathml svg xml ssml atom rss css clike javascript js typescript ts
|
|
11
|
+
jsx tsx python py java c cpp csharp cs dotnet php ruby rb go rust sql bash sh shell json webmanifest
|
|
12
|
+
yaml yml markdown md
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
LANGUAGE_ALIASES = { 'jsx' => 'javascript', 'tsx' => 'typescript' }.freeze
|
|
16
|
+
|
|
17
|
+
ROUGE_LEXERS = {
|
|
18
|
+
'markup' => 'html', 'mathml' => 'xml', 'svg' => 'xml', 'ssml' => 'xml', 'atom' => 'xml', 'rss' => 'xml',
|
|
19
|
+
'dotnet' => 'csharp', 'webmanifest' => 'json', 'plain' => nil, 'plaintext' => nil, 'text' => nil,
|
|
20
|
+
'txt' => nil, 'clike' => nil
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
THEME_BASES = {
|
|
24
|
+
'vscDarkPlus' => {
|
|
25
|
+
'color' => '#d4d4d4', 'fontSize' => '13px', 'textShadow' => 'none',
|
|
26
|
+
'fontFamily' => "Menlo, Monaco, Consolas, 'Andale Mono', 'Ubuntu Mono', 'Courier New', monospace",
|
|
27
|
+
'direction' => 'ltr', 'textAlign' => 'left', 'whiteSpace' => 'pre', 'wordSpacing' => 'normal',
|
|
28
|
+
'wordBreak' => 'normal', 'lineHeight' => '1.5', 'MozTabSize' => '4', 'OTabSize' => '4', 'tabSize' => '4',
|
|
29
|
+
'WebkitHyphens' => 'none', 'MozHyphens' => 'none', 'hyphens' => 'none', 'padding' => '16px',
|
|
30
|
+
'overflow' => 'auto', 'backgroundColor' => '#1e1e1e'
|
|
31
|
+
},
|
|
32
|
+
'vs' => {
|
|
33
|
+
'color' => '#393A34',
|
|
34
|
+
'fontFamily' => "'Consolas', 'Bitstream Vera Sans Mono', 'Courier New', Courier, monospace",
|
|
35
|
+
'direction' => 'ltr', 'textAlign' => 'left', 'whiteSpace' => 'pre', 'wordSpacing' => 'normal',
|
|
36
|
+
'wordBreak' => 'normal', 'fontSize' => '.9em', 'lineHeight' => '1.2em', 'MozTabSize' => '4',
|
|
37
|
+
'OTabSize' => '4', 'tabSize' => '4', 'WebkitHyphens' => 'none', 'MozHyphens' => 'none',
|
|
38
|
+
'hyphens' => 'none', 'padding' => '16px', 'overflow' => 'auto', 'border' => '1px solid #dddddd',
|
|
39
|
+
'backgroundColor' => 'white'
|
|
40
|
+
},
|
|
41
|
+
'oneDark' => {
|
|
42
|
+
'backgroundColor' => '#282c34', 'color' => '#abb2bf', 'textShadow' => '0 1px rgba(0, 0, 0, 0.3)',
|
|
43
|
+
'fontFamily' => "'Fira Code', 'Fira Mono', Menlo, Consolas, 'DejaVu Sans Mono', monospace",
|
|
44
|
+
'direction' => 'ltr', 'textAlign' => 'left', 'whiteSpace' => 'pre', 'wordSpacing' => 'normal',
|
|
45
|
+
'wordBreak' => 'normal', 'lineHeight' => '1.5', 'MozTabSize' => '2', 'OTabSize' => '2', 'tabSize' => '2',
|
|
46
|
+
'WebkitHyphens' => 'none', 'MozHyphens' => 'none', 'hyphens' => 'none', 'padding' => '16px',
|
|
47
|
+
'overflow' => 'auto', 'borderRadius' => '6px'
|
|
48
|
+
},
|
|
49
|
+
'oneLight' => {
|
|
50
|
+
'backgroundColor' => '#fafafa', 'color' => '#383a42',
|
|
51
|
+
'fontFamily' => "'Fira Code', 'Fira Mono', Menlo, Consolas, 'DejaVu Sans Mono', monospace",
|
|
52
|
+
'direction' => 'ltr', 'textAlign' => 'left', 'whiteSpace' => 'pre', 'wordSpacing' => 'normal',
|
|
53
|
+
'wordBreak' => 'normal', 'lineHeight' => '1.5', 'MozTabSize' => '2', 'OTabSize' => '2', 'tabSize' => '2',
|
|
54
|
+
'WebkitHyphens' => 'none', 'MozHyphens' => 'none', 'hyphens' => 'none', 'padding' => '16px',
|
|
55
|
+
'overflow' => 'auto', 'borderRadius' => '6px'
|
|
56
|
+
}
|
|
57
|
+
}.freeze
|
|
58
|
+
|
|
59
|
+
THEME_TOKENS = {
|
|
60
|
+
'vscDarkPlus' => {
|
|
61
|
+
'comment' => { 'color' => '#6a9955' }, 'prolog' => { 'color' => '#6a9955' },
|
|
62
|
+
'punctuation' => { 'color' => '#d4d4d4' }, 'property' => { 'color' => '#9cdcfe' },
|
|
63
|
+
'tag' => { 'color' => '#569cd6' }, 'boolean' => { 'color' => '#569cd6' },
|
|
64
|
+
'number' => { 'color' => '#b5cea8' }, 'constant' => { 'color' => '#9cdcfe' },
|
|
65
|
+
'symbol' => { 'color' => '#b5cea8' }, 'inserted' => { 'color' => '#b5cea8' },
|
|
66
|
+
'selector' => { 'color' => '#d7ba7d' }, 'attr-name' => { 'color' => '#9cdcfe' },
|
|
67
|
+
'string' => { 'color' => '#ce9178' }, 'char' => { 'color' => '#ce9178' },
|
|
68
|
+
'builtin' => { 'color' => '#ce9178' }, 'deleted' => { 'color' => '#ce9178' },
|
|
69
|
+
'operator' => { 'color' => '#d4d4d4' }, 'entity' => { 'color' => '#569cd6' },
|
|
70
|
+
'atrule' => { 'color' => '#ce9178' }, 'keyword' => { 'color' => '#569CD6' },
|
|
71
|
+
'function' => { 'color' => '#dcdcaa' }, 'regex' => { 'color' => '#d16969' },
|
|
72
|
+
'important' => { 'color' => '#569cd6' }, 'italic' => { 'font-style' => 'italic' },
|
|
73
|
+
'class-name' => { 'color' => '#4ec9b0' }, 'variable' => { 'color' => '#9cdcfe' },
|
|
74
|
+
'attr-value' => { 'color' => '#ce9178' }, 'cdata' => { 'color' => '#808080' },
|
|
75
|
+
'namespace' => { 'color' => '#4ec9b0' }
|
|
76
|
+
},
|
|
77
|
+
'vs' => {
|
|
78
|
+
'comment' => { 'color' => '#008000', 'font-style' => 'italic' },
|
|
79
|
+
'prolog' => { 'color' => '#008000', 'font-style' => 'italic' },
|
|
80
|
+
'doctype' => { 'color' => '#008000', 'font-style' => 'italic' },
|
|
81
|
+
'cdata' => { 'color' => '#008000', 'font-style' => 'italic' }, 'namespace' => { 'opacity' => '.7' },
|
|
82
|
+
'string' => { 'color' => '#A31515' }, 'punctuation' => { 'color' => '#393A34' },
|
|
83
|
+
'operator' => { 'color' => '#393A34' }, 'url' => { 'color' => '#36acaa' },
|
|
84
|
+
'symbol' => { 'color' => '#36acaa' }, 'number' => { 'color' => '#36acaa' },
|
|
85
|
+
'boolean' => { 'color' => '#36acaa' }, 'variable' => { 'color' => '#36acaa' },
|
|
86
|
+
'constant' => { 'color' => '#36acaa' }, 'inserted' => { 'color' => '#36acaa' },
|
|
87
|
+
'atrule' => { 'color' => '#0000ff' }, 'keyword' => { 'color' => '#0000ff' },
|
|
88
|
+
'attr-value' => { 'color' => '#0000ff' }, 'function' => { 'color' => '#393A34' },
|
|
89
|
+
'deleted' => { 'color' => '#9a050f' }, 'selector' => { 'color' => '#800000' },
|
|
90
|
+
'important' => { 'color' => '#e90', 'font-weight' => 'bold' }, 'bold' => { 'font-weight' => 'bold' },
|
|
91
|
+
'italic' => { 'font-style' => 'italic' }, 'class-name' => { 'color' => '#2B91AF' },
|
|
92
|
+
'tag' => { 'color' => '#800000' }, 'attr-name' => { 'color' => '#ff0000' },
|
|
93
|
+
'property' => { 'color' => '#ff0000' }, 'regex' => { 'color' => '#ff0000' },
|
|
94
|
+
'entity' => { 'color' => '#ff0000' }
|
|
95
|
+
},
|
|
96
|
+
'oneDark' => {
|
|
97
|
+
'comment' => { 'color' => '#5c6370', 'font-style' => 'italic' }, 'prolog' => { 'color' => '#5c6370' },
|
|
98
|
+
'cdata' => { 'color' => '#5c6370' }, 'doctype' => { 'color' => '#abb2bf' },
|
|
99
|
+
'punctuation' => { 'color' => '#abb2bf' }, 'entity' => { 'color' => '#abb2bf', 'cursor' => 'help' },
|
|
100
|
+
'attr-name' => { 'color' => '#d19a66' }, 'class-name' => { 'color' => '#d19a66' },
|
|
101
|
+
'boolean' => { 'color' => '#d19a66' }, 'constant' => { 'color' => '#d19a66' },
|
|
102
|
+
'number' => { 'color' => '#d19a66' }, 'atrule' => { 'color' => '#d19a66' },
|
|
103
|
+
'keyword' => { 'color' => '#c678dd' }, 'property' => { 'color' => '#e06c75' },
|
|
104
|
+
'tag' => { 'color' => '#e06c75' }, 'symbol' => { 'color' => '#e06c75' },
|
|
105
|
+
'deleted' => { 'color' => '#e06c75' }, 'important' => { 'color' => '#e06c75' },
|
|
106
|
+
'selector' => { 'color' => '#98c379' }, 'string' => { 'color' => '#98c379' },
|
|
107
|
+
'char' => { 'color' => '#98c379' }, 'builtin' => { 'color' => '#98c379' },
|
|
108
|
+
'inserted' => { 'color' => '#98c379' }, 'regex' => { 'color' => '#98c379' },
|
|
109
|
+
'attr-value' => { 'color' => '#98c379' }, 'variable' => { 'color' => '#61afef' },
|
|
110
|
+
'operator' => { 'color' => '#61afef' }, 'function' => { 'color' => '#61afef' },
|
|
111
|
+
'url' => { 'color' => '#56b6c2' }, 'bold' => { 'font-weight' => 'bold' },
|
|
112
|
+
'italic' => { 'font-style' => 'italic' }, 'namespace' => { 'opacity' => '0.8' }
|
|
113
|
+
},
|
|
114
|
+
'oneLight' => {
|
|
115
|
+
'comment' => { 'color' => '#a0a1a7', 'font-style' => 'italic' }, 'prolog' => { 'color' => '#a0a1a7' },
|
|
116
|
+
'cdata' => { 'color' => '#a0a1a7' }, 'doctype' => { 'color' => '#383a42' },
|
|
117
|
+
'punctuation' => { 'color' => '#383a42' }, 'entity' => { 'color' => '#383a42', 'cursor' => 'help' },
|
|
118
|
+
'attr-name' => { 'color' => '#b76b01' }, 'class-name' => { 'color' => '#b76b01' },
|
|
119
|
+
'boolean' => { 'color' => '#b76b01' }, 'constant' => { 'color' => '#b76b01' },
|
|
120
|
+
'number' => { 'color' => '#b76b01' }, 'atrule' => { 'color' => '#b76b01' },
|
|
121
|
+
'keyword' => { 'color' => '#a626a4' }, 'property' => { 'color' => '#e45649' },
|
|
122
|
+
'tag' => { 'color' => '#e45649' }, 'symbol' => { 'color' => '#e45649' },
|
|
123
|
+
'deleted' => { 'color' => '#e45649' }, 'important' => { 'color' => '#e45649' },
|
|
124
|
+
'selector' => { 'color' => '#50a14f' }, 'string' => { 'color' => '#50a14f' },
|
|
125
|
+
'char' => { 'color' => '#50a14f' }, 'builtin' => { 'color' => '#50a14f' },
|
|
126
|
+
'inserted' => { 'color' => '#50a14f' }, 'regex' => { 'color' => '#50a14f' },
|
|
127
|
+
'attr-value' => { 'color' => '#50a14f' }, 'variable' => { 'color' => '#4078f2' },
|
|
128
|
+
'operator' => { 'color' => '#4078f2' }, 'function' => { 'color' => '#4078f2' },
|
|
129
|
+
'url' => { 'color' => '#0184bc' }, 'bold' => { 'font-weight' => 'bold' },
|
|
130
|
+
'italic' => { 'font-style' => 'italic' }, 'namespace' => { 'opacity' => '0.8' }
|
|
131
|
+
}
|
|
132
|
+
}.freeze
|
|
133
|
+
|
|
134
|
+
ROUGE_TOKENS = {
|
|
135
|
+
'Comment' => 'comment', 'Keyword' => 'keyword', 'Keyword.Constant' => 'boolean', 'Keyword.Type' => 'class-name',
|
|
136
|
+
'Literal.String' => 'string', 'Literal.String.Regex' => 'regex', 'Literal.String.Symbol' => 'symbol',
|
|
137
|
+
'Literal.String.Escape' => 'string', 'Literal.Number' => 'number', 'Name.Function' => 'function',
|
|
138
|
+
'Name.Class' => 'class-name', 'Name.Constant' => 'constant', 'Name.Builtin' => 'builtin',
|
|
139
|
+
'Name.Variable' => 'variable', 'Name.Tag' => 'tag', 'Name.Attribute' => 'attr-name',
|
|
140
|
+
'Name.Decorator' => 'function', 'Name.Namespace' => 'namespace', 'Name.Entity' => 'entity',
|
|
141
|
+
'Name.Label' => 'symbol', 'Name.Property' => 'property', 'Operator' => 'operator',
|
|
142
|
+
'Operator.Word' => 'keyword', 'Punctuation' => 'punctuation', 'Generic.Deleted' => 'deleted',
|
|
143
|
+
'Generic.Inserted' => 'inserted', 'Generic.Emph' => 'italic', 'Generic.Strong' => 'bold',
|
|
144
|
+
'Generic.Heading' => 'important', 'Generic.Subheading' => 'important'
|
|
145
|
+
}.freeze
|
|
146
|
+
|
|
147
|
+
TOKEN_SPACE = "\u00A0\u200D\u200B"
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def process_codeblock(attrs, code)
|
|
152
|
+
requested = (attrs['language'].to_s.empty? ? 'javascript' : attrs['language']).downcase
|
|
153
|
+
language = LANGUAGE_ALIASES.fetch(requested, requested)
|
|
154
|
+
theme = THEME_BASES.key?(attrs['theme'].to_s) ? attrs['theme'] : 'oneDark'
|
|
155
|
+
custom_style = extract_all_style_attributes(attrs)
|
|
156
|
+
|
|
157
|
+
return '<pre><code></code></pre>' if code.empty?
|
|
158
|
+
|
|
159
|
+
length = code.length + code.count("\u{10000}-\u{10FFFF}")
|
|
160
|
+
|
|
161
|
+
if !LANGUAGES.include?(language) || length > MAX_CODEBLOCK_LENGTH ||
|
|
162
|
+
@codeblock_budget + length > MAX_TOTAL_CODEBLOCK_LENGTH
|
|
163
|
+
style = pre_style(THEME_BASES[theme], { 'width' => '100%' }, custom_style)
|
|
164
|
+
escaped = code.gsub('<', '<').gsub('>', '>')
|
|
165
|
+
return %(<pre style="#{style}"><code>#{escaped}</code></pre>)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
@codeblock_budget += length
|
|
169
|
+
inherited = attrs['font-family'].to_s.empty? ? {} : { 'font-family' => attrs['font-family'] }
|
|
170
|
+
lines = highlight_lines(code, language, theme, inherited)
|
|
171
|
+
numbered = attrs['line-numbers'] == 'true'
|
|
172
|
+
html = lines.each_with_index.map do |line, index|
|
|
173
|
+
number = numbered ? %(<span style="width:2em;display:inline-block">#{index + 1}</span>) : ''
|
|
174
|
+
%(<p style="margin:0;min-height:1em">#{number}#{line}</p>)
|
|
175
|
+
end.join
|
|
176
|
+
style = pre_style(THEME_BASES[theme], { 'width' => '100%', 'box-sizing' => 'border-box' }, custom_style)
|
|
177
|
+
%(<pre style="#{style}"><code>#{html}</code></pre>)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def pre_style(*layers)
|
|
181
|
+
style = {}
|
|
182
|
+
layers.each { |layer| layer.each { |key, value| style[key] = value } }
|
|
183
|
+
style.map do |key, value|
|
|
184
|
+
property = key.gsub(/[A-Z]/) { |letter| "-#{letter.downcase}" }
|
|
185
|
+
property = "-#{property}" if property.start_with?('ms-')
|
|
186
|
+
"#{property}:#{value}"
|
|
187
|
+
end.join(';')
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def highlight_lines(code, language, theme, inherited)
|
|
191
|
+
lexer_name = ROUGE_LEXERS.fetch(language, language)
|
|
192
|
+
lexer = lexer_name && Rouge::Lexer.find(lexer_name)&.new
|
|
193
|
+
tokens = [[nil, code]]
|
|
194
|
+
tokens = lexer.lex(code).map { |type, text| [token_style(theme, type, inherited), text] } if lexer
|
|
195
|
+
inherited_style = style_to_string(inherited)
|
|
196
|
+
lines = [+'']
|
|
197
|
+
|
|
198
|
+
tokens.each do |style, text|
|
|
199
|
+
style = inherited_style if style.nil? && !inherited.empty?
|
|
200
|
+
text.split("\n", -1).each_with_index do |part, index|
|
|
201
|
+
lines << +'' if index.positive?
|
|
202
|
+
next if part.empty?
|
|
203
|
+
|
|
204
|
+
escaped = part.gsub('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"')
|
|
205
|
+
.gsub("'", ''').gsub(' ', TOKEN_SPACE)
|
|
206
|
+
lines.last << (style.nil? || style.empty? ? escaped : %(<span style="#{style}">#{escaped}</span>))
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
lines
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def token_style(theme, type, inherited)
|
|
214
|
+
qualname = type.qualname
|
|
215
|
+
token = nil
|
|
216
|
+
until token || qualname.nil?
|
|
217
|
+
token = ROUGE_TOKENS[qualname]
|
|
218
|
+
dot = qualname.rindex('.')
|
|
219
|
+
qualname = dot ? qualname[0...dot] : nil
|
|
220
|
+
end
|
|
221
|
+
return nil if token.nil?
|
|
222
|
+
|
|
223
|
+
style_to_string(inherited.merge(THEME_TOKENS[theme].fetch(token, {})))
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevk
|
|
4
|
+
module Markup
|
|
5
|
+
module TagScanner
|
|
6
|
+
TagPatterns = Struct.new(:open, :close, :nested, :close_length)
|
|
7
|
+
|
|
8
|
+
def tag_patterns(tag_name)
|
|
9
|
+
TagPatterns.new(/<#{tag_name}([^>]*)>/i, %r{</#{tag_name}>}i, /<#{tag_name}/i, tag_name.length + 3)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def replace_self_closing(source, tag, self_close_only: false)
|
|
13
|
+
open_pattern = /<#{tag}/i
|
|
14
|
+
name_length = tag.length + 1
|
|
15
|
+
output = +''
|
|
16
|
+
last_index = 0
|
|
17
|
+
position = 0
|
|
18
|
+
|
|
19
|
+
while (opening = source.index(open_pattern, position))
|
|
20
|
+
closing_bracket = source.index('>', opening + name_length)
|
|
21
|
+
break unless closing_bracket
|
|
22
|
+
|
|
23
|
+
position = closing_bracket + 1
|
|
24
|
+
next if self_close_only && source[closing_bracket - 1] != '/'
|
|
25
|
+
|
|
26
|
+
attributes_end = self_close_only ? closing_bracket - 1 : closing_bracket
|
|
27
|
+
output << source[last_index...opening] << yield(source[(opening + name_length)...attributes_end])
|
|
28
|
+
last_index = closing_bracket + 1
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
output << source[last_index..]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def get_first_tag_content(source, tag)
|
|
35
|
+
scanner = StringScanner.new(source)
|
|
36
|
+
return nil unless scanner.skip_until(/<#{tag}/i) && scanner.skip_until(/>/)
|
|
37
|
+
|
|
38
|
+
content_start = scanner.pos
|
|
39
|
+
return nil unless scanner.skip_until(%r{</#{tag}>}i)
|
|
40
|
+
|
|
41
|
+
source.byteslice(content_start, scanner.pos - (tag.length + 3) - content_start)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def get_all_tag_contents(source, tag)
|
|
45
|
+
open_pattern = /<#{tag}/i
|
|
46
|
+
close_pattern = %r{</#{tag}>}i
|
|
47
|
+
scanner = StringScanner.new(source)
|
|
48
|
+
contents = []
|
|
49
|
+
|
|
50
|
+
while scanner.skip_until(open_pattern) && scanner.skip_until(/>/)
|
|
51
|
+
content_start = scanner.pos
|
|
52
|
+
break unless scanner.skip_until(close_pattern)
|
|
53
|
+
|
|
54
|
+
contents << source.byteslice(content_start, scanner.pos - (tag.length + 3) - content_start)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
contents
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def strip_tag_pairs(source, tag)
|
|
61
|
+
open_pattern = /<#{tag}/i
|
|
62
|
+
close_pattern = %r{</#{tag}>}i
|
|
63
|
+
scanner = StringScanner.new(source)
|
|
64
|
+
output = +''
|
|
65
|
+
last_index = 0
|
|
66
|
+
|
|
67
|
+
while scanner.skip_until(open_pattern)
|
|
68
|
+
opening = scanner.pos - scanner.matched_size
|
|
69
|
+
break unless scanner.skip_until(/>/) && scanner.skip_until(close_pattern)
|
|
70
|
+
|
|
71
|
+
output << source.byteslice(last_index, opening - last_index)
|
|
72
|
+
last_index = scanner.pos
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
output << source.byteslice(last_index, source.bytesize - last_index)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def get_tag_attributes(source, tag)
|
|
79
|
+
open_pattern = /<#{tag}/i
|
|
80
|
+
scanner = StringScanner.new(source)
|
|
81
|
+
attributes = []
|
|
82
|
+
|
|
83
|
+
while scanner.skip_until(open_pattern)
|
|
84
|
+
attributes_start = scanner.pos
|
|
85
|
+
break unless scanner.skip_until(/>/)
|
|
86
|
+
|
|
87
|
+
attributes << source.byteslice(attributes_start, scanner.pos - 1 - attributes_start)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
attributes
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def find_hidden_preview_div(html)
|
|
94
|
+
position = 0
|
|
95
|
+
|
|
96
|
+
while (opening = html.index(/<div/i, position))
|
|
97
|
+
closing_bracket = html.index('>', opening + 4)
|
|
98
|
+
return nil unless closing_bracket
|
|
99
|
+
|
|
100
|
+
if html[opening..closing_bracket].match?(/style=["'][^"']*display:\s*none/i)
|
|
101
|
+
closing = html.index(%r{</div>}i, closing_bracket + 1)
|
|
102
|
+
return nil unless closing
|
|
103
|
+
|
|
104
|
+
return [html[opening...(closing + 6)], html[(closing_bracket + 1)...closing]]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
position = closing_bracket + 1
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|