slacken 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +20 -0
- data/README.md +60 -0
- data/Rakefile +36 -0
- data/lib/slacken.rb +21 -0
- data/lib/slacken/document_component.rb +73 -0
- data/lib/slacken/document_component/elim_blanks.rb +36 -0
- data/lib/slacken/document_component/elim_invalid_links.rb +26 -0
- data/lib/slacken/document_component/elim_line_breaks.rb +27 -0
- data/lib/slacken/document_component/extract_img_alt.rb +12 -0
- data/lib/slacken/document_component/group_indent.rb +27 -0
- data/lib/slacken/document_component/group_inlines.rb +26 -0
- data/lib/slacken/document_component/sanitize_link_containers.rb +40 -0
- data/lib/slacken/document_component/sanitize_special_tag_containers.rb +102 -0
- data/lib/slacken/document_component/stringfy_checkbox.rb +20 -0
- data/lib/slacken/document_component/stringfy_emoji.rb +20 -0
- data/lib/slacken/dom_container.rb +45 -0
- data/lib/slacken/node_type.rb +53 -0
- data/lib/slacken/render_element.rb +91 -0
- data/lib/slacken/rendering.rb +102 -0
- data/lib/slacken/slack_url.rb +7 -0
- data/lib/slacken/table_element.rb +23 -0
- data/lib/slacken/version.rb +3 -0
- data/sample/out.txt +38 -0
- data/sample/source.html +55 -0
- data/scripts/update_markup_fixture.rb +8 -0
- data/slacken.gemspec +28 -0
- data/spec/fixtures/example.html +114 -0
- data/spec/fixtures/markup_text.txt +73 -0
- data/spec/helpers/document_component_dsl.rb +11 -0
- data/spec/slacken/document_component/elim_blanks_spec.rb +34 -0
- data/spec/slacken/document_component/elim_invalid_links_spec.rb +49 -0
- data/spec/slacken/document_component/elim_line_breaks_spec.rb +41 -0
- data/spec/slacken/document_component/group_indent_spec.rb +37 -0
- data/spec/slacken/document_component/group_inlines_spec.rb +33 -0
- data/spec/slacken/document_component/sanitize_special_tag_containers_spec.rb +64 -0
- data/spec/slacken/document_component_spec.rb +19 -0
- data/spec/slacken_spec.rb +12 -0
- data/spec/spec_helper.rb +13 -0
- metadata +194 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
class Slacken::DocumentComponent
|
2
|
+
module StringfyCheckbox
|
3
|
+
# Private: Reject blank elements
|
4
|
+
def stringfy_checkbox
|
5
|
+
if type.member_of?(:input) && attrs[:type] == 'checkbox'
|
6
|
+
self.class.new(:checkbox, [], checked: attrs[:checked])
|
7
|
+
else
|
8
|
+
derive(children.map(&:stringfy_checkbox))
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def checkbox_stringfied?
|
13
|
+
if type.member_of?(:input) && attrs[:type] == 'checkbox'
|
14
|
+
false
|
15
|
+
else
|
16
|
+
children.all?(&:checkbox_stringfied?)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Slacken::DocumentComponent
|
2
|
+
module StringfyEmoji
|
3
|
+
# Private: Reject blank elements
|
4
|
+
def stringfy_emoji
|
5
|
+
if type.member_of?(:img) && attrs[:class].include?('emoji')
|
6
|
+
self.class.new(:emoji, [], content: attrs[:alt])
|
7
|
+
else
|
8
|
+
derive(children.map(&:stringfy_emoji))
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def emoji_stringfied?
|
13
|
+
if type.member_of?(:img) && attrs[:class].include?('emoji')
|
14
|
+
false
|
15
|
+
else
|
16
|
+
children.all?(&:emoji_stringfied?)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
# Public: a DOM tree container parsed by Nokogiri.
|
4
|
+
module Slacken
|
5
|
+
class DomContainer
|
6
|
+
attr_reader :root
|
7
|
+
|
8
|
+
# Public: Parse a html source with nokogiri and create a container.
|
9
|
+
def self.parse_html(body)
|
10
|
+
new(Nokogiri::HTML(body))
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(root)
|
14
|
+
@root = root
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_component(node = root)
|
18
|
+
children = node.children.map { |nd| to_component(nd) }.compact
|
19
|
+
leave(node, children)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def leave(node, children)
|
25
|
+
if !(node.respond_to?(:html_dtd?) && node.html_dtd?)
|
26
|
+
DocumentComponent.new(node.name.downcase, children, attrs_of(node))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def attrs_of(node)
|
31
|
+
case node.name.to_sym
|
32
|
+
when :text
|
33
|
+
{ content: node.content }
|
34
|
+
when :iframe, :a
|
35
|
+
{ href: node['href'] }
|
36
|
+
when :input
|
37
|
+
{ type: node['type'], checked: node['checked'] }
|
38
|
+
when :img
|
39
|
+
{ src: node['src'], alt: node['alt'], class: (node['class'] || '').split }
|
40
|
+
else
|
41
|
+
{}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Slacken
|
2
|
+
class NodeType
|
3
|
+
def self.create(name)
|
4
|
+
name.is_a?(NodeType) ? name : new(name)
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :name
|
8
|
+
def initialize(name)
|
9
|
+
@name = name.to_sym
|
10
|
+
end
|
11
|
+
|
12
|
+
def member_of?(*names)
|
13
|
+
names.flatten.include?(name)
|
14
|
+
end
|
15
|
+
|
16
|
+
def text_types
|
17
|
+
%i(text emoji checkbox)
|
18
|
+
end
|
19
|
+
|
20
|
+
def block?
|
21
|
+
member_of?(%i(document div iframe p img ul ol dl dd li hr indent
|
22
|
+
p h1 h2 h3 h4 h5 h6 h7 h8 pre blockquote body html))
|
23
|
+
end
|
24
|
+
|
25
|
+
def inline?
|
26
|
+
!block?
|
27
|
+
end
|
28
|
+
|
29
|
+
def text_type?
|
30
|
+
member_of?(text_types)
|
31
|
+
end
|
32
|
+
|
33
|
+
def allowed_in_list?
|
34
|
+
member_of?(%i(code b strong i em wrapper div indent span ol ul dl li dd dt).concat(text_types))
|
35
|
+
end
|
36
|
+
|
37
|
+
def allowed_as_list_item?
|
38
|
+
member_of?(%i(code b strong i em wrapper span).concat(text_types))
|
39
|
+
end
|
40
|
+
|
41
|
+
def allowed_in_headline?
|
42
|
+
member_of?(%i(i em wrapper span).concat(text_types))
|
43
|
+
end
|
44
|
+
|
45
|
+
def allowed_in_table?
|
46
|
+
member_of?(%i(code b strong i em wrapper span).concat(text_types))
|
47
|
+
end
|
48
|
+
|
49
|
+
def allowed_in_link?
|
50
|
+
member_of?(%i(b strong i em wrapper span).concat(text_types))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Slacken
|
2
|
+
class RenderElement
|
3
|
+
attr_reader :type, :renderer, :attrs, :children
|
4
|
+
|
5
|
+
def initialize(type, children = [], attrs = {})
|
6
|
+
@type = NodeType.create(type)
|
7
|
+
@attrs = attrs
|
8
|
+
@children = children
|
9
|
+
end
|
10
|
+
|
11
|
+
def child
|
12
|
+
children.first
|
13
|
+
end
|
14
|
+
|
15
|
+
def render
|
16
|
+
case type.name
|
17
|
+
when :text
|
18
|
+
attrs[:content]
|
19
|
+
when :emoji
|
20
|
+
deco "#{attrs[:content]}"
|
21
|
+
when :checkbox
|
22
|
+
deco (attrs[:checked] ? '[x]' : '[ ]')
|
23
|
+
when :b, :strong
|
24
|
+
deco "*#{inner.to_s.strip}*"
|
25
|
+
when :i, :em
|
26
|
+
deco "_#{inner.to_s.strip}_"
|
27
|
+
when :iframe, :a
|
28
|
+
deco SlackUrl.link_tag(inner, attrs[:href])
|
29
|
+
when :img
|
30
|
+
deco SlackUrl.link_tag(inner, attrs[:src])
|
31
|
+
when :pre
|
32
|
+
deco "```#{inner}```"
|
33
|
+
when :blockquote
|
34
|
+
insert_head(inner.to_s, '> ')
|
35
|
+
when :code
|
36
|
+
deco "`#{inner}`"
|
37
|
+
when :br
|
38
|
+
"\n"
|
39
|
+
when :hr
|
40
|
+
'-----------'
|
41
|
+
when :li, :dd
|
42
|
+
# Item mark is appended by the parent list tag.
|
43
|
+
inner
|
44
|
+
when :ol, :ul, :dl
|
45
|
+
itemize
|
46
|
+
when :indent
|
47
|
+
insert_head(inner.to_s)
|
48
|
+
when /h\d/
|
49
|
+
"*#{inner.to_s.strip}*"
|
50
|
+
else
|
51
|
+
inner
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_s
|
56
|
+
render.to_s
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def itemize
|
62
|
+
children_strs = children.map.with_index(1) do |child, idx|
|
63
|
+
mark = type.member_of?(:ol) ? "#{idx}. " : '• '
|
64
|
+
"#{mark}#{child}"
|
65
|
+
end
|
66
|
+
grouping(children_strs)
|
67
|
+
end
|
68
|
+
|
69
|
+
def inner
|
70
|
+
grouping(children.map(&:render))
|
71
|
+
end
|
72
|
+
|
73
|
+
def grouping(children_strs)
|
74
|
+
if type.inline?
|
75
|
+
Rendering::Inlines.new(children_strs)
|
76
|
+
elsif type.member_of?(:ul, :ol, :dl, :li, :dd, :dt)
|
77
|
+
Rendering::Listings.new(children_strs)
|
78
|
+
elsif type.block?
|
79
|
+
Rendering::Paragraphs.new(children_strs)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def insert_head(str, head = ' ' * 4)
|
84
|
+
str.gsub(/^/, head)
|
85
|
+
end
|
86
|
+
|
87
|
+
def deco(str)
|
88
|
+
Rendering.decorate(str)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Slacken
|
2
|
+
module Rendering
|
3
|
+
def self.decorate(str)
|
4
|
+
DecorationWrapper.new(str)
|
5
|
+
end
|
6
|
+
|
7
|
+
# Private: A wrapper of string to concat node strings of a document tree.
|
8
|
+
class StringWrapper
|
9
|
+
def self.wrap(str)
|
10
|
+
str.kind_of?(StringWrapper) ? str : new(str)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(str)
|
14
|
+
@str = str
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
@str.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
# Public: Append another string to self.
|
22
|
+
def append(other)
|
23
|
+
other.concat_head(self)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Private: prepend another string to self.
|
27
|
+
def concat_head(other)
|
28
|
+
StringWrapper.new(other.to_s + to_s)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Private: A wrapper to space before and after the given string.
|
33
|
+
class DecorationWrapper < StringWrapper
|
34
|
+
|
35
|
+
# Public: Append another string to self.
|
36
|
+
# If the other string begin with a word character,
|
37
|
+
# A space is inserted between the two string.
|
38
|
+
def append(other)
|
39
|
+
if other.to_s.empty?
|
40
|
+
self
|
41
|
+
elsif other.to_s.match(/\A[\W&&[:ascii:]]/)
|
42
|
+
other.concat_head(self)
|
43
|
+
else
|
44
|
+
StringWrapper.new(to_s + " ").append(other)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Private: Prepend another string to self.
|
49
|
+
# If the other string end with a word character,
|
50
|
+
# A space is inserted between the two string.
|
51
|
+
def concat_head(other)
|
52
|
+
if other.to_s.empty?
|
53
|
+
self
|
54
|
+
elsif other.to_s.match(/[\W&&[:ascii:]]\Z/)
|
55
|
+
DecorationWrapper.new(other.to_s + to_s)
|
56
|
+
else
|
57
|
+
DecorationWrapper.new("#{other} #{to_s}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Public: an intermediate object to stringfy RenderElements.
|
63
|
+
#
|
64
|
+
class RenderingGroup
|
65
|
+
attr_reader :children
|
66
|
+
def initialize(children)
|
67
|
+
@children = children
|
68
|
+
end
|
69
|
+
|
70
|
+
def separator
|
71
|
+
fail NotImplementedError
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_a
|
75
|
+
extracted_children = children.map { |c| c.respond_to?(:to_a) ? c.to_a : c }
|
76
|
+
extracted_children.zip(Array.new(children.length, separator)).flatten[0..-2]
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_s
|
80
|
+
to_a.reduce(StringWrapper.new('')) { |r, el| r.append(StringWrapper.wrap(el)) }.to_s
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class Paragraphs < RenderingGroup
|
85
|
+
def separator
|
86
|
+
"\n\n"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class Listings < RenderingGroup
|
91
|
+
def separator
|
92
|
+
"\n"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class Inlines < RenderingGroup
|
97
|
+
def separator
|
98
|
+
''
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'kosi'
|
2
|
+
|
3
|
+
module Slacken
|
4
|
+
class TableElement
|
5
|
+
attr_reader :header, :columns
|
6
|
+
def initialize(children)
|
7
|
+
thead, tbody = children.slice(0, 2)
|
8
|
+
@header = thead.child # tr tag
|
9
|
+
@columns = tbody.children # tr tags
|
10
|
+
end
|
11
|
+
|
12
|
+
def render
|
13
|
+
head = header.children.map(&:to_s)
|
14
|
+
body = columns.map { |cl| cl.children.map(&:to_s) }
|
15
|
+
table = Kosi::Table.new(header: head).render(body)
|
16
|
+
table.to_s.chomp
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
render
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/sample/out.txt
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
*Slacken*
|
2
|
+
|
3
|
+
This gem translates a html source into *a markup text for Slack*.
|
4
|
+
<http://qiita.com|Qiita> uses this gem to decorate notification messages to Slack :trollface:.
|
5
|
+
|
6
|
+
*Examples*
|
7
|
+
|
8
|
+
*List*
|
9
|
+
|
10
|
+
1. Item 1
|
11
|
+
2. _Item 2 (italic)_
|
12
|
+
• [x] Checked
|
13
|
+
• [ ] Unchecked
|
14
|
+
3. *Item 3 (bold)*
|
15
|
+
• Nested Item 1
|
16
|
+
• Nested Item 2
|
17
|
+
|
18
|
+
*Citation*
|
19
|
+
|
20
|
+
> Qiita is a technical information sharing site for programmers.
|
21
|
+
> Kobito is an application for technical information recording.
|
22
|
+
|
23
|
+
*Source Code*
|
24
|
+
|
25
|
+
```class World
|
26
|
+
def hello
|
27
|
+
puts 'Hello, world!'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
*Image*
|
33
|
+
|
34
|
+
This is a Qiita logo.
|
35
|
+
|
36
|
+
-----------
|
37
|
+
|
38
|
+
<http://cdn.qiita.com/assets/siteid-reverse-1949e989f9d8b2f6fad65a57292b2b01.png|Qiita logo>
|
data/sample/source.html
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
|
2
|
+
<h1>Slacken</h1>
|
3
|
+
|
4
|
+
<p>This gem translates a html source into <strong>a markup text for Slack</strong>.<br>
|
5
|
+
<a href="http://qiita.com">Qiita</a> uses this gem to decorate notification messages to Slack <img class="emoji" title=":trollface:" alt=":trollface:" src="https://cdn.qiita.com/emoji/trollface.png" height="20" width="20" align="absmiddle">.</p>
|
6
|
+
|
7
|
+
<h2>Examples</h2>
|
8
|
+
|
9
|
+
<h3>List</h3>
|
10
|
+
|
11
|
+
<ol>
|
12
|
+
<li>Item 1</li>
|
13
|
+
<li>
|
14
|
+
<em>Item 2 (italic)</em>
|
15
|
+
|
16
|
+
<ul>
|
17
|
+
<li class="task-list-item">
|
18
|
+
<input type="checkbox" class="task-list-item-checkbox" checked disabled>Checked</li>
|
19
|
+
<li class="task-list-item">
|
20
|
+
<input type="checkbox" class="task-list-item-checkbox" disabled>Unchecked</li>
|
21
|
+
</ul>
|
22
|
+
</li>
|
23
|
+
<li>
|
24
|
+
<strong>Item 3 (bold)</strong>
|
25
|
+
|
26
|
+
<ul>
|
27
|
+
<li>Nested Item 1</li>
|
28
|
+
<li>Nested Item 2</li>
|
29
|
+
</ul>
|
30
|
+
</li>
|
31
|
+
</ol>
|
32
|
+
|
33
|
+
<h3>Citation</h3>
|
34
|
+
|
35
|
+
<blockquote>
|
36
|
+
<p>Qiita is a technical information sharing site for programmers.<br>
|
37
|
+
Kobito is an application for technical information recording.</p>
|
38
|
+
</blockquote>
|
39
|
+
|
40
|
+
<h3>Source Code</h3>
|
41
|
+
|
42
|
+
<div class="code-frame" data-lang="rb"><div class="highlight"><pre><span class="k">class</span> <span class="nc">World</span>
|
43
|
+
<span class="k">def</span> <span class="nf">hello</span>
|
44
|
+
<span class="nb">puts</span> <span class="s1">'Hello, world!'</span>
|
45
|
+
<span class="k">end</span>
|
46
|
+
<span class="k">end</span>
|
47
|
+
</pre></div></div>
|
48
|
+
|
49
|
+
<h3>Image</h3>
|
50
|
+
|
51
|
+
<p>This is a Qiita logo.</p>
|
52
|
+
|
53
|
+
<hr>
|
54
|
+
|
55
|
+
<p><img src="http://cdn.qiita.com/assets/siteid-reverse-1949e989f9d8b2f6fad65a57292b2b01.png" alt="Qiita logo"></p>
|