minidown 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.travis.yml +9 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +73 -0
  7. data/Rakefile +4 -0
  8. data/bin/minidown +33 -0
  9. data/lib/minidown.rb +13 -0
  10. data/lib/minidown/document.rb +142 -0
  11. data/lib/minidown/element.rb +40 -0
  12. data/lib/minidown/elements.rb +15 -0
  13. data/lib/minidown/elements/block_element.rb +37 -0
  14. data/lib/minidown/elements/code_block_element.rb +44 -0
  15. data/lib/minidown/elements/dividing_line_element.rb +11 -0
  16. data/lib/minidown/elements/html_element.rb +22 -0
  17. data/lib/minidown/elements/indent_code_element.rb +23 -0
  18. data/lib/minidown/elements/line_element.rb +28 -0
  19. data/lib/minidown/elements/list_element.rb +36 -0
  20. data/lib/minidown/elements/list_group_element.rb +87 -0
  21. data/lib/minidown/elements/order_list_element.rb +20 -0
  22. data/lib/minidown/elements/paragraph_element.rb +56 -0
  23. data/lib/minidown/elements/raw_html_element.rb +11 -0
  24. data/lib/minidown/elements/table_element.rb +80 -0
  25. data/lib/minidown/elements/text_element.rb +198 -0
  26. data/lib/minidown/elements/unorder_list_element.rb +31 -0
  27. data/lib/minidown/html_helper.rb +18 -0
  28. data/lib/minidown/parser.rb +19 -0
  29. data/lib/minidown/utils.rb +28 -0
  30. data/lib/minidown/version.rb +3 -0
  31. data/minidown.gemspec +26 -0
  32. data/spec/blank_line_spec.rb +21 -0
  33. data/spec/code_block_spec.rb +182 -0
  34. data/spec/dividing_line_spec.rb +35 -0
  35. data/spec/h1_and_h2_spec.rb +55 -0
  36. data/spec/indent_block_spec.rb +17 -0
  37. data/spec/li_spec.rb +354 -0
  38. data/spec/minidown_spec.rb +12 -0
  39. data/spec/paragraph_spec.rb +17 -0
  40. data/spec/raw_html_spec.rb +43 -0
  41. data/spec/spec_helper.rb +2 -0
  42. data/spec/start_with_gt_spec.rb +70 -0
  43. data/spec/start_with_shape_spec.rb +42 -0
  44. data/spec/table_spec.rb +63 -0
  45. data/spec/task_list_spec.rb +18 -0
  46. data/spec/text_element_spec.rb +317 -0
  47. metadata +175 -0
@@ -0,0 +1,11 @@
1
+ module Minidown
2
+ class DividingLineElement < Element
3
+ def parse
4
+ nodes << self
5
+ end
6
+
7
+ def to_html
8
+ '<hr>'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ module Minidown
2
+ class HtmlElement < Element
3
+ attr_reader :name
4
+
5
+ def initialize doc, content, name
6
+ super doc, content
7
+ @name = name
8
+ end
9
+
10
+ def parse
11
+ nodes << self
12
+ end
13
+
14
+ def to_html
15
+ build_tag @name do |tag|
16
+ # self.content is some Element
17
+ self.content = content.text if ParagraphElement === self.content
18
+ tag << self.content.to_html
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ module Minidown
2
+ class IndentCodeElement < Element
3
+ def initialize *_
4
+ super
5
+ @lines = [content]
6
+ end
7
+
8
+ def parse
9
+ while line = unparsed_lines.shift
10
+ case line
11
+ when Utils::Regexp[:indent_code]
12
+ @lines << $1
13
+ else
14
+ unparsed_lines.unshift line
15
+ break
16
+ end
17
+ end
18
+ unparsed_lines.unshift '```'
19
+ unparsed_lines.unshift *@lines
20
+ unparsed_lines.unshift '```'
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,28 @@
1
+ module Minidown
2
+ class LineElement < Element
3
+ attr_accessor :display
4
+
5
+ def initialize doc, content=nil
6
+ super
7
+ @display = true
8
+ end
9
+
10
+ def blank?
11
+ true
12
+ end
13
+
14
+ def parse
15
+ node = nodes.last
16
+ @display = (doc.within_block || TextElement === node)
17
+ nodes << self
18
+ end
19
+
20
+ def to_html
21
+ if @display
22
+ br_tag
23
+ else
24
+ ''
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ module Minidown
2
+ class ListElement < Element
3
+ attr_accessor :p_tag_content, :contents, :task_list, :checked
4
+ CheckedBox = '<input type="checkbox" class="task-list-item-checkbox" checked="" disabled="">'.freeze
5
+ UnCheckedBox = '<input type="checkbox" class="task-list-item-checkbox" disabled="">'.freeze
6
+
7
+ def initialize *_
8
+ super
9
+ @p_tag_content = false
10
+ @contents = [TextElement.new(doc, content)]
11
+ @task_list = false
12
+ @checked = false
13
+ end
14
+
15
+ def to_html
16
+ @contents.map! do |content|
17
+ ParagraphElement === content ? content.text : content
18
+ end if @p_tag_content
19
+ content = list_content
20
+ content = build_tag('p'.freeze){|p| p << content} if @p_tag_content
21
+ attr = nil
22
+ attr = {class: 'task-list-item'.freeze} if @task_list
23
+ build_tag 'li'.freeze, attr do |li|
24
+ li << content
25
+ end
26
+ end
27
+
28
+ def list_content
29
+ content = @contents.map(&:to_html).join(@p_tag_content ? br_tag : "\n".freeze)
30
+ if @task_list
31
+ content = (@checked ? CheckedBox : UnCheckedBox) + content
32
+ end
33
+ content
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,87 @@
1
+ module Minidown
2
+ class ListGroupElement < Element
3
+ IndentRegexp = /\A\s{4,}(.+)/
4
+ StartWithBlankRegexp = /\A\s+(.+)/
5
+
6
+ attr_accessor :lists, :indent_level
7
+
8
+ def parse
9
+ nodes << self
10
+ while line = unparsed_lines.shift
11
+ #handle nested list
12
+ if (line =~ UnorderListElement::NestRegexp && list_class = UnorderListElement) || (line =~ OrderListElement::NestRegexp && list_class = OrderListElement)
13
+ li, str = $1.size, $2
14
+ if li > @indent_level
15
+ list_class.new(doc, str, li).parse
16
+ @lists.last.contents << nodes.pop
17
+ next
18
+ elsif li == @indent_level
19
+ list_class.new(doc, str, li).parse
20
+ child = nodes.pop
21
+ if LineElement === nodes.last
22
+ @lists.last.p_tag_content = child.lists.first.p_tag_content = true
23
+ end
24
+ if child.is_a?(ListGroupElement)
25
+ nodes.push *child.children
26
+ @lists.push *child.lists
27
+ else
28
+ @lists.last.contents << child
29
+ end
30
+ next
31
+ else
32
+ unparsed_lines.unshift line
33
+ break
34
+ end
35
+ end
36
+
37
+ doc.parse_line line
38
+ child = nodes.pop
39
+ case child
40
+ when self.class
41
+ if LineElement === nodes.last
42
+ @lists.last.p_tag_content = child.lists.first.p_tag_content = true
43
+ end
44
+ nodes.push *child.children
45
+ @lists.push *child.lists
46
+ break
47
+ when ParagraphElement
48
+ contents = @lists.last.contents
49
+ if line =~ StartWithBlankRegexp
50
+ doc.parse_line $1
51
+ node = nodes.pop
52
+ if TextElement === node || ParagraphElement === node
53
+ if TextElement === contents.last
54
+ contents.push(contents.pop.paragraph)
55
+ end
56
+ node = node.paragraph if TextElement === node
57
+ end
58
+ else
59
+ if @blank
60
+ unparsed_lines.unshift line
61
+ break
62
+ end
63
+ node = child.text
64
+ end
65
+ contents << node if node
66
+ when LineElement
67
+ next_line = unparsed_lines.first
68
+ if next_line.nil? || next_line.empty? || StartWithBlankRegexp === next_line || self.class.const_get(:ListRegexp) === next_line
69
+ child.display = false
70
+ nodes << child
71
+ else
72
+ unparsed_lines.unshift line
73
+ break
74
+ end
75
+ else
76
+ @put_back << child if child
77
+ break
78
+ end
79
+ @blank = (LineElement === child)
80
+ end
81
+ children_range = (nodes.index(self) + 1)..-1
82
+ children.push *nodes[children_range]
83
+ nodes[children_range] = []
84
+ nodes.push *@put_back
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,20 @@
1
+ module Minidown
2
+ class OrderListElement < ListGroupElement
3
+ NestRegexp = /\A(\s*)\d+\.\s+(.+)/
4
+ ListRegexp = Minidown::Utils::Regexp[:order_list]
5
+
6
+ def initialize doc, line, indent_level = 0
7
+ super doc, line
8
+ @children << ListElement.new(doc, content)
9
+ @lists = @children.dup
10
+ @indent_level = indent_level
11
+ @put_back = []
12
+ end
13
+
14
+ def to_html
15
+ build_tag 'ol'.freeze do |content|
16
+ children.each { |child| content << child.to_html}
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,56 @@
1
+ module Minidown
2
+ class ParagraphElement < Element
3
+ attr_reader :contents
4
+ attr_accessor :extra
5
+
6
+ ExcludeSchemeRegexp = /\A[^@:]+\z/
7
+ StringSymbolRegexp = /"|'/
8
+
9
+ def initialize *_
10
+ super
11
+ @contents = [raw_content]
12
+ @extra = false
13
+ end
14
+
15
+ def parse
16
+ if ParagraphElement === nodes.last
17
+ nodes.last.contents << raw_content
18
+ else
19
+ nodes << self
20
+ end
21
+ end
22
+
23
+ def text
24
+ build_element raw_content
25
+ end
26
+
27
+ def to_html
28
+ if @extra
29
+ contents.map{|content| ParagraphElement.new(doc, content).to_html }.join ''.freeze
30
+ else
31
+ contents.map! do |line|
32
+ build_element line
33
+ end
34
+ build_tag 'p'.freeze do |content|
35
+ pre_elem = contents.shift
36
+ content << pre_elem.to_html
37
+ while elem = contents.shift
38
+ content << br_tag if TextElement === pre_elem && TextElement === elem
39
+ content << elem.to_html
40
+ pre_elem = elem
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def build_element content_str
49
+ if Utils::Regexp[:raw_html] =~ content_str && (raw = $1) && (ExcludeSchemeRegexp =~ raw || StringSymbolRegexp =~ raw)
50
+ RawHtmlElement.new doc, raw
51
+ else
52
+ TextElement.new doc, content_str
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,11 @@
1
+ module Minidown
2
+ class RawHtmlElement < Element
3
+ def parse
4
+ nodes << self
5
+ end
6
+
7
+ def to_html
8
+ content
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,80 @@
1
+ module Minidown
2
+ class TableElement < Element
3
+ AlignSpecRegexp = /\A\|?\s*([\-:]\-+[\-:]\s*(?:\|\s*[\-:]\-+[\-:]\s*)*)\|?\s*\z/
4
+
5
+ def initialize doc, content, raw_head
6
+ super doc, content
7
+ @raw_head = raw_head
8
+ @heads = @raw_head.split('|'.freeze).map! &:strip
9
+ @column_count = @heads.count
10
+ end
11
+
12
+ def check_column_spec raw_column_spec
13
+ if @valid.nil?
14
+ @valid = Utils::Regexp[:pipe_symbol] =~ raw_column_spec && AlignSpecRegexp =~ raw_column_spec && (column_spec_str = $1) && (@column_specs = column_spec_str.split('|'.freeze).map! &:strip) && @column_specs.count == @column_count
15
+ else
16
+ @valid
17
+ end
18
+ end
19
+
20
+ def parse
21
+ if @valid
22
+ nodes << self
23
+ @bodys = []
24
+ @column_specs.map! do |column_spec|
25
+ if column_spec[0] == column_spec[-1]
26
+ column_spec[0] == ':'.freeze ? 'center'.freeze : nil
27
+ else
28
+ column_spec[0] == ':'.freeze ? 'left'.freeze : 'right'.freeze
29
+ end
30
+ end
31
+
32
+ while line = unparsed_lines.shift
33
+ if Utils::Regexp[:table] =~ line && (cells = $1.split('|'.freeze).map! &:strip) && @column_count == cells.count
34
+ @bodys << cells
35
+ else
36
+ unparsed_lines.unshift line
37
+ break
38
+ end
39
+ end
40
+ else
41
+ raise 'table column specs not valid'
42
+ end
43
+ end
44
+
45
+ def to_html
46
+ attrs = @column_specs.map do |align|
47
+ {align: align}.freeze if align
48
+ end
49
+ build_tag 'table'.freeze do |table|
50
+ thead = build_tag 'thead'.freeze do |thead|
51
+ tr = build_tag 'tr'.freeze do |tr|
52
+ @heads.each_with_index do |cell, i|
53
+ th = build_tag 'th'.freeze, attrs[i] do |th|
54
+ th << cell
55
+ end
56
+ tr << th
57
+ end
58
+ end
59
+ thead << tr
60
+ end
61
+ table << thead
62
+
63
+ tbody = build_tag 'tbody'.freeze do |tbody|
64
+ @bodys.each do |row|
65
+ tr = build_tag 'tr'.freeze do |tr|
66
+ row.each_with_index do |cell, i|
67
+ td = build_tag 'td'.freeze, attrs[i] do |td|
68
+ td << TextElement.new(doc, cell).to_html
69
+ end
70
+ tr << td
71
+ end
72
+ end
73
+ tbody << tr
74
+ end
75
+ end
76
+ table << tbody
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,198 @@
1
+ module Minidown
2
+ class TextElement < Element
3
+ EscapeChars = %w{# &gt; * + \- ` _ { } ( ) . ! \[ \] ~}
4
+ EscapeRegexp = /(?<!\\)\\([#{EscapeChars.join '|'}|\\])/
5
+
6
+ Regexp = {
7
+ tag: /(\\*)&lt;(.+?)(\\*)&gt;/,
8
+ quot: /&quot;/,
9
+ link: /(?<!\!)\[(.+?)\]\((.+?)\)/,
10
+ link_title: /((?<=").+?(?="))/,
11
+ link_url: /(\S+)/,
12
+ link_ref: /(?<!\!)\[(.+?)\]\s*\[(.*?)\]/,
13
+ image: /\!\[(.+?)\]\((.+?)\)/,
14
+ image_ref: /\!\[(.+?)\]\s*\[(.*?)\]/,
15
+ star: /((?<!\\)\*{1,2})(.+?)\1/,
16
+ underline: /(?<=\A|\s)((?<!\\)\_{1,2})(\S+)\1(?=\z|\s)/,
17
+ delete_line: /(?<!\\)~~(?!\s)(.+?)(?<!\s)~~/,
18
+ quotlink: /\<(.+?)\>/,
19
+ link_scheme: /\A\S+?\:\/\//,
20
+ email: /\A[A-Za-z0-9]+@[A-Za-z0-9]+\.[A-Za-z0-9]+/,
21
+ auto_email: /(?<!\S)[A-Za-z0-9]+@[A-Za-z0-9]+\.[A-Za-z0-9]+(?!\S)/,
22
+ auto_link: /(?<!\S)\w+?\:\/\/.+?(?!\S)/,
23
+ inline_code: /(?<!\\)(`+)\s*(.+?)\s*(?<!\\)\1/
24
+ }.freeze
25
+
26
+ attr_accessor :escape, :convert, :sanitize
27
+
28
+ def initialize *_
29
+ super
30
+ @escape = true
31
+ @sanitize = false
32
+ @convert = true
33
+ end
34
+
35
+ def parse
36
+ nodes << self
37
+ end
38
+
39
+ def content
40
+ str = super
41
+ str = convert_str(str) if convert
42
+ escape_content! str
43
+ escape_str! str
44
+ escape_html str if sanitize
45
+ str
46
+ end
47
+
48
+ def escape_str! str
49
+ str.gsub!(EscapeRegexp, '\\1'.freeze) if escape
50
+ end
51
+
52
+ def escape_html str
53
+ str.replace Utils.escape_html(str)
54
+ end
55
+
56
+ def escape_content! str
57
+ return str unless @escape
58
+ escape_html str
59
+
60
+ str.gsub! Regexp[:tag] do
61
+ left, tag, right = $1, $2, $3
62
+ tag.gsub! Regexp[:quot] do
63
+ '"'.freeze
64
+ end
65
+
66
+ left = left.size.odd? ? "#{left[0..-2]}&lt;" : "#{left}<" if left
67
+ left ||= "<".freeze
68
+
69
+ right = right.size.odd? ? "#{right[0..-2]}&gt;" : "#{right}>" if right
70
+ right ||= ">".freeze
71
+
72
+ "#{left}#{tag}#{right}"
73
+ end
74
+ str
75
+ end
76
+
77
+ def convert_str str
78
+ #auto link
79
+ str.gsub! Regexp[:auto_link] do |origin_str|
80
+ build_tag 'a'.freeze, href: origin_str do |a|
81
+ a << origin_str
82
+ end
83
+ end
84
+
85
+ #auto email
86
+ str.gsub! Regexp[:auto_email] do |origin_str|
87
+ build_tag 'a'.freeze, href: "mailto:#{origin_str}" do |a|
88
+ a << origin_str
89
+ end
90
+ end
91
+
92
+ #parse <link>
93
+ str.gsub! Regexp[:quotlink] do |origin_str|
94
+ link = $1
95
+ attr = case link
96
+ when Regexp[:link_scheme]
97
+ {href: link}
98
+ when Regexp[:email]
99
+ {href: "mailto:#{link}"}
100
+ end
101
+ attr ? build_tag('a'.freeze, attr){|a| a << link} : origin_str
102
+ end
103
+
104
+ #parse * _
105
+ Regexp.values_at(:star, :underline).each do |regex|
106
+ str.gsub! regex do |origin_str|
107
+ tag_name = $1.size > 1 ? 'strong'.freeze : 'em'.freeze
108
+ build_tag tag_name do |tag|
109
+ tag << $2
110
+ end
111
+ end
112
+ end
113
+
114
+ #parse ~~del~~
115
+ str.gsub! Regexp[:delete_line] do |origin_str|
116
+ build_tag 'del'.freeze do |tag|
117
+ tag << $1
118
+ end
119
+ end
120
+
121
+ #convert image reference
122
+ str.gsub! Regexp[:image_ref] do |origin_str|
123
+ alt = $1
124
+ id = ($2 && !$2.empty?) ? $2 : $1
125
+ ref = doc.links_ref[id.downcase]
126
+ if ref
127
+ attr = {src: ref[:url], alt: alt}
128
+ attr[:title] = ref[:title] if ref[:title] && !ref[:title].empty?
129
+ build_tag 'img'.freeze, attr
130
+ else
131
+ origin_str
132
+ end
133
+ end
134
+
135
+ #convert image syntax
136
+ str.gsub! Regexp[:image] do
137
+ alt, url = $1, $2
138
+ url =~ Regexp[:link_title]
139
+ title = $1
140
+ url =~ Regexp[:link_url]
141
+ url = $1
142
+ attr = {src: url, alt: alt}
143
+ attr[:title] = title if title
144
+ build_tag 'img'.freeze, attr
145
+ end
146
+
147
+ #convert link reference
148
+ str.gsub! Regexp[:link_ref] do |origin_str|
149
+ text = $1
150
+ id = ($2 && !$2.empty?) ? $2 : $1
151
+ ref = doc.links_ref[id.downcase]
152
+ if ref
153
+ attr = {href: ref[:url]}
154
+ attr[:title] = ref[:title] if ref[:title] && !ref[:title].empty?
155
+ build_tag 'a'.freeze, attr do |a|
156
+ a << text
157
+ end
158
+ else
159
+ origin_str
160
+ end
161
+ end
162
+
163
+ #convert link syntax
164
+ str.gsub! Regexp[:link] do
165
+ text, url = $1, $2
166
+ url =~ Regexp[:link_title]
167
+ title = $1
168
+ url =~ Regexp[:link_url]
169
+ url = $1
170
+ attr = {href: url}
171
+ attr[:title] = title if title
172
+ build_tag 'a'.freeze, attr do |content|
173
+ content << text
174
+ end
175
+ end
176
+
177
+ escape_content! str
178
+
179
+ #inline code
180
+ str.gsub! Regexp[:inline_code] do |origin_str|
181
+ build_tag 'code'.freeze do |code|
182
+ code << escape_html($2)
183
+ end
184
+ end
185
+ escape_str! str
186
+ @escape = false
187
+ str
188
+ end
189
+
190
+ def paragraph
191
+ ParagraphElement.new doc, raw_content
192
+ end
193
+
194
+ def to_html
195
+ content
196
+ end
197
+ end
198
+ end