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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -1
  3. data/README.md +18 -21
  4. data/docs/GettingStarted.md +41 -15
  5. data/docs/Tags.md +5 -5
  6. data/docs/WhatsNew.md +59 -7
  7. data/docs/templates/default/yard_tags/html/setup.rb +1 -1
  8. data/lib/yard/autoload.rb +17 -0
  9. data/lib/yard/cli/diff.rb +7 -2
  10. data/lib/yard/code_objects/proxy.rb +1 -1
  11. data/lib/yard/handlers/processor.rb +1 -0
  12. data/lib/yard/handlers/rbs/attribute_handler.rb +43 -0
  13. data/lib/yard/handlers/rbs/base.rb +38 -0
  14. data/lib/yard/handlers/rbs/constant_handler.rb +18 -0
  15. data/lib/yard/handlers/rbs/method_handler.rb +327 -0
  16. data/lib/yard/handlers/rbs/mixin_handler.rb +20 -0
  17. data/lib/yard/handlers/rbs/namespace_handler.rb +26 -0
  18. data/lib/yard/handlers/ruby/attribute_handler.rb +7 -4
  19. data/lib/yard/handlers/ruby/constant_handler.rb +1 -0
  20. data/lib/yard/i18n/locale.rb +1 -1
  21. data/lib/yard/i18n/pot_generator.rb +1 -1
  22. data/lib/yard/parser/rbs/rbs_parser.rb +325 -0
  23. data/lib/yard/parser/rbs/statement.rb +75 -0
  24. data/lib/yard/parser/ruby/ruby_parser.rb +51 -1
  25. data/lib/yard/parser/source_parser.rb +3 -2
  26. data/lib/yard/registry_resolver.rb +7 -0
  27. data/lib/yard/server/library_version.rb +1 -1
  28. data/lib/yard/server/templates/default/fulldoc/html/js/autocomplete.js +208 -12
  29. data/lib/yard/server/templates/default/layout/html/breadcrumb.erb +1 -17
  30. data/lib/yard/server/templates/default/method_details/html/permalink.erb +4 -2
  31. data/lib/yard/server/templates/doc_server/library_list/html/headers.erb +3 -3
  32. data/lib/yard/server/templates/doc_server/library_list/html/library_list.erb +2 -3
  33. data/lib/yard/server/templates/doc_server/processing/html/processing.erb +22 -16
  34. data/lib/yard/tags/directives.rb +7 -0
  35. data/lib/yard/tags/library.rb +3 -3
  36. data/lib/yard/tags/types_explainer.rb +2 -1
  37. data/lib/yard/templates/helpers/base_helper.rb +1 -1
  38. data/lib/yard/templates/helpers/html_helper.rb +15 -4
  39. data/lib/yard/templates/helpers/html_syntax_highlight_helper.rb +6 -1
  40. data/lib/yard/templates/helpers/markup/hybrid_markdown.rb +2125 -0
  41. data/lib/yard/templates/helpers/markup_helper.rb +4 -2
  42. data/lib/yard/version.rb +1 -1
  43. data/po/ja.po +82 -82
  44. data/templates/default/fulldoc/html/full_list.erb +4 -4
  45. data/templates/default/fulldoc/html/js/app.js +503 -319
  46. data/templates/default/fulldoc/html/js/full_list.js +310 -213
  47. data/templates/default/layout/html/headers.erb +1 -1
  48. data/templates/default/method/html/header.erb +3 -3
  49. data/templates/default/module/html/defines.erb +3 -3
  50. data/templates/default/module/html/inherited_methods.erb +1 -0
  51. data/templates/default/module/html/method_summary.erb +8 -0
  52. data/templates/default/module/setup.rb +20 -0
  53. data/templates/default/onefile/html/layout.erb +3 -4
  54. data/templates/guide/fulldoc/html/js/app.js +57 -26
  55. data/templates/guide/layout/html/layout.erb +9 -11
  56. 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?('&lt;') || before_url.end_with?('&lt; ')
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('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;')
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