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