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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.md +60 -0
  7. data/Rakefile +36 -0
  8. data/lib/slacken.rb +21 -0
  9. data/lib/slacken/document_component.rb +73 -0
  10. data/lib/slacken/document_component/elim_blanks.rb +36 -0
  11. data/lib/slacken/document_component/elim_invalid_links.rb +26 -0
  12. data/lib/slacken/document_component/elim_line_breaks.rb +27 -0
  13. data/lib/slacken/document_component/extract_img_alt.rb +12 -0
  14. data/lib/slacken/document_component/group_indent.rb +27 -0
  15. data/lib/slacken/document_component/group_inlines.rb +26 -0
  16. data/lib/slacken/document_component/sanitize_link_containers.rb +40 -0
  17. data/lib/slacken/document_component/sanitize_special_tag_containers.rb +102 -0
  18. data/lib/slacken/document_component/stringfy_checkbox.rb +20 -0
  19. data/lib/slacken/document_component/stringfy_emoji.rb +20 -0
  20. data/lib/slacken/dom_container.rb +45 -0
  21. data/lib/slacken/node_type.rb +53 -0
  22. data/lib/slacken/render_element.rb +91 -0
  23. data/lib/slacken/rendering.rb +102 -0
  24. data/lib/slacken/slack_url.rb +7 -0
  25. data/lib/slacken/table_element.rb +23 -0
  26. data/lib/slacken/version.rb +3 -0
  27. data/sample/out.txt +38 -0
  28. data/sample/source.html +55 -0
  29. data/scripts/update_markup_fixture.rb +8 -0
  30. data/slacken.gemspec +28 -0
  31. data/spec/fixtures/example.html +114 -0
  32. data/spec/fixtures/markup_text.txt +73 -0
  33. data/spec/helpers/document_component_dsl.rb +11 -0
  34. data/spec/slacken/document_component/elim_blanks_spec.rb +34 -0
  35. data/spec/slacken/document_component/elim_invalid_links_spec.rb +49 -0
  36. data/spec/slacken/document_component/elim_line_breaks_spec.rb +41 -0
  37. data/spec/slacken/document_component/group_indent_spec.rb +37 -0
  38. data/spec/slacken/document_component/group_inlines_spec.rb +33 -0
  39. data/spec/slacken/document_component/sanitize_special_tag_containers_spec.rb +64 -0
  40. data/spec/slacken/document_component_spec.rb +19 -0
  41. data/spec/slacken_spec.rb +12 -0
  42. data/spec/spec_helper.rb +13 -0
  43. 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,7 @@
1
+ module Slacken
2
+ module SlackUrl
3
+ def self.link_tag(title, url)
4
+ "<#{url}|#{title}>"
5
+ end
6
+ end
7
+ 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
@@ -0,0 +1,3 @@
1
+ module Slacken
2
+ VERSION = "0.1.0"
3
+ end
@@ -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>
@@ -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>