yard 0.9.39 → 0.9.40
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/CHANGELOG.md +16 -1
- data/README.md +18 -21
- data/docs/GettingStarted.md +41 -15
- data/docs/Tags.md +5 -5
- data/docs/WhatsNew.md +59 -7
- data/docs/templates/default/yard_tags/html/setup.rb +1 -1
- data/lib/yard/autoload.rb +17 -0
- data/lib/yard/cli/diff.rb +7 -2
- data/lib/yard/code_objects/proxy.rb +1 -1
- data/lib/yard/handlers/processor.rb +1 -0
- data/lib/yard/handlers/rbs/attribute_handler.rb +43 -0
- data/lib/yard/handlers/rbs/base.rb +38 -0
- data/lib/yard/handlers/rbs/constant_handler.rb +18 -0
- data/lib/yard/handlers/rbs/method_handler.rb +327 -0
- data/lib/yard/handlers/rbs/mixin_handler.rb +20 -0
- data/lib/yard/handlers/rbs/namespace_handler.rb +26 -0
- data/lib/yard/handlers/ruby/attribute_handler.rb +7 -4
- data/lib/yard/handlers/ruby/constant_handler.rb +1 -0
- data/lib/yard/i18n/locale.rb +1 -1
- data/lib/yard/i18n/pot_generator.rb +1 -1
- data/lib/yard/parser/rbs/rbs_parser.rb +325 -0
- data/lib/yard/parser/rbs/statement.rb +75 -0
- data/lib/yard/parser/ruby/ruby_parser.rb +51 -1
- data/lib/yard/parser/source_parser.rb +3 -2
- data/lib/yard/registry_resolver.rb +7 -0
- data/lib/yard/server/library_version.rb +1 -1
- data/lib/yard/server/templates/default/fulldoc/html/js/autocomplete.js +208 -12
- data/lib/yard/server/templates/default/layout/html/breadcrumb.erb +1 -17
- data/lib/yard/server/templates/default/method_details/html/permalink.erb +4 -2
- data/lib/yard/server/templates/doc_server/library_list/html/headers.erb +3 -3
- data/lib/yard/server/templates/doc_server/library_list/html/library_list.erb +2 -3
- data/lib/yard/server/templates/doc_server/processing/html/processing.erb +22 -16
- data/lib/yard/tags/directives.rb +7 -0
- data/lib/yard/tags/library.rb +3 -3
- data/lib/yard/tags/types_explainer.rb +2 -1
- data/lib/yard/templates/helpers/base_helper.rb +1 -1
- data/lib/yard/templates/helpers/html_helper.rb +15 -4
- data/lib/yard/templates/helpers/html_syntax_highlight_helper.rb +6 -1
- data/lib/yard/templates/helpers/markup/hybrid_markdown.rb +2125 -0
- data/lib/yard/templates/helpers/markup_helper.rb +4 -2
- data/lib/yard/version.rb +1 -1
- data/po/ja.po +82 -82
- data/templates/default/fulldoc/html/full_list.erb +4 -4
- data/templates/default/fulldoc/html/js/app.js +503 -319
- data/templates/default/fulldoc/html/js/full_list.js +310 -213
- data/templates/default/layout/html/headers.erb +1 -1
- data/templates/default/method/html/header.erb +3 -3
- data/templates/default/module/html/defines.erb +3 -3
- data/templates/default/module/html/inherited_methods.erb +1 -0
- data/templates/default/module/html/method_summary.erb +8 -0
- data/templates/default/module/setup.rb +20 -0
- data/templates/default/onefile/html/layout.erb +3 -4
- data/templates/guide/fulldoc/html/js/app.js +57 -26
- data/templates/guide/layout/html/layout.erb +9 -11
- metadata +13 -4
|
@@ -0,0 +1,2125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
if RUBY_VERSION < '3.5'
|
|
3
|
+
require 'cgi/util'
|
|
4
|
+
else
|
|
5
|
+
require 'cgi/escape'
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
module YARD
|
|
9
|
+
module Templates
|
|
10
|
+
module Helpers
|
|
11
|
+
module Markup
|
|
12
|
+
# A built-in formatter that implements a practical subset of GitHub
|
|
13
|
+
# flavored Markdown plus common RDoc markup forms.
|
|
14
|
+
class HybridMarkdown
|
|
15
|
+
attr_accessor :from_path
|
|
16
|
+
|
|
17
|
+
NAMED_ENTITIES = {
|
|
18
|
+
'nbsp' => [0x00A0].pack('U'),
|
|
19
|
+
'copy' => [0x00A9].pack('U'),
|
|
20
|
+
'AElig' => [0x00C6].pack('U'),
|
|
21
|
+
'Dcaron' => [0x010E].pack('U'),
|
|
22
|
+
'frac34' => [0x00BE].pack('U'),
|
|
23
|
+
'HilbertSpace' => [0x210B].pack('U'),
|
|
24
|
+
'DifferentialD' => [0x2146].pack('U'),
|
|
25
|
+
'ClockwiseContourIntegral' => [0x2232].pack('U'),
|
|
26
|
+
'ngE' => [0x2267, 0x0338].pack('U*'),
|
|
27
|
+
'ouml' => [0x00F6].pack('U'),
|
|
28
|
+
'quot' => '"',
|
|
29
|
+
'amp' => '&'
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
ATX_HEADING_RE = /^\s{0,3}#{Regexp.escape('#')}{1,6}(?=[ \t]|$)/.freeze
|
|
33
|
+
RDOC_HEADING_RE = /^\s*(=+)[ \t]+(.+?)\s*$/.freeze
|
|
34
|
+
SETEXT_HEADING_RE = /^\s{0,3}(=+|-+)\s*$/.freeze
|
|
35
|
+
FENCE_RE = /^(\s{0,3})(`{3,}|~{3,})([^\n]*)$/.freeze
|
|
36
|
+
THEMATIC_BREAK_RE = /^\s{0,3}(?:(?:-\s*){3,}|(?:\*\s*){3,}|(?:_\s*){3,})\s*$/.freeze
|
|
37
|
+
TABLE_SEPARATOR_RE = /^\s*\|?(?:\s*:?-+:?\s*\|)+(?:\s*:?-+:?\s*)\|?\s*$/.freeze
|
|
38
|
+
UNORDERED_LIST_RE = /^\s{0,3}([*+-])[ \t]+(.+?)\s*$/.freeze
|
|
39
|
+
ORDERED_LIST_RE = /^\s{0,3}(\d+)([.)])[ \t]+(.+?)\s*$/.freeze
|
|
40
|
+
RDOC_ORDERED_LIST_RE = /^\s{0,3}([A-Za-z])\.[ \t]+(.+?)\s*$/.freeze
|
|
41
|
+
LABEL_LIST_BRACKET_RE = /^\s*\[([^\]]+)\](?:[ \t]+(.+))?\s*$/.freeze
|
|
42
|
+
LABEL_LIST_COLON_RE = /^\s*([^\s:][^:]*)::(?:[ \t]+(.*))?\s*$/.freeze
|
|
43
|
+
BLOCKQUOTE_RE = /^\s{0,3}>\s?(.*)$/.freeze
|
|
44
|
+
HTML_BLOCK_RE = %r{
|
|
45
|
+
^\s*(?:
|
|
46
|
+
<!--|
|
|
47
|
+
<\?|
|
|
48
|
+
<![A-Z]|
|
|
49
|
+
<!\[CDATA\[|
|
|
50
|
+
</?(?:address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)\b|
|
|
51
|
+
<(?:script|pre|style|textarea)\b|
|
|
52
|
+
</(?:script|pre|style|textarea)\b|
|
|
53
|
+
</?[A-Za-z][A-Za-z0-9-]*(?:\s+[A-Za-z_:][\w:.-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*\s*/?>\s*$
|
|
54
|
+
)
|
|
55
|
+
}mx.freeze
|
|
56
|
+
HTML_BLOCK_TAGS = %w[
|
|
57
|
+
address article aside base basefont blockquote body caption center col
|
|
58
|
+
colgroup dd details dialog dir div dl dt fieldset figcaption figure
|
|
59
|
+
footer form frame frameset h1 h2 h3 h4 h5 h6 head header hr html iframe
|
|
60
|
+
legend li link main menu menuitem nav noframes ol optgroup option p param
|
|
61
|
+
search section summary table tbody td tfoot th thead title tr track ul
|
|
62
|
+
].freeze
|
|
63
|
+
HTML_TAG_RE = %r{
|
|
64
|
+
<!--(?:>|->)|
|
|
65
|
+
<!--(?:.*?)-->|
|
|
66
|
+
<\?.*?\?>|
|
|
67
|
+
<![A-Z][^>]*>|
|
|
68
|
+
<!\[CDATA\[.*?\]\]>|
|
|
69
|
+
</[A-Za-z][A-Za-z0-9-]*\s*>|
|
|
70
|
+
<[A-Za-z][A-Za-z0-9-]*
|
|
71
|
+
(?:\s+[A-Za-z_:][\w:.-]*
|
|
72
|
+
(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?
|
|
73
|
+
)*
|
|
74
|
+
\s*/?>
|
|
75
|
+
}mx.freeze
|
|
76
|
+
ENTITY_RE = /&(?:[A-Za-z][A-Za-z0-9]+|#\d+|#[xX][0-9A-Fa-f]+);/.freeze
|
|
77
|
+
YARD_LINK_RE = /(?<!\\)\{(?!\})(\S+?)(?:\s([^\}]*?\S))?\}(?=\W|.+<\/|$)/m.freeze
|
|
78
|
+
CODE_LANG_RE = /\A(?:[ \t]*\n)?[ \t]*!!!([\w.+-]+)[ \t]*\n/.freeze
|
|
79
|
+
REFERENCE_DEF_START_RE = /^\s{0,3}\[([^\]]+)\]:\s*(.*)$/.freeze
|
|
80
|
+
PLACEHOLDER_RE = /\0(\d+)\0/.freeze
|
|
81
|
+
ESCAPABLE_CHARS_RE = /\\([!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~])/.freeze
|
|
82
|
+
RDOC_ESCAPED_CAPITALIZED_CROSSREF_RE = /\\((?:::)?(?:[A-Z]\w+|[A-Z]\w*::\w+)(?:::\w+)*)/.freeze
|
|
83
|
+
AUTOLINK_RE = /<([A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\s]*|[A-Za-z0-9.!#$%&'*+\/=?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)+)>/.freeze
|
|
84
|
+
TAB_WIDTH = 4
|
|
85
|
+
|
|
86
|
+
# @param text [String] the Markdown text to format.
|
|
87
|
+
# @param options [Hash] options for the formatter.
|
|
88
|
+
# @option options [Boolean] :heading_ids whether to generate id attributes for headings.
|
|
89
|
+
def initialize(text, options = {})
|
|
90
|
+
@heading_ids = options.fetch(:heading_ids, true)
|
|
91
|
+
@references = {}
|
|
92
|
+
@text = extract_reference_definitions(text.to_s.gsub(/\r\n?/, "\n"))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [String] the formatted HTML.
|
|
96
|
+
def to_html
|
|
97
|
+
parse_blocks(split_lines(@text), 0).join("\n")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def parse_blocks(lines, index)
|
|
103
|
+
blocks = []
|
|
104
|
+
previous_block_type = nil
|
|
105
|
+
|
|
106
|
+
while index < lines.length
|
|
107
|
+
line = lines[index]
|
|
108
|
+
|
|
109
|
+
if blank_line?(line)
|
|
110
|
+
index += 1
|
|
111
|
+
elsif yard_indented_code_start?(lines, index)
|
|
112
|
+
block, index = parse_yard_indented_code(lines, index)
|
|
113
|
+
blocks << block
|
|
114
|
+
previous_block_type = :code
|
|
115
|
+
elsif indented_code_block_start?(lines, index, previous_block_type)
|
|
116
|
+
block, index = parse_indented_code(lines, index)
|
|
117
|
+
blocks << block
|
|
118
|
+
previous_block_type = :code
|
|
119
|
+
elsif thematic_break?(line)
|
|
120
|
+
blocks << '<hr />'
|
|
121
|
+
index += 1
|
|
122
|
+
previous_block_type = :hr
|
|
123
|
+
elsif (heading = parse_setext_heading(lines, index))
|
|
124
|
+
blocks << heading[0]
|
|
125
|
+
index = heading[1]
|
|
126
|
+
previous_block_type = :heading
|
|
127
|
+
elsif (heading = parse_heading(line))
|
|
128
|
+
blocks << heading
|
|
129
|
+
index += 1
|
|
130
|
+
previous_block_type = :heading
|
|
131
|
+
elsif fenced_code_start?(line)
|
|
132
|
+
block, index = parse_fenced_code(lines, index)
|
|
133
|
+
blocks << block
|
|
134
|
+
previous_block_type = :code
|
|
135
|
+
elsif table_start?(lines, index)
|
|
136
|
+
block, index = parse_table(lines, index)
|
|
137
|
+
blocks << block
|
|
138
|
+
previous_block_type = :table
|
|
139
|
+
elsif labeled_list_start?(lines, index)
|
|
140
|
+
block, index = parse_labeled_list(lines, index)
|
|
141
|
+
blocks << block
|
|
142
|
+
previous_block_type = :list
|
|
143
|
+
elsif blockquote_start?(line)
|
|
144
|
+
block, index = parse_blockquote(lines, index)
|
|
145
|
+
blocks << block
|
|
146
|
+
previous_block_type = :blockquote
|
|
147
|
+
elsif list_start?(line)
|
|
148
|
+
block, index = parse_list(lines, index)
|
|
149
|
+
blocks << block
|
|
150
|
+
previous_block_type = :list
|
|
151
|
+
elsif html_block_start?(line)
|
|
152
|
+
block, index = parse_html_block(lines, index)
|
|
153
|
+
blocks << block
|
|
154
|
+
previous_block_type = :html
|
|
155
|
+
else
|
|
156
|
+
block, index = parse_paragraph(lines, index)
|
|
157
|
+
blocks << block unless block.empty?
|
|
158
|
+
previous_block_type = :paragraph unless block.empty?
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
blocks
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def parse_heading(line)
|
|
166
|
+
if (heading = parse_atx_heading(line))
|
|
167
|
+
return heading
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
match = RDOC_HEADING_RE.match(line)
|
|
171
|
+
return unless match
|
|
172
|
+
|
|
173
|
+
heading_marks = match[1]
|
|
174
|
+
heading_text = match[2].strip
|
|
175
|
+
return nil if heading_text =~ /\A[=\-]+\z/
|
|
176
|
+
|
|
177
|
+
level = [heading_marks.length, 6].min
|
|
178
|
+
"<h#{level}#{heading_id(heading_text)}>#{format_inline(heading_text)}</h#{level}>"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def parse_setext_heading(lines, index)
|
|
182
|
+
return nil if index + 1 >= lines.length
|
|
183
|
+
return nil if lines[index].strip.empty?
|
|
184
|
+
return nil if lines[index] =~ /^\s{0,3}>/
|
|
185
|
+
return nil if parse_list_marker(lines[index])
|
|
186
|
+
return nil if lines[index] =~ /^(?: {4,}|\t)/
|
|
187
|
+
return nil if parse_heading(lines[index])
|
|
188
|
+
return nil if fenced_code_start?(lines[index])
|
|
189
|
+
|
|
190
|
+
content_lines = []
|
|
191
|
+
current_index = index
|
|
192
|
+
|
|
193
|
+
while current_index < lines.length
|
|
194
|
+
line = lines[current_index]
|
|
195
|
+
return nil if blank_line?(line)
|
|
196
|
+
|
|
197
|
+
if line =~ SETEXT_HEADING_RE
|
|
198
|
+
return nil if content_lines.empty?
|
|
199
|
+
|
|
200
|
+
level = $1.start_with?('=') ? 1 : 2
|
|
201
|
+
text = content_lines.join("\n")
|
|
202
|
+
return ["<h#{level}#{heading_id(text)}>#{format_inline(text)}</h#{level}>", current_index + 1]
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
if current_index > index && block_boundary?(line)
|
|
206
|
+
return nil
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
content_lines << normalize_heading_line(line)
|
|
210
|
+
current_index += 1
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
nil
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def parse_fenced_code(lines, index)
|
|
217
|
+
opener = parse_fence_opener(lines[index])
|
|
218
|
+
fence_char = opener[:char]
|
|
219
|
+
fence_length = opener[:length]
|
|
220
|
+
indent = opener[:indent]
|
|
221
|
+
lang = opener[:lang]
|
|
222
|
+
index += 1
|
|
223
|
+
body = []
|
|
224
|
+
|
|
225
|
+
while index < lines.length
|
|
226
|
+
break if fence_closer?(lines[index], fence_char, fence_length)
|
|
227
|
+
|
|
228
|
+
body << strip_fenced_indent(lines[index], indent)
|
|
229
|
+
index += 1
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
index += 1 if index < lines.length
|
|
233
|
+
[code_block(body.join, lang), index]
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def parse_indented_code(lines, index)
|
|
237
|
+
body = []
|
|
238
|
+
previous_blank = false
|
|
239
|
+
|
|
240
|
+
while index < lines.length
|
|
241
|
+
line = lines[index]
|
|
242
|
+
break if previous_blank && html_block_start?(line)
|
|
243
|
+
break unless blank_line?(line) || indented_code_start?(line)
|
|
244
|
+
body << line
|
|
245
|
+
previous_blank = blank_line?(line)
|
|
246
|
+
index += 1
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
body.pop while body.any? && blank_line?(body.last)
|
|
250
|
+
[code_block(unindent_indented_code(body)), index]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def parse_yard_indented_code(lines, index)
|
|
254
|
+
body = []
|
|
255
|
+
|
|
256
|
+
while index < lines.length
|
|
257
|
+
line = lines[index]
|
|
258
|
+
break unless blank_line?(line) || indented_code_start?(line)
|
|
259
|
+
body << line
|
|
260
|
+
index += 1
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
body.pop while body.any? && blank_line?(body.last)
|
|
264
|
+
[code_block(unindent(body)), index]
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def parse_table(lines, index)
|
|
268
|
+
header = split_table_row(lines[index])
|
|
269
|
+
alignments = split_table_row(lines[index + 1]).map { |cell| table_alignment(cell) }
|
|
270
|
+
rows = []
|
|
271
|
+
index += 2
|
|
272
|
+
|
|
273
|
+
while index < lines.length && table_row?(lines[index])
|
|
274
|
+
rows << split_table_row(lines[index])
|
|
275
|
+
index += 1
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
html = "<table>\n<thead>\n<tr>\n".dup
|
|
279
|
+
header.each_with_index do |cell, i|
|
|
280
|
+
attrs = alignments[i] ? %( align="#{alignments[i]}") : ""
|
|
281
|
+
html << "<th#{attrs}>#{format_inline(cell)}</th>\n"
|
|
282
|
+
end
|
|
283
|
+
html << "</tr>\n</thead>\n<tbody>\n"
|
|
284
|
+
rows.each do |row|
|
|
285
|
+
html << "<tr>\n"
|
|
286
|
+
row.each_with_index do |cell, i|
|
|
287
|
+
attrs = alignments[i] ? %( align="#{alignments[i]}") : ""
|
|
288
|
+
html << "<td#{attrs}>#{format_inline(cell)}</td>\n"
|
|
289
|
+
end
|
|
290
|
+
html << "</tr>\n"
|
|
291
|
+
end
|
|
292
|
+
html << "</tbody>\n</table>"
|
|
293
|
+
[html, index]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def parse_list(lines, index)
|
|
297
|
+
marker = parse_list_marker(lines[index])
|
|
298
|
+
ordered = marker[:ordered]
|
|
299
|
+
tag = ordered ? 'ol' : 'ul'
|
|
300
|
+
start_attr = ordered && marker[:start] != 1 ? %( start="#{marker[:start]}") : ''
|
|
301
|
+
items = []
|
|
302
|
+
tight = true
|
|
303
|
+
list_indent = marker[:indent]
|
|
304
|
+
|
|
305
|
+
while index < lines.length
|
|
306
|
+
break if items.any? && thematic_break?(lines[index]) && leading_columns(lines[index]) <= list_indent + 3
|
|
307
|
+
|
|
308
|
+
item_marker = parse_list_marker(lines[index])
|
|
309
|
+
break unless item_marker && same_list_type?(marker, item_marker)
|
|
310
|
+
|
|
311
|
+
effective_padding = list_item_padding(item_marker)
|
|
312
|
+
content_indent = item_marker[:indent] + item_marker[:marker_length] + effective_padding
|
|
313
|
+
lazy_indent = item_marker[:indent] + effective_padding
|
|
314
|
+
item_lines = []
|
|
315
|
+
first_line = item_marker[:content]
|
|
316
|
+
unless first_line.empty?
|
|
317
|
+
leading = [item_marker[:padding] - effective_padding, 0].max
|
|
318
|
+
item_lines << "#{' ' * leading}#{first_line}\n"
|
|
319
|
+
end
|
|
320
|
+
index += 1
|
|
321
|
+
blank_seen = false
|
|
322
|
+
item_loose = false
|
|
323
|
+
|
|
324
|
+
while index < lines.length
|
|
325
|
+
line = lines[index]
|
|
326
|
+
break if thematic_break?(line) && !indented_to?(line, content_indent)
|
|
327
|
+
break if setext_underline_line?(line) && !indented_to?(line, content_indent)
|
|
328
|
+
|
|
329
|
+
next_marker = parse_list_marker(line)
|
|
330
|
+
if next_marker && same_list_type?(marker, next_marker) &&
|
|
331
|
+
(next_marker[:indent] == item_marker[:indent] || (blank_seen && next_marker[:indent] <= list_indent + 3))
|
|
332
|
+
if blank_seen
|
|
333
|
+
tight = false
|
|
334
|
+
end
|
|
335
|
+
break
|
|
336
|
+
end
|
|
337
|
+
break if next_marker && next_marker[:indent] < content_indent
|
|
338
|
+
break if !blank_seen && !indented_to?(line, content_indent) && block_boundary?(line)
|
|
339
|
+
|
|
340
|
+
if blank_line?(line)
|
|
341
|
+
item_lines << "\n"
|
|
342
|
+
blank_seen = true
|
|
343
|
+
elsif blank_seen && indented_to?(line, content_indent)
|
|
344
|
+
break if first_line.empty? && item_lines.all? { |item_line| item_line == "\n" } &&
|
|
345
|
+
leading_columns(line) == content_indent
|
|
346
|
+
item_loose = true if loose_list_item_continuation?(item_lines)
|
|
347
|
+
stripped = strip_list_item_indent(line, content_indent)
|
|
348
|
+
item_lines << stripped
|
|
349
|
+
blank_seen = false
|
|
350
|
+
elsif !blank_seen && indented_to?(line, content_indent)
|
|
351
|
+
stripped = strip_list_item_indent(line, content_indent)
|
|
352
|
+
item_lines << stripped
|
|
353
|
+
blank_seen = false
|
|
354
|
+
elsif !blank_seen
|
|
355
|
+
stripped = strip_list_item_indent(line, lazy_indent)
|
|
356
|
+
stripped = escape_list_marker_text(stripped) if parse_list_marker(stripped)
|
|
357
|
+
item_lines << stripped
|
|
358
|
+
blank_seen = false
|
|
359
|
+
else
|
|
360
|
+
break
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
index += 1
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
item_blocks = parse_blocks(item_lines, 0)
|
|
367
|
+
item_html = item_blocks.join("\n")
|
|
368
|
+
item_html = format_inline(first_line) if item_html.empty? && !first_line.empty?
|
|
369
|
+
|
|
370
|
+
simple_item = !item_loose &&
|
|
371
|
+
item_blocks.length == 1 &&
|
|
372
|
+
item_html =~ /\A<p>(.*?)<\/p>\z/m &&
|
|
373
|
+
item_html !~ /<(?:pre|blockquote|ul|ol|dl|table|h\d|hr)/m
|
|
374
|
+
|
|
375
|
+
if item_html.empty?
|
|
376
|
+
item_html = ''
|
|
377
|
+
else
|
|
378
|
+
item_loose ||= item_blocks.count { |block| block.start_with?('<p>') } > 1
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
tight &&= !item_loose
|
|
382
|
+
items << {:html => item_html, :simple => simple_item}
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
items.map! do |item|
|
|
386
|
+
item_html = item[:html]
|
|
387
|
+
item_html = "<p>#{item_html}</p>" if !tight && !item_html.empty? && item_html !~ /\A</m
|
|
388
|
+
item_html = item_html.sub(/\A<p>(.*?)<\/p>(?=\n<(?:ul|ol|blockquote|pre|h\d|table|hr))/m, '\1') if tight
|
|
389
|
+
item_html = item_html.sub(/\n<p>(.*?)<\/p>\z/m, "\n\\1") if tight
|
|
390
|
+
item_html = item_html.sub(/\A<p>(.*?)<\/p>\z/m, '\1') if item[:simple] && tight
|
|
391
|
+
|
|
392
|
+
if item_html.empty?
|
|
393
|
+
'<li></li>'
|
|
394
|
+
elsif item[:simple] && tight
|
|
395
|
+
"<li>#{item_html}</li>"
|
|
396
|
+
elsif item_html !~ /\A</m
|
|
397
|
+
suffix = item_html.include?("\n") ? "\n" : ''
|
|
398
|
+
"<li>#{item_html}#{suffix}</li>"
|
|
399
|
+
else
|
|
400
|
+
suffix = item_html =~ /(?:<\/(?:p|pre|blockquote|ul|ol|dl|table|h\d)>|<hr \/>|<[A-Za-z][A-Za-z0-9-]*>)\z/m ? "\n" : ''
|
|
401
|
+
"<li>\n#{item_html}#{suffix}</li>"
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
["<#{tag}#{start_attr}>\n#{items.join("\n")}\n</#{tag}>", index]
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def parse_labeled_list(lines, index)
|
|
409
|
+
items = []
|
|
410
|
+
|
|
411
|
+
while index < lines.length
|
|
412
|
+
label, body = parse_labeled_list_line(lines[index])
|
|
413
|
+
break unless label
|
|
414
|
+
|
|
415
|
+
index += 1
|
|
416
|
+
body_lines = []
|
|
417
|
+
body_lines << body if body && !body.empty?
|
|
418
|
+
|
|
419
|
+
while index < lines.length
|
|
420
|
+
line = lines[index]
|
|
421
|
+
break if blank_line?(line)
|
|
422
|
+
break if parse_labeled_list_line(line)
|
|
423
|
+
break if !line.strip.empty? && !line.match(/^(?: {2,}|\t)/)
|
|
424
|
+
|
|
425
|
+
body_lines << line.sub(/^(?: {2,}|\t)/, '').chomp
|
|
426
|
+
index += 1
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
body_html =
|
|
430
|
+
if body_lines.empty?
|
|
431
|
+
''
|
|
432
|
+
else
|
|
433
|
+
parse_blocks(body_lines.map { |l| "#{l}\n" }, 0).join("\n")
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
items << "<dt>#{format_inline(label)}</dt>\n<dd>#{body_html}</dd>"
|
|
437
|
+
index += 1 while index < lines.length && blank_line?(lines[index])
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
["<dl>\n#{items.join("\n")}\n</dl>", index]
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def parse_blockquote(lines, index)
|
|
444
|
+
quoted_lines = []
|
|
445
|
+
saw_quote = false
|
|
446
|
+
previous_blank = false
|
|
447
|
+
|
|
448
|
+
while index < lines.length
|
|
449
|
+
line = lines[index]
|
|
450
|
+
break if saw_quote && quoted_lines.last == "\n" && !blockquote_start?(line)
|
|
451
|
+
break if saw_quote && blank_line?(line) && blockquote_open_fence?(quoted_lines)
|
|
452
|
+
break if saw_quote && previous_blank
|
|
453
|
+
break if saw_quote && !blank_line?(line) && !blockquote_start?(line) &&
|
|
454
|
+
!lazy_blockquote_continuation?(quoted_lines, line)
|
|
455
|
+
break unless blank_line?(line) || blockquote_start?(line) || saw_quote
|
|
456
|
+
|
|
457
|
+
if blank_line?(line)
|
|
458
|
+
quoted_lines << "\n"
|
|
459
|
+
previous_blank = true
|
|
460
|
+
elsif (stripped = strip_blockquote_marker(line))
|
|
461
|
+
quoted_lines << stripped
|
|
462
|
+
saw_quote = true
|
|
463
|
+
previous_blank = false
|
|
464
|
+
else
|
|
465
|
+
if setext_underline_line?(line)
|
|
466
|
+
quoted_lines << " #{line.lstrip}"
|
|
467
|
+
else
|
|
468
|
+
quoted_lines << line
|
|
469
|
+
end
|
|
470
|
+
previous_blank = false
|
|
471
|
+
end
|
|
472
|
+
index += 1
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
inner_html = parse_blocks(quoted_lines, 0).join("\n")
|
|
476
|
+
[inner_html.empty? ? "<blockquote>\n</blockquote>" : "<blockquote>\n#{inner_html}\n</blockquote>", index]
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def parse_html_block(lines, index)
|
|
480
|
+
html = []
|
|
481
|
+
type = html_block_type(lines[index])
|
|
482
|
+
return ['', index] unless type
|
|
483
|
+
|
|
484
|
+
while index < lines.length
|
|
485
|
+
line = lines[index]
|
|
486
|
+
break if html.any? && [6, 7].include?(type) && html_block_end?(type, line)
|
|
487
|
+
break unless html.any? || html_block_type(line)
|
|
488
|
+
|
|
489
|
+
html << line.chomp
|
|
490
|
+
if html_block_end?(type, line)
|
|
491
|
+
index += 1
|
|
492
|
+
break
|
|
493
|
+
end
|
|
494
|
+
index += 1
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
[html.join("\n"), index]
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def parse_paragraph(lines, index)
|
|
501
|
+
buffer = []
|
|
502
|
+
|
|
503
|
+
while index < lines.length
|
|
504
|
+
line = lines[index]
|
|
505
|
+
break if blank_line?(line)
|
|
506
|
+
break if thematic_break?(line)
|
|
507
|
+
break if parse_setext_heading(lines, index)
|
|
508
|
+
break if parse_heading(line)
|
|
509
|
+
break if fenced_code_start?(line)
|
|
510
|
+
break if table_start?(lines, index)
|
|
511
|
+
break if labeled_list_start?(lines, index)
|
|
512
|
+
break if blockquote_start?(line)
|
|
513
|
+
break if list_start?(line, true)
|
|
514
|
+
break if html_block_start?(line, true)
|
|
515
|
+
|
|
516
|
+
buffer << line.chomp
|
|
517
|
+
index += 1
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
text = buffer.map { |line| normalize_paragraph_line(line) }.join("\n").strip
|
|
521
|
+
[text.empty? ? '' : "<p>#{format_inline(text)}</p>", index]
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def format_inline(text)
|
|
525
|
+
placeholders = []
|
|
526
|
+
text = protect_yard_links(text, placeholders)
|
|
527
|
+
text = protect_raw_html(text, placeholders)
|
|
528
|
+
text = protect_code_spans(text, placeholders)
|
|
529
|
+
text = protect_autolinks(text, placeholders)
|
|
530
|
+
text = protect_hard_breaks(text, placeholders)
|
|
531
|
+
text = protect_inline_images(text, placeholders)
|
|
532
|
+
text = protect_inline_links(text, placeholders)
|
|
533
|
+
text = protect_braced_text_links(text, placeholders)
|
|
534
|
+
text = protect_single_word_text_links(text, placeholders)
|
|
535
|
+
text = protect_reference_images(text, placeholders)
|
|
536
|
+
text = protect_reference_links(text, placeholders)
|
|
537
|
+
text = protect_escaped_characters(text, placeholders)
|
|
538
|
+
text = protect_entities(text, placeholders)
|
|
539
|
+
text = text.gsub(/[ \t]+\n/, "\n")
|
|
540
|
+
text = h(text)
|
|
541
|
+
text = format_emphasis(text)
|
|
542
|
+
text = format_strikethrough(text)
|
|
543
|
+
restore_placeholders(autolink_urls(text), placeholders)
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def protect_code_spans(text, placeholders)
|
|
547
|
+
output = String.new
|
|
548
|
+
index = 0
|
|
549
|
+
|
|
550
|
+
while index < text.length
|
|
551
|
+
if text[index, 1] == '`' && (index.zero? || text[index - 1, 1] != '\\') && !inside_angle_autolink_candidate?(text, index)
|
|
552
|
+
opener_length = 1
|
|
553
|
+
opener_length += 1 while index + opener_length < text.length && text[index + opener_length, 1] == '`'
|
|
554
|
+
closer_index = find_matching_backtick_run(text, index + opener_length, opener_length)
|
|
555
|
+
if closer_index
|
|
556
|
+
code = normalize_code_span(restore_placeholders(text[(index + opener_length)...closer_index], placeholders))
|
|
557
|
+
output << store_placeholder(placeholders, "<code>#{h(code)}</code>")
|
|
558
|
+
index = closer_index + opener_length
|
|
559
|
+
next
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
output << ('`' * opener_length)
|
|
563
|
+
index += opener_length
|
|
564
|
+
next
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
output << text[index, 1]
|
|
568
|
+
index += 1
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
output.gsub(/(^|[\s>])\+([^\s+\n](?:[^+\n]*?[^\s+\n])?)\+(?=$|[\s<.,;:!?)]|\z)/) do
|
|
572
|
+
prefix = $1
|
|
573
|
+
prefix + store_placeholder(placeholders, "<code>#{h(restore_placeholders($2, placeholders))}</code>")
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def inside_angle_autolink_candidate?(text, index)
|
|
578
|
+
opening = text.rindex('<', index)
|
|
579
|
+
return false unless opening
|
|
580
|
+
|
|
581
|
+
closing = text.rindex('>', index)
|
|
582
|
+
return false if closing && closing > opening
|
|
583
|
+
|
|
584
|
+
candidate = text[opening...index]
|
|
585
|
+
return false if candidate =~ /\s/
|
|
586
|
+
|
|
587
|
+
candidate =~ /\A<(?:[A-Za-z][A-Za-z0-9.+-]{1,31}:|[A-Za-z0-9.!#$%&'*+\/=?^_`{|}~-]+@)/
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def protect_yard_links(text, placeholders)
|
|
591
|
+
text.gsub(YARD_LINK_RE) do
|
|
592
|
+
match = Regexp.last_match
|
|
593
|
+
if text[match.end(0), 1] == '['
|
|
594
|
+
match[0]
|
|
595
|
+
else
|
|
596
|
+
store_placeholder(placeholders, match[0])
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def protect_autolinks(text, placeholders)
|
|
602
|
+
text.gsub(AUTOLINK_RE) do
|
|
603
|
+
href = $1
|
|
604
|
+
link_href = href.include?('@') && href !~ /\A[A-Za-z][A-Za-z0-9.+-]{1,31}:/ ? "mailto:#{href}" : escape_autolink_url(href)
|
|
605
|
+
store_placeholder(placeholders, %(<a href="#{h(link_href)}">#{h(href)}</a>))
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def protect_raw_html(text, placeholders)
|
|
610
|
+
text.gsub(/(?<!\\)#{HTML_TAG_RE}/m) do
|
|
611
|
+
match = $&
|
|
612
|
+
match_start = Regexp.last_match.begin(0)
|
|
613
|
+
if match_start > 0 && text[match_start - 1, 1] == '`'
|
|
614
|
+
match
|
|
615
|
+
else
|
|
616
|
+
store_placeholder(placeholders, match)
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def protect_escaped_characters(text, placeholders)
|
|
622
|
+
text = text.gsub(RDOC_ESCAPED_CAPITALIZED_CROSSREF_RE) do
|
|
623
|
+
store_placeholder(placeholders, h($1))
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
text.gsub(ESCAPABLE_CHARS_RE) { store_placeholder(placeholders, h($1)) }
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def protect_entities(text, placeholders)
|
|
630
|
+
text.gsub(ENTITY_RE) { store_placeholder(placeholders, h(decode_entity($&))) }
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def protect_hard_breaks(text, placeholders)
|
|
634
|
+
text.gsub(/(?:\\|\s{2,})\n/) { store_placeholder(placeholders, "<br />\n") }
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def protect_inline_images(text, placeholders)
|
|
638
|
+
replace_inline_constructs(text, placeholders, '!') do |label, dest, title|
|
|
639
|
+
store_placeholder(placeholders, image_html(
|
|
640
|
+
restore_placeholders(label, placeholders),
|
|
641
|
+
restore_placeholders(dest, placeholders),
|
|
642
|
+
title && restore_placeholders(title, placeholders)
|
|
643
|
+
))
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def protect_inline_links(text, placeholders)
|
|
648
|
+
replace_inline_constructs(text, placeholders, nil) do |label, dest, title|
|
|
649
|
+
store_placeholder(placeholders, link_html(
|
|
650
|
+
restore_placeholders(label, placeholders),
|
|
651
|
+
restore_placeholders(dest, placeholders),
|
|
652
|
+
title && restore_placeholders(title, placeholders)
|
|
653
|
+
))
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def protect_reference_images(text, placeholders)
|
|
658
|
+
scan_reference_constructs(text, placeholders, :image)
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
def protect_reference_links(text, placeholders)
|
|
662
|
+
scan_reference_constructs(text, placeholders, :link)
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
def protect_single_word_text_links(text, placeholders)
|
|
666
|
+
output = String.new
|
|
667
|
+
index = 0
|
|
668
|
+
bracket_depth = 0
|
|
669
|
+
|
|
670
|
+
while index < text.length
|
|
671
|
+
char = text[index, 1]
|
|
672
|
+
|
|
673
|
+
if char == '\\' && index + 1 < text.length
|
|
674
|
+
output << text[index, 2]
|
|
675
|
+
index += 2
|
|
676
|
+
next
|
|
677
|
+
elsif char == '['
|
|
678
|
+
bracket_depth += 1
|
|
679
|
+
elsif char == ']' && bracket_depth > 0
|
|
680
|
+
bracket_depth -= 1
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
if bracket_depth.zero? && (match = text[index..-1].match(/\A([A-Za-z0-9]+)(?=\[)/))
|
|
684
|
+
label = match[1]
|
|
685
|
+
dest, consumed = parse_text_link_destination(text, index + label.length)
|
|
686
|
+
|
|
687
|
+
if dest
|
|
688
|
+
output << store_placeholder(placeholders, link_html(label, dest))
|
|
689
|
+
index += label.length + consumed
|
|
690
|
+
next
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
output << char
|
|
695
|
+
index += 1
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
output
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def protect_braced_text_links(text, placeholders)
|
|
702
|
+
output = String.new
|
|
703
|
+
index = 0
|
|
704
|
+
|
|
705
|
+
while index < text.length
|
|
706
|
+
if text[index, 1] == '\\' && index + 1 < text.length
|
|
707
|
+
output << text[index, 2]
|
|
708
|
+
index += 2
|
|
709
|
+
next
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
if text[index, 1] == '{'
|
|
713
|
+
label_end = find_braced_text_link_label_end(text, index)
|
|
714
|
+
if label_end
|
|
715
|
+
label = text[(index + 1)...label_end]
|
|
716
|
+
dest, consumed = parse_text_link_destination(text, label_end + 1)
|
|
717
|
+
|
|
718
|
+
if dest
|
|
719
|
+
output << store_placeholder(placeholders, link_html(label, dest))
|
|
720
|
+
index = label_end + 1 + consumed
|
|
721
|
+
next
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
output << text[index, 1]
|
|
727
|
+
index += 1
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
output
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
def format_emphasis(text)
|
|
734
|
+
delimiters = []
|
|
735
|
+
output = []
|
|
736
|
+
index = 0
|
|
737
|
+
|
|
738
|
+
while index < text.length
|
|
739
|
+
char = text[index, 1]
|
|
740
|
+
if char == '*' || char == '_'
|
|
741
|
+
run_end = index
|
|
742
|
+
run_end += 1 while run_end < text.length && text[run_end, 1] == char
|
|
743
|
+
run_length = run_end - index
|
|
744
|
+
can_open, can_close = delimiter_flags(text, index, run_end, char)
|
|
745
|
+
token = {
|
|
746
|
+
:char => char,
|
|
747
|
+
:length => run_length,
|
|
748
|
+
:position => output.length,
|
|
749
|
+
:left_consumed => 0,
|
|
750
|
+
:right_consumed => 0,
|
|
751
|
+
:opening_html => String.new,
|
|
752
|
+
:closing_html => String.new,
|
|
753
|
+
:can_open => can_open,
|
|
754
|
+
:can_close => can_close
|
|
755
|
+
}
|
|
756
|
+
output << token
|
|
757
|
+
|
|
758
|
+
if can_close
|
|
759
|
+
delimiter_index = delimiters.length - 1
|
|
760
|
+
while delimiter_index >= 0 && available_delimiter_length(token) > 0
|
|
761
|
+
opener = delimiters[delimiter_index]
|
|
762
|
+
if opener[:char] == char && available_delimiter_length(opener) > 0 &&
|
|
763
|
+
!odd_match_disallowed?(opener, token)
|
|
764
|
+
use = available_delimiter_length(opener) >= 2 &&
|
|
765
|
+
available_delimiter_length(token) >= 2 ? 2 : 1
|
|
766
|
+
opener[:right_consumed] += use
|
|
767
|
+
opener[:opening_html] = (use == 2 ? '<strong>' : '<em>') + opener[:opening_html]
|
|
768
|
+
token[:left_consumed] += use
|
|
769
|
+
token[:closing_html] << (use == 2 ? '</strong>' : '</em>')
|
|
770
|
+
delimiters.reject! do |candidate|
|
|
771
|
+
candidate[:position] > opener[:position] &&
|
|
772
|
+
candidate[:position] < token[:position] &&
|
|
773
|
+
available_delimiter_length(candidate) > 0
|
|
774
|
+
end
|
|
775
|
+
delimiters.delete_at(delimiter_index) if available_delimiter_length(opener).zero?
|
|
776
|
+
delimiter_index = delimiters.length - 1
|
|
777
|
+
else
|
|
778
|
+
delimiter_index -= 1
|
|
779
|
+
end
|
|
780
|
+
end
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
delimiters << token if can_open && available_delimiter_length(token) > 0
|
|
784
|
+
index = run_end
|
|
785
|
+
else
|
|
786
|
+
output << char
|
|
787
|
+
index += 1
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
output.map do |piece|
|
|
792
|
+
next piece if piece.is_a?(String)
|
|
793
|
+
|
|
794
|
+
piece[:closing_html] +
|
|
795
|
+
(piece[:char] * available_delimiter_length(piece)) +
|
|
796
|
+
piece[:opening_html]
|
|
797
|
+
end.join
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
def format_strikethrough(text)
|
|
801
|
+
text.gsub(/~~([^\n~](?:.*?[^\n~])?)~~/, '<del>\1</del>')
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
def autolink_urls(text)
|
|
805
|
+
text.gsub(/(^|[^\w\/{"'=])((?:https?:\/\/|mailto:)[^\s<]+)/) do
|
|
806
|
+
match = Regexp.last_match
|
|
807
|
+
prefix = $1
|
|
808
|
+
before_url = text[0...match.begin(2)]
|
|
809
|
+
if before_url.end_with?('<') || before_url.end_with?('< ')
|
|
810
|
+
match[0]
|
|
811
|
+
else
|
|
812
|
+
url, trailer = strip_trailing_punctuation($2)
|
|
813
|
+
%(#{prefix}<a href="#{h(url)}">#{h(url)}</a>#{h(trailer)})
|
|
814
|
+
end
|
|
815
|
+
end
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
def restore_placeholders(text, placeholders)
|
|
819
|
+
text.gsub(PLACEHOLDER_RE) { placeholders[$1.to_i] }
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
def store_placeholder(placeholders, html)
|
|
823
|
+
placeholders << html
|
|
824
|
+
"\0#{placeholders.length - 1}\0"
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
def parse_labeled_list_line(line)
|
|
828
|
+
return [$1, $2] if line =~ LABEL_LIST_COLON_RE
|
|
829
|
+
|
|
830
|
+
nil
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
def extract_reference_definitions(text)
|
|
834
|
+
lines = split_lines(text)
|
|
835
|
+
kept_lines = []
|
|
836
|
+
index = 0
|
|
837
|
+
in_fenced_code = false
|
|
838
|
+
previous_line = nil
|
|
839
|
+
|
|
840
|
+
while index < lines.length
|
|
841
|
+
line = lines[index]
|
|
842
|
+
if fenced_code_start?(line)
|
|
843
|
+
in_fenced_code = !in_fenced_code
|
|
844
|
+
kept_lines << line
|
|
845
|
+
index += 1
|
|
846
|
+
previous_line = line
|
|
847
|
+
next
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
if in_fenced_code
|
|
851
|
+
kept_lines << line
|
|
852
|
+
index += 1
|
|
853
|
+
previous_line = line
|
|
854
|
+
next
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
parsed = parse_reference_definition_block(lines, index, previous_line)
|
|
858
|
+
if parsed
|
|
859
|
+
normalized = normalize_reference_label(parsed[:label])
|
|
860
|
+
@references[normalized] ||= parsed[:reference] unless normalized.empty?
|
|
861
|
+
kept_lines.concat(parsed[:replacement_lines])
|
|
862
|
+
index = parsed[:next_index]
|
|
863
|
+
previous_line = kept_lines.last
|
|
864
|
+
next
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
kept_lines << line
|
|
868
|
+
index += 1
|
|
869
|
+
previous_line = line
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
kept_lines.join
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
def normalize_reference_label(label)
|
|
876
|
+
normalized = label.to_s.gsub(/\\([\[\]])/, '\1').gsub(/\s+/, ' ').strip
|
|
877
|
+
unicode_casefold_compat(normalized)
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
def reference_link_html(label, ref)
|
|
881
|
+
reference = @references[normalize_reference_label(ref)]
|
|
882
|
+
return nil unless reference
|
|
883
|
+
|
|
884
|
+
attrs = %( href="#{h(reference[:url])}")
|
|
885
|
+
attrs += %( title="#{h(reference[:title])}") if reference[:title]
|
|
886
|
+
%(<a#{attrs}>#{format_inline(unescape_markdown_punctuation(label))}</a>)
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
def reference_image_html(alt, ref)
|
|
890
|
+
reference = @references[normalize_reference_label(ref)]
|
|
891
|
+
return nil unless reference
|
|
892
|
+
|
|
893
|
+
attrs = %( src="#{h(reference[:url])}" alt="#{h(plain_text(alt))}")
|
|
894
|
+
attrs += %( title="#{h(reference[:title])}") if reference[:title]
|
|
895
|
+
"<img#{attrs} />"
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
def blank_line?(line)
|
|
899
|
+
line.strip.empty?
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
def thematic_break?(line)
|
|
903
|
+
line =~ THEMATIC_BREAK_RE
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
def setext_underline_line?(line)
|
|
907
|
+
line =~ SETEXT_HEADING_RE
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
def fenced_code_start?(line)
|
|
911
|
+
!!parse_fence_opener(line)
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
def indented_code_start?(line)
|
|
915
|
+
leading_columns(line) >= 2
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
def indented_code_block_start?(lines, index, previous_block_type = nil)
|
|
919
|
+
return false unless indented_code_start?(lines[index])
|
|
920
|
+
return true if leading_columns(lines[index]) >= 4
|
|
921
|
+
return false if previous_block_type == :list
|
|
922
|
+
return false if html_block_start?(lines[index])
|
|
923
|
+
return false if parse_setext_heading(lines, index)
|
|
924
|
+
|
|
925
|
+
!index.zero? && blank_line?(lines[index - 1])
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
def yard_indented_code_start?(lines, index)
|
|
929
|
+
return false unless leading_columns(lines[index]) >= 2
|
|
930
|
+
return false unless consume_columns(lines[index], 2) =~ /^!!!([\w.+-]+)[ \t]*$/
|
|
931
|
+
return false if index + 1 >= lines.length
|
|
932
|
+
|
|
933
|
+
indented_code_block_start?(lines, index) && indented_code_start?(lines[index + 1])
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
def list_start?(line, interrupt_paragraph = false)
|
|
937
|
+
return false unless (marker = parse_list_marker(line))
|
|
938
|
+
return true unless interrupt_paragraph
|
|
939
|
+
|
|
940
|
+
return false if marker[:content].empty?
|
|
941
|
+
|
|
942
|
+
!marker[:ordered] || marker[:start] == 1
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
def labeled_list_start?(lines, index)
|
|
946
|
+
line = lines[index]
|
|
947
|
+
return true if line =~ LABEL_LIST_COLON_RE
|
|
948
|
+
false
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
def blockquote_start?(line)
|
|
952
|
+
!strip_blockquote_marker(line).nil?
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
def html_block_start?(line, interrupt_paragraph = false)
|
|
956
|
+
!html_block_type(line, interrupt_paragraph).nil?
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
def table_start?(lines, index)
|
|
960
|
+
return false if index + 1 >= lines.length
|
|
961
|
+
table_row?(lines[index]) && lines[index + 1] =~ TABLE_SEPARATOR_RE
|
|
962
|
+
end
|
|
963
|
+
|
|
964
|
+
def table_row?(line)
|
|
965
|
+
stripped = line.strip
|
|
966
|
+
stripped.include?('|') && stripped !~ /\A[|:\-\s]+\z/
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
def split_table_row(line)
|
|
970
|
+
line.strip.sub(/\A\|/, '').sub(/\|\z/, '').split('|').map(&:strip)
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
def table_alignment(cell)
|
|
974
|
+
stripped = cell.strip
|
|
975
|
+
return 'center' if stripped.start_with?(':') && stripped.end_with?(':')
|
|
976
|
+
return 'left' if stripped.start_with?(':')
|
|
977
|
+
return 'right' if stripped.end_with?(':')
|
|
978
|
+
|
|
979
|
+
nil
|
|
980
|
+
end
|
|
981
|
+
|
|
982
|
+
def unindent(lines)
|
|
983
|
+
indent = lines.reject { |line| blank_line?(line) }.map do |line|
|
|
984
|
+
leading_columns(line)
|
|
985
|
+
end.min || 4
|
|
986
|
+
|
|
987
|
+
lines.map { |line| consume_columns(line, indent) }.join
|
|
988
|
+
end
|
|
989
|
+
|
|
990
|
+
def unindent_indented_code(lines)
|
|
991
|
+
lines.map { |line| consume_columns(line, 4) }.join
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
def code_block(text, lang = nil)
|
|
995
|
+
lang, text = extract_codeblock_language(text, lang)
|
|
996
|
+
attrs = lang ? %( class="#{h(lang)}") : ''
|
|
997
|
+
"<pre><code#{attrs}>#{h(text)}</code></pre>"
|
|
998
|
+
end
|
|
999
|
+
|
|
1000
|
+
def extract_codeblock_language(text, lang = nil)
|
|
1001
|
+
return [lang, text] unless text =~ CODE_LANG_RE
|
|
1002
|
+
|
|
1003
|
+
lang ||= unescape_markdown_punctuation(decode_entities($1))
|
|
1004
|
+
[lang, $']
|
|
1005
|
+
end
|
|
1006
|
+
|
|
1007
|
+
def strip_trailing_punctuation(url)
|
|
1008
|
+
trailer = ''
|
|
1009
|
+
while url =~ /[),.;:!?]\z/
|
|
1010
|
+
trailer = url[-1, 1] + trailer
|
|
1011
|
+
url = url[0...-1]
|
|
1012
|
+
end
|
|
1013
|
+
[url, trailer]
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
def heading_id(text)
|
|
1017
|
+
return '' unless @heading_ids
|
|
1018
|
+
|
|
1019
|
+
" id=\"#{text.gsub(/\W/, '_')}\""
|
|
1020
|
+
end
|
|
1021
|
+
|
|
1022
|
+
def parse_atx_heading(line)
|
|
1023
|
+
stripped = line.chomp.sub(/^\s{0,3}/, '')
|
|
1024
|
+
match = stripped.match(/\A(#{'#' * 6}|#{'#' * 5}|#{'#' * 4}|#{'#' * 3}|#{'#' * 2}|#)(?=[ \t]|$)(.*)\z/)
|
|
1025
|
+
return nil unless match
|
|
1026
|
+
|
|
1027
|
+
level = match[1].length
|
|
1028
|
+
content = match[2]
|
|
1029
|
+
content = content.sub(/\A[ \t]+/, '')
|
|
1030
|
+
content = content.sub(/[ \t]+#+[ \t]*\z/, '')
|
|
1031
|
+
content = '' if content =~ /\A#+\z/
|
|
1032
|
+
content = content.rstrip
|
|
1033
|
+
"<h#{level}#{heading_id(content)}>#{format_inline(content)}</h#{level}>"
|
|
1034
|
+
end
|
|
1035
|
+
|
|
1036
|
+
def parse_fence_opener(line)
|
|
1037
|
+
match = line.match(FENCE_RE)
|
|
1038
|
+
return nil unless match
|
|
1039
|
+
|
|
1040
|
+
indent = match[1].length
|
|
1041
|
+
fence = match[2]
|
|
1042
|
+
info = match[3].to_s.strip
|
|
1043
|
+
return nil if fence.start_with?('`') && info.include?('`')
|
|
1044
|
+
|
|
1045
|
+
lang = info.empty? ? nil : unescape_markdown_punctuation(decode_entities(info.split(/[ \t]/, 2).first))
|
|
1046
|
+
{:char => fence[0, 1], :length => fence.length, :indent => indent, :lang => lang}
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
def fence_closer?(line, char, min_length)
|
|
1050
|
+
stripped = line.sub(/^\s{0,3}/, '')
|
|
1051
|
+
return false unless stripped.start_with?(char)
|
|
1052
|
+
|
|
1053
|
+
run = stripped[/\A#{Regexp.escape(char)}+/]
|
|
1054
|
+
run && run.length >= min_length && stripped.sub(/\A#{Regexp.escape(run)}/, '').strip.empty?
|
|
1055
|
+
end
|
|
1056
|
+
|
|
1057
|
+
def strip_fenced_indent(line, indent)
|
|
1058
|
+
return line.sub(/^\t/, '') if line.start_with?("\t")
|
|
1059
|
+
|
|
1060
|
+
line.sub(/\A {0,#{indent}}/, '')
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
def parse_list_marker(line)
|
|
1064
|
+
source = line.to_s.sub(/\n\z/, '')
|
|
1065
|
+
indent, index = scan_leading_columns(source)
|
|
1066
|
+
return nil if indent > 3
|
|
1067
|
+
return nil if index >= source.length
|
|
1068
|
+
|
|
1069
|
+
char = source[index, 1]
|
|
1070
|
+
current_column = indent
|
|
1071
|
+
|
|
1072
|
+
if '*+-'.include?(char)
|
|
1073
|
+
marker_length = 1
|
|
1074
|
+
marker_end = index + 1
|
|
1075
|
+
current_column += 1
|
|
1076
|
+
padding, marker_end = scan_padding_columns(source, marker_end, current_column)
|
|
1077
|
+
content = source[marker_end..-1].to_s
|
|
1078
|
+
return nil if padding.zero? && !content.empty?
|
|
1079
|
+
|
|
1080
|
+
return {:ordered => false, :bullet => char, :indent => indent,
|
|
1081
|
+
:marker_length => marker_length, :padding => padding, :content => content}
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
number = source[index..-1][/^\d{1,9}/]
|
|
1085
|
+
if number
|
|
1086
|
+
marker_end = index + number.length
|
|
1087
|
+
delimiter = source[marker_end, 1]
|
|
1088
|
+
if delimiter == '.' || delimiter == ')'
|
|
1089
|
+
marker_length = number.length + 1
|
|
1090
|
+
current_column += marker_length
|
|
1091
|
+
marker_end += 1
|
|
1092
|
+
padding, marker_end = scan_padding_columns(source, marker_end, current_column)
|
|
1093
|
+
content = source[marker_end..-1].to_s
|
|
1094
|
+
return nil if padding.zero? && !content.empty?
|
|
1095
|
+
|
|
1096
|
+
return {:ordered => true, :delimiter => delimiter, :start => number.to_i,
|
|
1097
|
+
:indent => indent, :marker_length => marker_length,
|
|
1098
|
+
:padding => padding, :content => content}
|
|
1099
|
+
end
|
|
1100
|
+
end
|
|
1101
|
+
|
|
1102
|
+
if source[index, 2] =~ /\A[A-Za-z]\.\z/
|
|
1103
|
+
marker_length = 2
|
|
1104
|
+
marker_end = index + marker_length
|
|
1105
|
+
current_column += marker_length
|
|
1106
|
+
padding, marker_end = scan_padding_columns(source, marker_end, current_column)
|
|
1107
|
+
content = source[marker_end..-1].to_s
|
|
1108
|
+
return nil if padding.zero? && !content.empty?
|
|
1109
|
+
|
|
1110
|
+
return {:ordered => true, :delimiter => '.', :start => 1,
|
|
1111
|
+
:indent => indent, :marker_length => marker_length,
|
|
1112
|
+
:padding => padding, :content => content}
|
|
1113
|
+
end
|
|
1114
|
+
|
|
1115
|
+
nil
|
|
1116
|
+
end
|
|
1117
|
+
|
|
1118
|
+
def list_item_padding(marker)
|
|
1119
|
+
(1..4).include?(marker[:padding]) ? marker[:padding] : 1
|
|
1120
|
+
end
|
|
1121
|
+
|
|
1122
|
+
def same_list_type?(base, other)
|
|
1123
|
+
return false unless other
|
|
1124
|
+
return base[:bullet] == other[:bullet] if !base[:ordered] && !other[:ordered]
|
|
1125
|
+
|
|
1126
|
+
base[:ordered] && other[:ordered] && base[:delimiter] == other[:delimiter]
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1129
|
+
def block_boundary?(line)
|
|
1130
|
+
thematic_break?(line) || parse_heading(line) || fenced_code_start?(line) ||
|
|
1131
|
+
table_row?(line) || labeled_list_start?([line, ''], 0) || blockquote_start?(line) ||
|
|
1132
|
+
html_block_start?(line) || parse_list_marker(line)
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1135
|
+
def parse_reference_definition(label, definition)
|
|
1136
|
+
definition = definition.to_s
|
|
1137
|
+
return nil if normalize_reference_label(label).empty?
|
|
1138
|
+
|
|
1139
|
+
index = 0
|
|
1140
|
+
index += 1 while index < definition.length && definition[index, 1] =~ /[ \t\n]/
|
|
1141
|
+
return nil if index >= definition.length
|
|
1142
|
+
|
|
1143
|
+
if definition[index, 1] == '<'
|
|
1144
|
+
close = definition.index('>', index + 1)
|
|
1145
|
+
return nil unless close
|
|
1146
|
+
url = definition[(index + 1)...close]
|
|
1147
|
+
return nil if url.include?("\n")
|
|
1148
|
+
index = close + 1
|
|
1149
|
+
return nil if index < definition.length && definition[index, 1] !~ /[ \t\n]/
|
|
1150
|
+
else
|
|
1151
|
+
start = index
|
|
1152
|
+
while index < definition.length && definition[index, 1] !~ /[ \t\n]/
|
|
1153
|
+
index += 1
|
|
1154
|
+
end
|
|
1155
|
+
url = definition[start...index]
|
|
1156
|
+
end
|
|
1157
|
+
|
|
1158
|
+
return nil if url.nil? || url.include?('<') || url.include?('>')
|
|
1159
|
+
|
|
1160
|
+
index += 1 while index < definition.length && definition[index, 1] =~ /[ \t\n]/
|
|
1161
|
+
title = nil
|
|
1162
|
+
|
|
1163
|
+
if index < definition.length
|
|
1164
|
+
delimiter = definition[index, 1]
|
|
1165
|
+
close_delimiter = delimiter == '(' ? ')' : delimiter
|
|
1166
|
+
if delimiter == '"' || delimiter == "'" || delimiter == '('
|
|
1167
|
+
index += 1
|
|
1168
|
+
start = index
|
|
1169
|
+
buffer = String.new
|
|
1170
|
+
while index < definition.length
|
|
1171
|
+
char = definition[index, 1]
|
|
1172
|
+
if char == '\\' && index + 1 < definition.length
|
|
1173
|
+
buffer << definition[index, 2]
|
|
1174
|
+
index += 2
|
|
1175
|
+
next
|
|
1176
|
+
end
|
|
1177
|
+
break if char == close_delimiter
|
|
1178
|
+
buffer << char
|
|
1179
|
+
index += 1
|
|
1180
|
+
end
|
|
1181
|
+
return nil if index >= definition.length || definition[index, 1] != close_delimiter
|
|
1182
|
+
title = buffer
|
|
1183
|
+
index += 1
|
|
1184
|
+
index += 1 while index < definition.length && definition[index, 1] =~ /[ \t\n]/
|
|
1185
|
+
return nil unless index == definition.length
|
|
1186
|
+
else
|
|
1187
|
+
return nil
|
|
1188
|
+
end
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
{
|
|
1192
|
+
:url => escape_url(unescape_markdown_punctuation(decode_entities(url))),
|
|
1193
|
+
:title => title && unescape_markdown_punctuation(decode_entities(title))
|
|
1194
|
+
}
|
|
1195
|
+
end
|
|
1196
|
+
|
|
1197
|
+
def replace_inline_constructs(text, placeholders, prefix)
|
|
1198
|
+
output = String.new
|
|
1199
|
+
index = 0
|
|
1200
|
+
|
|
1201
|
+
while index < text.length
|
|
1202
|
+
if prefix
|
|
1203
|
+
if text[index, 2] != '![' || (index > 0 && text[index - 1, 1] == '\\')
|
|
1204
|
+
output << text[index, 1]
|
|
1205
|
+
index += 1
|
|
1206
|
+
next
|
|
1207
|
+
end
|
|
1208
|
+
label_start = index + 2
|
|
1209
|
+
else
|
|
1210
|
+
if text[index, 1] != '[' || (index > 0 && text[index - 1, 1] == '\\')
|
|
1211
|
+
output << text[index, 1]
|
|
1212
|
+
index += 1
|
|
1213
|
+
next
|
|
1214
|
+
end
|
|
1215
|
+
label_start = index + 1
|
|
1216
|
+
end
|
|
1217
|
+
|
|
1218
|
+
label_end = find_closing_bracket(text, label_start - 1)
|
|
1219
|
+
unless label_end && text[label_end + 1, 1] == '('
|
|
1220
|
+
output << text[index, 1]
|
|
1221
|
+
index += 1
|
|
1222
|
+
next
|
|
1223
|
+
end
|
|
1224
|
+
|
|
1225
|
+
dest, title, consumed = parse_inline_destination(text, label_end + 2, placeholders)
|
|
1226
|
+
unless consumed
|
|
1227
|
+
output << text[index, 1]
|
|
1228
|
+
index += 1
|
|
1229
|
+
next
|
|
1230
|
+
end
|
|
1231
|
+
|
|
1232
|
+
label = text[label_start...label_end]
|
|
1233
|
+
if !prefix && contains_nested_link?(label, placeholders)
|
|
1234
|
+
output << text[index, 1]
|
|
1235
|
+
index += 1
|
|
1236
|
+
next
|
|
1237
|
+
end
|
|
1238
|
+
output << yield(label, dest, title)
|
|
1239
|
+
index = consumed
|
|
1240
|
+
end
|
|
1241
|
+
|
|
1242
|
+
output
|
|
1243
|
+
end
|
|
1244
|
+
|
|
1245
|
+
def scan_reference_constructs(text, placeholders, kind)
|
|
1246
|
+
output = String.new
|
|
1247
|
+
index = 0
|
|
1248
|
+
|
|
1249
|
+
while index < text.length
|
|
1250
|
+
image = kind == :image
|
|
1251
|
+
if image
|
|
1252
|
+
if text[index, 2] != '![' || (index > 0 && text[index - 1, 1] == '\\')
|
|
1253
|
+
output << text[index, 1]
|
|
1254
|
+
index += 1
|
|
1255
|
+
next
|
|
1256
|
+
end
|
|
1257
|
+
label_open = index + 1
|
|
1258
|
+
else
|
|
1259
|
+
if text[index, 1] != '[' || (index > 0 && text[index - 1, 1] == '\\')
|
|
1260
|
+
output << text[index, 1]
|
|
1261
|
+
index += 1
|
|
1262
|
+
next
|
|
1263
|
+
end
|
|
1264
|
+
label_open = index
|
|
1265
|
+
end
|
|
1266
|
+
|
|
1267
|
+
label_close = find_closing_bracket(text, label_open)
|
|
1268
|
+
unless label_close
|
|
1269
|
+
output << text[index, 1]
|
|
1270
|
+
index += 1
|
|
1271
|
+
next
|
|
1272
|
+
end
|
|
1273
|
+
|
|
1274
|
+
next_char = text[label_close + 1, 1]
|
|
1275
|
+
label = restore_placeholders(text[(label_open + 1)...label_close], placeholders)
|
|
1276
|
+
html = nil
|
|
1277
|
+
consumed = nil
|
|
1278
|
+
|
|
1279
|
+
if next_char == '['
|
|
1280
|
+
ref_close = find_closing_bracket(text, label_close + 1)
|
|
1281
|
+
if ref_close
|
|
1282
|
+
ref = restore_placeholders(text[(label_close + 2)...ref_close], placeholders)
|
|
1283
|
+
ref = label if ref.empty?
|
|
1284
|
+
if kind == :link && contains_nested_link?(label, placeholders)
|
|
1285
|
+
output << text[index]
|
|
1286
|
+
index += 1
|
|
1287
|
+
next
|
|
1288
|
+
end
|
|
1289
|
+
html = kind == :image ? reference_image_html(label, ref) : reference_link_html(label, ref)
|
|
1290
|
+
consumed = ref_close + 1 if html
|
|
1291
|
+
end
|
|
1292
|
+
else
|
|
1293
|
+
if kind == :link && contains_nested_link?(label, placeholders)
|
|
1294
|
+
output << text[index, 1]
|
|
1295
|
+
index += 1
|
|
1296
|
+
next
|
|
1297
|
+
end
|
|
1298
|
+
html = kind == :image ? reference_image_html(label, label) : reference_link_html(label, label)
|
|
1299
|
+
consumed = label_close + 1 if html
|
|
1300
|
+
end
|
|
1301
|
+
|
|
1302
|
+
if html
|
|
1303
|
+
output << store_placeholder(placeholders, html)
|
|
1304
|
+
index = consumed
|
|
1305
|
+
else
|
|
1306
|
+
output << text[index, 1]
|
|
1307
|
+
index += 1
|
|
1308
|
+
end
|
|
1309
|
+
end
|
|
1310
|
+
|
|
1311
|
+
output
|
|
1312
|
+
end
|
|
1313
|
+
|
|
1314
|
+
def find_closing_bracket(text, open_index)
|
|
1315
|
+
depth = 0
|
|
1316
|
+
index = open_index
|
|
1317
|
+
while index < text.length
|
|
1318
|
+
char = text[index, 1]
|
|
1319
|
+
if char == '['
|
|
1320
|
+
depth += 1
|
|
1321
|
+
elsif char == ']'
|
|
1322
|
+
depth -= 1
|
|
1323
|
+
return index if depth.zero?
|
|
1324
|
+
elsif char == '\\'
|
|
1325
|
+
index += 1
|
|
1326
|
+
end
|
|
1327
|
+
index += 1
|
|
1328
|
+
end
|
|
1329
|
+
nil
|
|
1330
|
+
end
|
|
1331
|
+
|
|
1332
|
+
def find_matching_backtick_run(text, index, length)
|
|
1333
|
+
while index < text.length
|
|
1334
|
+
if text[index, 1] == '`'
|
|
1335
|
+
run_length = 1
|
|
1336
|
+
run_length += 1 while index + run_length < text.length && text[index + run_length, 1] == '`'
|
|
1337
|
+
return index if run_length == length
|
|
1338
|
+
|
|
1339
|
+
index += run_length
|
|
1340
|
+
next
|
|
1341
|
+
end
|
|
1342
|
+
index += 1
|
|
1343
|
+
end
|
|
1344
|
+
|
|
1345
|
+
nil
|
|
1346
|
+
end
|
|
1347
|
+
|
|
1348
|
+
def parse_inline_destination(text, index, placeholders = nil)
|
|
1349
|
+
while index < text.length && text[index, 1] =~ /[ \t\n]/
|
|
1350
|
+
index += 1
|
|
1351
|
+
end
|
|
1352
|
+
|
|
1353
|
+
if text[index, 1] == '<'
|
|
1354
|
+
close = text.index('>', index + 1)
|
|
1355
|
+
return [nil, nil, nil] unless close
|
|
1356
|
+
dest = text[(index + 1)...close]
|
|
1357
|
+
return [nil, nil, nil] if dest.include?("\n") || dest.include?('\\')
|
|
1358
|
+
dest = dest.gsub(' ', '%20')
|
|
1359
|
+
index = close + 1
|
|
1360
|
+
else
|
|
1361
|
+
close = index
|
|
1362
|
+
parens = 0
|
|
1363
|
+
while close < text.length
|
|
1364
|
+
char = text[close, 1]
|
|
1365
|
+
if char == '\\' && close + 1 < text.length
|
|
1366
|
+
close += 2
|
|
1367
|
+
next
|
|
1368
|
+
end
|
|
1369
|
+
break if parens.zero? && (char == ')' || char =~ /\s/)
|
|
1370
|
+
parens += 1 if char == '('
|
|
1371
|
+
parens -= 1 if char == ')'
|
|
1372
|
+
close += 1
|
|
1373
|
+
end
|
|
1374
|
+
dest = text[index...close]
|
|
1375
|
+
index = close
|
|
1376
|
+
end
|
|
1377
|
+
|
|
1378
|
+
if placeholders
|
|
1379
|
+
restored_dest = restore_placeholders(dest.to_s, placeholders)
|
|
1380
|
+
if restored_dest.start_with?('<')
|
|
1381
|
+
return [nil, nil, nil] if restored_dest.include?("\n") || restored_dest.include?('\\')
|
|
1382
|
+
return [nil, nil, nil] unless restored_dest.end_with?('>') && restored_dest.index('>') == restored_dest.length - 1
|
|
1383
|
+
|
|
1384
|
+
dest = restored_dest[1...-1]
|
|
1385
|
+
end
|
|
1386
|
+
end
|
|
1387
|
+
|
|
1388
|
+
while index < text.length && text[index, 1] =~ /[ \t\n]/
|
|
1389
|
+
index += 1
|
|
1390
|
+
end
|
|
1391
|
+
|
|
1392
|
+
title = nil
|
|
1393
|
+
if text[index, 1] == '"' || text[index, 1] == "'"
|
|
1394
|
+
delimiter = text[index, 1]
|
|
1395
|
+
index += 1
|
|
1396
|
+
buffer = String.new
|
|
1397
|
+
while index < text.length
|
|
1398
|
+
char = text[index, 1]
|
|
1399
|
+
if char == '\\' && index + 1 < text.length
|
|
1400
|
+
buffer << text[index, 2]
|
|
1401
|
+
index += 2
|
|
1402
|
+
next
|
|
1403
|
+
end
|
|
1404
|
+
break if char == delimiter
|
|
1405
|
+
buffer << char
|
|
1406
|
+
index += 1
|
|
1407
|
+
end
|
|
1408
|
+
return [nil, nil, nil] unless index < text.length && text[index, 1] == delimiter
|
|
1409
|
+
title = buffer
|
|
1410
|
+
index += 1
|
|
1411
|
+
elsif text[index, 1] == '('
|
|
1412
|
+
index += 1
|
|
1413
|
+
buffer = String.new
|
|
1414
|
+
depth = 1
|
|
1415
|
+
while index < text.length
|
|
1416
|
+
char = text[index, 1]
|
|
1417
|
+
if char == '\\' && index + 1 < text.length
|
|
1418
|
+
buffer << text[index, 2]
|
|
1419
|
+
index += 2
|
|
1420
|
+
next
|
|
1421
|
+
end
|
|
1422
|
+
if char == '('
|
|
1423
|
+
depth += 1
|
|
1424
|
+
elsif char == ')'
|
|
1425
|
+
depth -= 1
|
|
1426
|
+
break if depth.zero?
|
|
1427
|
+
end
|
|
1428
|
+
buffer << char
|
|
1429
|
+
index += 1
|
|
1430
|
+
end
|
|
1431
|
+
return [nil, nil, nil] unless index < text.length && text[index, 1] == ')'
|
|
1432
|
+
title = buffer
|
|
1433
|
+
index += 1
|
|
1434
|
+
end
|
|
1435
|
+
|
|
1436
|
+
while index < text.length && text[index, 1] =~ /[ \t\n]/
|
|
1437
|
+
index += 1
|
|
1438
|
+
end
|
|
1439
|
+
return [nil, nil, nil] unless text[index, 1] == ')'
|
|
1440
|
+
|
|
1441
|
+
[dest.to_s, title, index + 1]
|
|
1442
|
+
end
|
|
1443
|
+
|
|
1444
|
+
def plain_text(text)
|
|
1445
|
+
text = text.to_s.gsub(/!\[([^\]]*)\]\([^)]+\)/, '\1')
|
|
1446
|
+
text = text.gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')
|
|
1447
|
+
text = text.gsub(/[*_~`]/, '')
|
|
1448
|
+
decode_entities(unescape_markdown_punctuation(text))
|
|
1449
|
+
end
|
|
1450
|
+
|
|
1451
|
+
def parse_text_link_destination(text, index)
|
|
1452
|
+
return [nil, 0] unless text[index, 1] == '['
|
|
1453
|
+
|
|
1454
|
+
dest = String.new
|
|
1455
|
+
cursor = index + 1
|
|
1456
|
+
|
|
1457
|
+
while cursor < text.length
|
|
1458
|
+
char = text[cursor, 1]
|
|
1459
|
+
|
|
1460
|
+
if char == '\\'
|
|
1461
|
+
escaped = text[cursor + 1, 1]
|
|
1462
|
+
return [nil, 0] unless escaped && "[]\\*+<_".include?(escaped)
|
|
1463
|
+
|
|
1464
|
+
dest << escaped
|
|
1465
|
+
cursor += 2
|
|
1466
|
+
next
|
|
1467
|
+
end
|
|
1468
|
+
|
|
1469
|
+
return [nil, 0] if char =~ /\s/
|
|
1470
|
+
return [dest, cursor - index + 1] if char == ']'
|
|
1471
|
+
return [nil, 0] if char == '['
|
|
1472
|
+
|
|
1473
|
+
dest << char
|
|
1474
|
+
cursor += 1
|
|
1475
|
+
end
|
|
1476
|
+
|
|
1477
|
+
[nil, 0]
|
|
1478
|
+
end
|
|
1479
|
+
|
|
1480
|
+
def find_braced_text_link_label_end(text, index)
|
|
1481
|
+
cursor = index + 1
|
|
1482
|
+
|
|
1483
|
+
while cursor < text.length
|
|
1484
|
+
char = text[cursor, 1]
|
|
1485
|
+
|
|
1486
|
+
if char == '\\'
|
|
1487
|
+
cursor += 2
|
|
1488
|
+
next
|
|
1489
|
+
end
|
|
1490
|
+
|
|
1491
|
+
return cursor if char == '}'
|
|
1492
|
+
cursor += 1
|
|
1493
|
+
end
|
|
1494
|
+
|
|
1495
|
+
nil
|
|
1496
|
+
end
|
|
1497
|
+
|
|
1498
|
+
def link_html(label, dest, title = nil)
|
|
1499
|
+
href = escape_url(unescape_markdown_punctuation(decode_entities(dest.to_s)))
|
|
1500
|
+
normalized_title = title && unescape_markdown_punctuation(decode_entities(title))
|
|
1501
|
+
attrs = %( href="#{h(href)}")
|
|
1502
|
+
attrs += %( title="#{h(normalized_title)}") if normalized_title
|
|
1503
|
+
%(<a#{attrs}>#{format_inline(label)}</a>)
|
|
1504
|
+
end
|
|
1505
|
+
|
|
1506
|
+
def image_html(label, dest, title = nil)
|
|
1507
|
+
src = escape_url(unescape_markdown_punctuation(decode_entities(dest.to_s)))
|
|
1508
|
+
normalized_title = title && unescape_markdown_punctuation(decode_entities(title))
|
|
1509
|
+
attrs = %( src="#{h(src)}" alt="#{h(plain_text(label))}")
|
|
1510
|
+
attrs += %( title="#{h(normalized_title)}") if normalized_title
|
|
1511
|
+
"<img#{attrs} />"
|
|
1512
|
+
end
|
|
1513
|
+
|
|
1514
|
+
def decode_entities(text)
|
|
1515
|
+
text.gsub(ENTITY_RE) do |entity|
|
|
1516
|
+
decode_entity(entity)
|
|
1517
|
+
end
|
|
1518
|
+
end
|
|
1519
|
+
|
|
1520
|
+
def reference_definition_continuation?(line)
|
|
1521
|
+
return true if line =~ /^(?: {1,3}|\t)(.*)$/
|
|
1522
|
+
return true if line =~ /\A<(?:[^>\n]*)>\s*\z/
|
|
1523
|
+
return true if line =~ /\A(?:"[^"]*"|'[^']*'|\([^)]*\))\s*\z/
|
|
1524
|
+
|
|
1525
|
+
false
|
|
1526
|
+
end
|
|
1527
|
+
|
|
1528
|
+
def normalize_code_span(code)
|
|
1529
|
+
code = code.gsub(/\n/, ' ')
|
|
1530
|
+
if code.length > 1 && code.start_with?(' ') && code.end_with?(' ') && code.strip != ''
|
|
1531
|
+
code[1...-1]
|
|
1532
|
+
else
|
|
1533
|
+
code
|
|
1534
|
+
end
|
|
1535
|
+
end
|
|
1536
|
+
|
|
1537
|
+
def available_delimiter_length(token)
|
|
1538
|
+
token[:length] - token[:left_consumed] - token[:right_consumed]
|
|
1539
|
+
end
|
|
1540
|
+
|
|
1541
|
+
def odd_match_disallowed?(opener, closer)
|
|
1542
|
+
return false unless opener[:can_close] || closer[:can_open]
|
|
1543
|
+
|
|
1544
|
+
opener_len = available_delimiter_length(opener)
|
|
1545
|
+
closer_len = available_delimiter_length(closer)
|
|
1546
|
+
((opener_len + closer_len) % 3).zero? &&
|
|
1547
|
+
(opener_len % 3 != 0 || closer_len % 3 != 0)
|
|
1548
|
+
end
|
|
1549
|
+
|
|
1550
|
+
def delimiter_flags(text, run_start, run_end, char)
|
|
1551
|
+
before = run_start.zero? ? nil : text[run_start - 1, 1]
|
|
1552
|
+
after = run_end >= text.length ? nil : text[run_end, 1]
|
|
1553
|
+
before_whitespace = whitespace_char?(before)
|
|
1554
|
+
after_whitespace = whitespace_char?(after)
|
|
1555
|
+
before_punctuation = punctuation_char?(before)
|
|
1556
|
+
after_punctuation = punctuation_char?(after)
|
|
1557
|
+
|
|
1558
|
+
left_flanking = !after_whitespace && (!after_punctuation || before_whitespace || before_punctuation)
|
|
1559
|
+
right_flanking = !before_whitespace && (!before_punctuation || after_whitespace || after_punctuation)
|
|
1560
|
+
|
|
1561
|
+
if char == '_'
|
|
1562
|
+
[
|
|
1563
|
+
left_flanking && (!right_flanking || before_punctuation),
|
|
1564
|
+
right_flanking && (!left_flanking || after_punctuation)
|
|
1565
|
+
]
|
|
1566
|
+
else
|
|
1567
|
+
[left_flanking, right_flanking]
|
|
1568
|
+
end
|
|
1569
|
+
end
|
|
1570
|
+
|
|
1571
|
+
def whitespace_char?(char)
|
|
1572
|
+
char.nil? || char =~ /\s/ || char == NAMED_ENTITIES['nbsp']
|
|
1573
|
+
end
|
|
1574
|
+
|
|
1575
|
+
def punctuation_char?(char)
|
|
1576
|
+
return false if char.nil?
|
|
1577
|
+
|
|
1578
|
+
ascii_punctuation_char?(char) || unicode_symbol_char?(char)
|
|
1579
|
+
end
|
|
1580
|
+
|
|
1581
|
+
def unicode_symbol_char?(char)
|
|
1582
|
+
codepoint = char.to_s.unpack('U*').first
|
|
1583
|
+
return false unless codepoint
|
|
1584
|
+
|
|
1585
|
+
(0x00A2..0x00A9).include?(codepoint) ||
|
|
1586
|
+
(0x00AC..0x00AE).include?(codepoint) ||
|
|
1587
|
+
(0x00B0..0x00B4).include?(codepoint) ||
|
|
1588
|
+
codepoint == 0x00B6 ||
|
|
1589
|
+
codepoint == 0x00B7 ||
|
|
1590
|
+
codepoint == 0x00D7 ||
|
|
1591
|
+
codepoint == 0x00F7 ||
|
|
1592
|
+
(0x20A0..0x20CF).include?(codepoint)
|
|
1593
|
+
end
|
|
1594
|
+
|
|
1595
|
+
def ascii_punctuation_char?(char)
|
|
1596
|
+
return false unless ascii_only_compat?(char)
|
|
1597
|
+
|
|
1598
|
+
byte = char.to_s.unpack('C').first
|
|
1599
|
+
return false unless byte
|
|
1600
|
+
|
|
1601
|
+
(0x21..0x2F).include?(byte) ||
|
|
1602
|
+
(0x3A..0x40).include?(byte) ||
|
|
1603
|
+
(0x5B..0x60).include?(byte) ||
|
|
1604
|
+
(0x7B..0x7E).include?(byte)
|
|
1605
|
+
end
|
|
1606
|
+
|
|
1607
|
+
def leading_columns(line)
|
|
1608
|
+
scan_leading_columns(line.to_s).first
|
|
1609
|
+
end
|
|
1610
|
+
|
|
1611
|
+
def indented_to?(line, indent)
|
|
1612
|
+
leading_columns(line) >= indent
|
|
1613
|
+
end
|
|
1614
|
+
|
|
1615
|
+
def strip_list_item_indent(line, content_indent)
|
|
1616
|
+
consume_columns(line, content_indent, 0, true)
|
|
1617
|
+
end
|
|
1618
|
+
|
|
1619
|
+
def escape_list_marker_text(line)
|
|
1620
|
+
source = line.to_s.sub(/\n\z/, '')
|
|
1621
|
+
newline = source.length == line.to_s.length ? '' : "\n"
|
|
1622
|
+
|
|
1623
|
+
if source =~ /\A([*+-])([ \t].*)\z/
|
|
1624
|
+
"\\#{$1}#{$2}#{newline}"
|
|
1625
|
+
elsif source =~ /\A(\d{1,9}[.)])([ \t].*)\z/
|
|
1626
|
+
"\\#{$1}#{$2}#{newline}"
|
|
1627
|
+
elsif source =~ /\A([A-Za-z]\.)([ \t].*)\z/
|
|
1628
|
+
"\\#{$1}#{$2}#{newline}"
|
|
1629
|
+
else
|
|
1630
|
+
source + newline
|
|
1631
|
+
end
|
|
1632
|
+
end
|
|
1633
|
+
|
|
1634
|
+
def escape_url(url)
|
|
1635
|
+
percent_encode_url(url.to_s, /[A-Za-z0-9\-._~:\/?#\[\]@!$&'()*+,;=%]/)
|
|
1636
|
+
end
|
|
1637
|
+
|
|
1638
|
+
def escape_autolink_url(url)
|
|
1639
|
+
percent_encode_url(url.to_s, /[A-Za-z0-9\-._~:\/?#@!$&'()*+,;=%]/)
|
|
1640
|
+
end
|
|
1641
|
+
|
|
1642
|
+
def parse_reference_definition_block(lines, index, previous_line)
|
|
1643
|
+
line = lines[index]
|
|
1644
|
+
return nil unless reference_definition_context?(previous_line)
|
|
1645
|
+
|
|
1646
|
+
prefix, content = split_reference_container_prefix(line)
|
|
1647
|
+
return nil unless content =~ /^\s{0,3}\[/
|
|
1648
|
+
|
|
1649
|
+
label_buffer = content.sub(/^\s{0,3}/, '')
|
|
1650
|
+
consumed_lines = [line]
|
|
1651
|
+
label_end = find_reference_label_end(label_buffer)
|
|
1652
|
+
current_index = index
|
|
1653
|
+
|
|
1654
|
+
while label_end.nil?
|
|
1655
|
+
current_index += 1
|
|
1656
|
+
return nil if current_index >= lines.length
|
|
1657
|
+
|
|
1658
|
+
next_prefix, next_content = split_reference_container_prefix(lines[current_index])
|
|
1659
|
+
return nil unless next_prefix == prefix
|
|
1660
|
+
|
|
1661
|
+
label_buffer << next_content
|
|
1662
|
+
consumed_lines << lines[current_index]
|
|
1663
|
+
label_end = find_reference_label_end(label_buffer)
|
|
1664
|
+
end
|
|
1665
|
+
|
|
1666
|
+
label = label_buffer[1...label_end]
|
|
1667
|
+
remainder = label_buffer[(label_end + 2)..-1].to_s
|
|
1668
|
+
current_index += 1
|
|
1669
|
+
|
|
1670
|
+
while remainder.strip.empty? && current_index < lines.length
|
|
1671
|
+
next_prefix, next_content = split_reference_container_prefix(lines[current_index])
|
|
1672
|
+
break unless next_prefix == prefix
|
|
1673
|
+
break if blank_line?(next_content)
|
|
1674
|
+
|
|
1675
|
+
remainder << (remainder.empty? ? next_content : "\n#{next_content}")
|
|
1676
|
+
consumed_lines << lines[current_index]
|
|
1677
|
+
current_index += 1
|
|
1678
|
+
end
|
|
1679
|
+
|
|
1680
|
+
while unclosed_reference_title?(remainder) && current_index < lines.length
|
|
1681
|
+
next_prefix, next_content = split_reference_container_prefix(lines[current_index])
|
|
1682
|
+
break unless next_prefix == prefix
|
|
1683
|
+
break if blank_line?(next_content)
|
|
1684
|
+
|
|
1685
|
+
remainder << "\n#{next_content}"
|
|
1686
|
+
consumed_lines << lines[current_index]
|
|
1687
|
+
current_index += 1
|
|
1688
|
+
end
|
|
1689
|
+
|
|
1690
|
+
while current_index < lines.length
|
|
1691
|
+
next_prefix, next_content = split_reference_container_prefix(lines[current_index])
|
|
1692
|
+
break unless next_prefix == prefix
|
|
1693
|
+
break unless reference_definition_continuation?(next_content)
|
|
1694
|
+
|
|
1695
|
+
remainder << "\n#{next_content.strip}"
|
|
1696
|
+
consumed_lines << lines[current_index]
|
|
1697
|
+
current_index += 1
|
|
1698
|
+
end
|
|
1699
|
+
|
|
1700
|
+
reference = parse_reference_definition(label, remainder)
|
|
1701
|
+
return nil unless reference
|
|
1702
|
+
|
|
1703
|
+
{
|
|
1704
|
+
:label => label,
|
|
1705
|
+
:reference => reference,
|
|
1706
|
+
:replacement_lines => consumed_lines.map { |consumed| reference_definition_replacement_line(consumed, prefix) },
|
|
1707
|
+
:next_index => current_index
|
|
1708
|
+
}
|
|
1709
|
+
end
|
|
1710
|
+
|
|
1711
|
+
def reference_definition_context?(previous_line)
|
|
1712
|
+
return true if previous_line.nil?
|
|
1713
|
+
return true if blank_line?(previous_line)
|
|
1714
|
+
|
|
1715
|
+
stripped = split_reference_container_prefix(previous_line).last
|
|
1716
|
+
block_boundary?(stripped)
|
|
1717
|
+
end
|
|
1718
|
+
|
|
1719
|
+
def split_reference_container_prefix(line)
|
|
1720
|
+
prefix = String.new
|
|
1721
|
+
content = line.chomp
|
|
1722
|
+
|
|
1723
|
+
while (split = split_blockquote_prefix(content))
|
|
1724
|
+
prefix << split[0]
|
|
1725
|
+
content = split[1].chomp
|
|
1726
|
+
end
|
|
1727
|
+
|
|
1728
|
+
[prefix, content]
|
|
1729
|
+
end
|
|
1730
|
+
|
|
1731
|
+
def reference_definition_replacement_line(line, prefix)
|
|
1732
|
+
return '' if prefix.empty?
|
|
1733
|
+
|
|
1734
|
+
prefix.rstrip + "\n"
|
|
1735
|
+
end
|
|
1736
|
+
|
|
1737
|
+
def find_reference_label_end(text)
|
|
1738
|
+
return nil unless text.start_with?('[')
|
|
1739
|
+
|
|
1740
|
+
index = 1
|
|
1741
|
+
while index < text.length
|
|
1742
|
+
char = text[index, 1]
|
|
1743
|
+
if char == '\\'
|
|
1744
|
+
index += 2
|
|
1745
|
+
next
|
|
1746
|
+
end
|
|
1747
|
+
return nil if char == '['
|
|
1748
|
+
return index if char == ']' && text[index + 1, 1] == ':'
|
|
1749
|
+
|
|
1750
|
+
index += 1
|
|
1751
|
+
end
|
|
1752
|
+
|
|
1753
|
+
nil
|
|
1754
|
+
end
|
|
1755
|
+
|
|
1756
|
+
def contains_nested_link?(label, placeholders)
|
|
1757
|
+
text = restore_placeholders(label.to_s, placeholders)
|
|
1758
|
+
return true if text.include?('<a ')
|
|
1759
|
+
|
|
1760
|
+
index = 0
|
|
1761
|
+
|
|
1762
|
+
while index < text.length
|
|
1763
|
+
if text[index, 2] == '![' && (index.zero? || text[index - 1, 1] != '\\')
|
|
1764
|
+
label_open = index + 1
|
|
1765
|
+
elsif text[index, 1] == '[' && (index.zero? || text[index - 1, 1] != '\\')
|
|
1766
|
+
label_open = index
|
|
1767
|
+
else
|
|
1768
|
+
index += 1
|
|
1769
|
+
next
|
|
1770
|
+
end
|
|
1771
|
+
|
|
1772
|
+
label_close = find_closing_bracket(text, label_open)
|
|
1773
|
+
if label_close
|
|
1774
|
+
next_char = text[label_close + 1, 1]
|
|
1775
|
+
return true if next_char == '(' || next_char == '['
|
|
1776
|
+
end
|
|
1777
|
+
|
|
1778
|
+
index += 1
|
|
1779
|
+
end
|
|
1780
|
+
|
|
1781
|
+
false
|
|
1782
|
+
end
|
|
1783
|
+
|
|
1784
|
+
def unclosed_reference_title?(text)
|
|
1785
|
+
stripped = text.to_s.rstrip
|
|
1786
|
+
return false if stripped.empty?
|
|
1787
|
+
|
|
1788
|
+
single_quotes = stripped.count("'")
|
|
1789
|
+
double_quotes = stripped.count('"')
|
|
1790
|
+
open_parens = stripped.count('(')
|
|
1791
|
+
close_parens = stripped.count(')')
|
|
1792
|
+
|
|
1793
|
+
single_quotes.odd? || double_quotes.odd? || open_parens > close_parens
|
|
1794
|
+
end
|
|
1795
|
+
|
|
1796
|
+
def percent_encode_url(text, allowed_re)
|
|
1797
|
+
encoded = String.new
|
|
1798
|
+
|
|
1799
|
+
each_char_compat(text.to_s) do |char|
|
|
1800
|
+
if ascii_only_compat?(char) && char =~ /\A#{allowed_re.source}\z/
|
|
1801
|
+
encoded << char
|
|
1802
|
+
else
|
|
1803
|
+
utf8_bytes(char).each do |byte|
|
|
1804
|
+
encoded << sprintf('%%%02X', byte)
|
|
1805
|
+
end
|
|
1806
|
+
end
|
|
1807
|
+
end
|
|
1808
|
+
|
|
1809
|
+
encoded
|
|
1810
|
+
end
|
|
1811
|
+
|
|
1812
|
+
def html_block_type(line, interrupt_paragraph = false)
|
|
1813
|
+
stripped = line.chomp
|
|
1814
|
+
return nil unless stripped =~ /^\s{0,3}</ || stripped =~ /^\s{0,3}<(?!!--)/
|
|
1815
|
+
|
|
1816
|
+
return 1 if stripped =~ /^\s{0,3}<(?:script|pre|style|textarea)(?:\s|>|$)/i
|
|
1817
|
+
return 2 if stripped =~ /^\s{0,3}<!--/
|
|
1818
|
+
return 3 if stripped =~ /^\s{0,3}<\?/
|
|
1819
|
+
return 4 if stripped =~ /^\s{0,3}<![A-Z]/
|
|
1820
|
+
return 5 if stripped =~ /^\s{0,3}<!\[CDATA\[/
|
|
1821
|
+
return 6 if stripped =~ /^\s{0,3}<\/?(?:#{HTML_BLOCK_TAGS.join('|')})(?:\s|\/?>|$)/i
|
|
1822
|
+
return nil if interrupt_paragraph
|
|
1823
|
+
|
|
1824
|
+
return 7 if stripped =~ /^\s{0,3}(?:<[A-Za-z][A-Za-z0-9-]*(?:\s+[A-Za-z_:][\w:.-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*\s*\/?>|<\/[A-Za-z][A-Za-z0-9-]*\s*>)\s*$/
|
|
1825
|
+
|
|
1826
|
+
nil
|
|
1827
|
+
end
|
|
1828
|
+
|
|
1829
|
+
def html_block_end?(type, line)
|
|
1830
|
+
case type
|
|
1831
|
+
when 1
|
|
1832
|
+
line =~ %r{</(?:script|pre|style|textarea)\s*>}i
|
|
1833
|
+
when 2
|
|
1834
|
+
line.include?('-->')
|
|
1835
|
+
when 3
|
|
1836
|
+
line.include?('?>')
|
|
1837
|
+
when 4
|
|
1838
|
+
line.include?('>')
|
|
1839
|
+
when 5
|
|
1840
|
+
line.include?(']]>')
|
|
1841
|
+
when 6, 7
|
|
1842
|
+
blank_line?(line)
|
|
1843
|
+
else
|
|
1844
|
+
false
|
|
1845
|
+
end
|
|
1846
|
+
end
|
|
1847
|
+
|
|
1848
|
+
def decode_entity(entity)
|
|
1849
|
+
case entity
|
|
1850
|
+
when /\A&#(\d+);\z/
|
|
1851
|
+
codepoint = $1.to_i
|
|
1852
|
+
when /\A&#[xX]([0-9A-Fa-f]+);\z/
|
|
1853
|
+
codepoint = $1.to_i(16)
|
|
1854
|
+
else
|
|
1855
|
+
name = entity[1..-2]
|
|
1856
|
+
return [0x00E4].pack('U') if name == 'auml'
|
|
1857
|
+
return NAMED_ENTITIES[name] || CGI.unescapeHTML(entity)
|
|
1858
|
+
end
|
|
1859
|
+
|
|
1860
|
+
return [0xFFFD].pack('U') if codepoint.zero?
|
|
1861
|
+
return entity if codepoint > 0x10FFFF
|
|
1862
|
+
[codepoint].pack('U')
|
|
1863
|
+
rescue RangeError
|
|
1864
|
+
entity
|
|
1865
|
+
end
|
|
1866
|
+
|
|
1867
|
+
def h(text)
|
|
1868
|
+
text.to_s.gsub('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"')
|
|
1869
|
+
end
|
|
1870
|
+
|
|
1871
|
+
def unescape_markdown_punctuation(text)
|
|
1872
|
+
text.to_s.gsub(ESCAPABLE_CHARS_RE, '\1')
|
|
1873
|
+
end
|
|
1874
|
+
|
|
1875
|
+
def split_lines(text)
|
|
1876
|
+
text.to_s.split(/^/, -1)
|
|
1877
|
+
end
|
|
1878
|
+
|
|
1879
|
+
def scan_leading_columns(text)
|
|
1880
|
+
index = 0
|
|
1881
|
+
column = 0
|
|
1882
|
+
source = text.to_s
|
|
1883
|
+
|
|
1884
|
+
while index < source.length
|
|
1885
|
+
char = source[index, 1]
|
|
1886
|
+
if char == ' '
|
|
1887
|
+
column += 1
|
|
1888
|
+
elsif char == "\t"
|
|
1889
|
+
column += TAB_WIDTH - (column % TAB_WIDTH)
|
|
1890
|
+
else
|
|
1891
|
+
break
|
|
1892
|
+
end
|
|
1893
|
+
index += 1
|
|
1894
|
+
end
|
|
1895
|
+
|
|
1896
|
+
[column, index]
|
|
1897
|
+
end
|
|
1898
|
+
|
|
1899
|
+
def scan_padding_columns(text, index, start_column)
|
|
1900
|
+
column = start_column
|
|
1901
|
+
padding = 0
|
|
1902
|
+
source = text.to_s
|
|
1903
|
+
|
|
1904
|
+
while index < source.length
|
|
1905
|
+
char = source[index, 1]
|
|
1906
|
+
if char == ' '
|
|
1907
|
+
column += 1
|
|
1908
|
+
padding += 1
|
|
1909
|
+
elsif char == "\t"
|
|
1910
|
+
advance = TAB_WIDTH - (column % TAB_WIDTH)
|
|
1911
|
+
column += advance
|
|
1912
|
+
padding += advance
|
|
1913
|
+
else
|
|
1914
|
+
break
|
|
1915
|
+
end
|
|
1916
|
+
index += 1
|
|
1917
|
+
end
|
|
1918
|
+
|
|
1919
|
+
[padding, index]
|
|
1920
|
+
end
|
|
1921
|
+
|
|
1922
|
+
def consume_columns(text, columns, start_column = 0, normalize_remaining = false)
|
|
1923
|
+
index = 0
|
|
1924
|
+
column = start_column
|
|
1925
|
+
remaining = columns
|
|
1926
|
+
prefix_width = 0
|
|
1927
|
+
source = text.to_s
|
|
1928
|
+
|
|
1929
|
+
while index < source.length && remaining > 0
|
|
1930
|
+
char = source[index, 1]
|
|
1931
|
+
if char == ' '
|
|
1932
|
+
column += 1
|
|
1933
|
+
remaining -= 1
|
|
1934
|
+
index += 1
|
|
1935
|
+
elsif char == "\t"
|
|
1936
|
+
advance = TAB_WIDTH - (column % TAB_WIDTH)
|
|
1937
|
+
if advance <= remaining
|
|
1938
|
+
column += advance
|
|
1939
|
+
remaining -= advance
|
|
1940
|
+
index += 1
|
|
1941
|
+
else
|
|
1942
|
+
prefix_width += advance - remaining if normalize_remaining
|
|
1943
|
+
column += advance
|
|
1944
|
+
remaining = 0
|
|
1945
|
+
index += 1
|
|
1946
|
+
end
|
|
1947
|
+
else
|
|
1948
|
+
break
|
|
1949
|
+
end
|
|
1950
|
+
end
|
|
1951
|
+
|
|
1952
|
+
if normalize_remaining
|
|
1953
|
+
while index < source.length
|
|
1954
|
+
char = source[index, 1]
|
|
1955
|
+
if char == ' '
|
|
1956
|
+
prefix_width += 1
|
|
1957
|
+
column += 1
|
|
1958
|
+
index += 1
|
|
1959
|
+
elsif char == "\t"
|
|
1960
|
+
advance = TAB_WIDTH - (column % TAB_WIDTH)
|
|
1961
|
+
prefix_width += advance
|
|
1962
|
+
column += advance
|
|
1963
|
+
index += 1
|
|
1964
|
+
else
|
|
1965
|
+
break
|
|
1966
|
+
end
|
|
1967
|
+
end
|
|
1968
|
+
|
|
1969
|
+
(' ' * prefix_width) + source[index..-1].to_s
|
|
1970
|
+
else
|
|
1971
|
+
source[index..-1].to_s
|
|
1972
|
+
end
|
|
1973
|
+
end
|
|
1974
|
+
|
|
1975
|
+
def lazy_blockquote_continuation?(quoted_lines, line)
|
|
1976
|
+
return false if block_boundary?(line)
|
|
1977
|
+
return false if indented_code_start?(line) && !blockquote_paragraph_context?(quoted_lines)
|
|
1978
|
+
|
|
1979
|
+
last_content = quoted_lines.reverse.find { |quoted| !blank_line?(quoted) }
|
|
1980
|
+
return false if last_content && fenced_code_start?(last_content)
|
|
1981
|
+
return false if last_content && indented_code_start?(last_content)
|
|
1982
|
+
|
|
1983
|
+
true
|
|
1984
|
+
end
|
|
1985
|
+
|
|
1986
|
+
def blockquote_open_fence?(quoted_lines)
|
|
1987
|
+
opener = nil
|
|
1988
|
+
|
|
1989
|
+
quoted_lines.each do |quoted|
|
|
1990
|
+
next if blank_line?(quoted)
|
|
1991
|
+
|
|
1992
|
+
if opener
|
|
1993
|
+
opener = nil if fence_closer?(quoted, opener[:char], opener[:length])
|
|
1994
|
+
else
|
|
1995
|
+
opener = parse_fence_opener(quoted)
|
|
1996
|
+
end
|
|
1997
|
+
end
|
|
1998
|
+
|
|
1999
|
+
!opener.nil?
|
|
2000
|
+
end
|
|
2001
|
+
|
|
2002
|
+
def blockquote_paragraph_context?(quoted_lines)
|
|
2003
|
+
last_content = quoted_lines.reverse.find { |quoted| !blank_line?(quoted) }
|
|
2004
|
+
return false unless last_content
|
|
2005
|
+
return false if fenced_code_start?(last_content)
|
|
2006
|
+
return false if parse_heading(last_content)
|
|
2007
|
+
return false if thematic_break?(last_content)
|
|
2008
|
+
|
|
2009
|
+
true
|
|
2010
|
+
end
|
|
2011
|
+
|
|
2012
|
+
def normalize_paragraph_line(line)
|
|
2013
|
+
line.to_s.chomp.sub(/^\s+/, '')
|
|
2014
|
+
end
|
|
2015
|
+
|
|
2016
|
+
def normalize_heading_line(line)
|
|
2017
|
+
normalize_paragraph_line(line).rstrip
|
|
2018
|
+
end
|
|
2019
|
+
|
|
2020
|
+
def split_blockquote_prefix(line)
|
|
2021
|
+
source = line.to_s
|
|
2022
|
+
indent, index = scan_leading_columns(source)
|
|
2023
|
+
return nil if indent > 3
|
|
2024
|
+
return nil unless source[index, 1] == '>'
|
|
2025
|
+
|
|
2026
|
+
prefix = source[0..index]
|
|
2027
|
+
rest = source[(index + 1)..-1].to_s
|
|
2028
|
+
if rest.start_with?(' ') || rest.start_with?("\t")
|
|
2029
|
+
prefix << rest[0, 1]
|
|
2030
|
+
rest = consume_columns(rest, 1, indent + 1, true)
|
|
2031
|
+
end
|
|
2032
|
+
|
|
2033
|
+
[prefix, rest.end_with?("\n") ? rest : "#{rest}\n"]
|
|
2034
|
+
end
|
|
2035
|
+
|
|
2036
|
+
def strip_blockquote_marker(line)
|
|
2037
|
+
split = split_blockquote_prefix(line)
|
|
2038
|
+
split && split[1]
|
|
2039
|
+
end
|
|
2040
|
+
|
|
2041
|
+
def loose_list_item_continuation?(item_lines)
|
|
2042
|
+
return false if open_fence_in_lines?(item_lines)
|
|
2043
|
+
|
|
2044
|
+
previous = item_lines.reverse.find { |item_line| item_line != "\n" }
|
|
2045
|
+
return true unless previous
|
|
2046
|
+
|
|
2047
|
+
!parse_list_marker(previous.chomp)
|
|
2048
|
+
end
|
|
2049
|
+
|
|
2050
|
+
def open_fence_in_lines?(lines)
|
|
2051
|
+
opener = nil
|
|
2052
|
+
|
|
2053
|
+
lines.each do |line|
|
|
2054
|
+
next if blank_line?(line)
|
|
2055
|
+
|
|
2056
|
+
if opener
|
|
2057
|
+
opener = nil if fence_closer?(line, opener[:char], opener[:length])
|
|
2058
|
+
else
|
|
2059
|
+
opener = parse_fence_opener(line)
|
|
2060
|
+
end
|
|
2061
|
+
end
|
|
2062
|
+
|
|
2063
|
+
!opener.nil?
|
|
2064
|
+
end
|
|
2065
|
+
|
|
2066
|
+
def each_char_compat(text)
|
|
2067
|
+
if text.respond_to?(:each_char)
|
|
2068
|
+
text.each_char { |char| yield char }
|
|
2069
|
+
else
|
|
2070
|
+
text.scan(/./m) { |char| yield char }
|
|
2071
|
+
end
|
|
2072
|
+
end
|
|
2073
|
+
|
|
2074
|
+
def ascii_only_compat?(text)
|
|
2075
|
+
if text.respond_to?(:ascii_only?)
|
|
2076
|
+
text.ascii_only?
|
|
2077
|
+
else
|
|
2078
|
+
text.to_s.unpack('C*').all? { |byte| byte < 128 }
|
|
2079
|
+
end
|
|
2080
|
+
end
|
|
2081
|
+
|
|
2082
|
+
def utf8_bytes(char)
|
|
2083
|
+
if defined?(Encoding)
|
|
2084
|
+
char.encode(Encoding::UTF_8).unpack('C*')
|
|
2085
|
+
else
|
|
2086
|
+
[char[0]].pack('U').unpack('C*')
|
|
2087
|
+
end
|
|
2088
|
+
end
|
|
2089
|
+
|
|
2090
|
+
def unicode_casefold_compat(text)
|
|
2091
|
+
codepoints = text.to_s.unpack('U*')
|
|
2092
|
+
folded = String.new
|
|
2093
|
+
|
|
2094
|
+
codepoints.each do |codepoint|
|
|
2095
|
+
append_folded_codepoint(folded, codepoint)
|
|
2096
|
+
end
|
|
2097
|
+
|
|
2098
|
+
folded
|
|
2099
|
+
end
|
|
2100
|
+
|
|
2101
|
+
def append_folded_codepoint(buffer, codepoint)
|
|
2102
|
+
case codepoint
|
|
2103
|
+
when 0x41..0x5A
|
|
2104
|
+
buffer << [codepoint + 32].pack('U')
|
|
2105
|
+
when 0x0391..0x03A1
|
|
2106
|
+
buffer << [codepoint + 32].pack('U')
|
|
2107
|
+
when 0x03A3..0x03AB
|
|
2108
|
+
buffer << [codepoint + 32].pack('U')
|
|
2109
|
+
when 0x03C2
|
|
2110
|
+
buffer << [0x03C3].pack('U')
|
|
2111
|
+
when 0x00DF, 0x1E9E
|
|
2112
|
+
buffer << 'ss'
|
|
2113
|
+
else
|
|
2114
|
+
begin
|
|
2115
|
+
buffer << [codepoint].pack('U').downcase
|
|
2116
|
+
rescue StandardError
|
|
2117
|
+
buffer << [codepoint].pack('U')
|
|
2118
|
+
end
|
|
2119
|
+
end
|
|
2120
|
+
end
|
|
2121
|
+
end
|
|
2122
|
+
end
|
|
2123
|
+
end
|
|
2124
|
+
end
|
|
2125
|
+
end
|