rdoc 7.1.0 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +3 -4
  3. data/LICENSE.rdoc +4 -0
  4. data/README.md +43 -2
  5. data/doc/markup_reference/markdown.md +104 -3
  6. data/lib/rdoc/code_object/alias.rb +2 -8
  7. data/lib/rdoc/code_object/any_method.rb +11 -6
  8. data/lib/rdoc/code_object/attr.rb +12 -6
  9. data/lib/rdoc/code_object/class_module.rb +62 -32
  10. data/lib/rdoc/code_object/constant.rb +29 -3
  11. data/lib/rdoc/code_object/context/section.rb +30 -43
  12. data/lib/rdoc/code_object/context.rb +53 -37
  13. data/lib/rdoc/code_object/method_attr.rb +9 -15
  14. data/lib/rdoc/code_object/mixin.rb +3 -0
  15. data/lib/rdoc/code_object/top_level.rb +11 -3
  16. data/lib/rdoc/code_object.rb +2 -4
  17. data/lib/rdoc/comment.rb +1 -66
  18. data/lib/rdoc/cross_reference.rb +8 -30
  19. data/lib/rdoc/encoding.rb +3 -3
  20. data/lib/rdoc/generator/aliki.rb +17 -0
  21. data/lib/rdoc/generator/darkfish.rb +12 -6
  22. data/lib/rdoc/generator/json_index.rb +2 -2
  23. data/lib/rdoc/generator/markup.rb +56 -31
  24. data/lib/rdoc/generator/template/aliki/DESIGN.md +536 -0
  25. data/lib/rdoc/generator/template/aliki/_aside_toc.rhtml +1 -1
  26. data/lib/rdoc/generator/template/aliki/_head.rhtml +6 -1
  27. data/lib/rdoc/generator/template/aliki/_sidebar_extends.rhtml +8 -6
  28. data/lib/rdoc/generator/template/aliki/_sidebar_includes.rhtml +8 -6
  29. data/lib/rdoc/generator/template/aliki/_sidebar_installed.rhtml +1 -1
  30. data/lib/rdoc/generator/template/aliki/_sidebar_pages.rhtml +2 -2
  31. data/lib/rdoc/generator/template/aliki/_sidebar_sections.rhtml +1 -1
  32. data/lib/rdoc/generator/template/aliki/_sidebar_toggle.rhtml +1 -1
  33. data/lib/rdoc/generator/template/aliki/class.rhtml +61 -51
  34. data/lib/rdoc/generator/template/aliki/css/rdoc.css +365 -147
  35. data/lib/rdoc/generator/template/aliki/index.rhtml +1 -1
  36. data/lib/rdoc/generator/template/aliki/js/aliki.js +28 -20
  37. data/lib/rdoc/generator/template/aliki/js/bash_highlighter.js +167 -0
  38. data/lib/rdoc/generator/template/aliki/js/search_controller.js +1 -1
  39. data/lib/rdoc/generator/template/aliki/page.rhtml +1 -1
  40. data/lib/rdoc/generator/template/aliki/servlet_not_found.rhtml +1 -1
  41. data/lib/rdoc/generator/template/aliki/servlet_root.rhtml +2 -2
  42. data/lib/rdoc/generator/template/darkfish/_sidebar_extends.rhtml +8 -6
  43. data/lib/rdoc/generator/template/darkfish/_sidebar_includes.rhtml +8 -6
  44. data/lib/rdoc/generator/template/darkfish/_sidebar_installed.rhtml +1 -1
  45. data/lib/rdoc/generator/template/darkfish/_sidebar_pages.rhtml +1 -1
  46. data/lib/rdoc/generator/template/darkfish/_sidebar_sections.rhtml +1 -1
  47. data/lib/rdoc/generator/template/darkfish/_sidebar_table_of_contents.rhtml +5 -5
  48. data/lib/rdoc/generator/template/darkfish/class.rhtml +18 -21
  49. data/lib/rdoc/generator/template/darkfish/css/rdoc.css +0 -1
  50. data/lib/rdoc/generator/template/darkfish/table_of_contents.rhtml +3 -3
  51. data/lib/rdoc/i18n/text.rb +3 -3
  52. data/lib/rdoc/markdown.kpeg +36 -17
  53. data/lib/rdoc/markdown.rb +324 -125
  54. data/lib/rdoc/markup/document.rb +2 -2
  55. data/lib/rdoc/markup/formatter.rb +128 -115
  56. data/lib/rdoc/markup/heading.rb +3 -6
  57. data/lib/rdoc/markup/indented_paragraph.rb +1 -1
  58. data/lib/rdoc/markup/inline_parser.rb +312 -0
  59. data/lib/rdoc/markup/list.rb +2 -2
  60. data/lib/rdoc/markup/list_item.rb +2 -2
  61. data/lib/rdoc/markup/parser.rb +1 -1
  62. data/lib/rdoc/markup/pre_process.rb +0 -25
  63. data/lib/rdoc/markup/to_ansi.rb +52 -5
  64. data/lib/rdoc/markup/to_bs.rb +23 -43
  65. data/lib/rdoc/markup/to_html.rb +279 -226
  66. data/lib/rdoc/markup/to_html_crossref.rb +122 -136
  67. data/lib/rdoc/markup/to_html_snippet.rb +65 -65
  68. data/lib/rdoc/markup/to_joined_paragraph.rb +0 -5
  69. data/lib/rdoc/markup/to_label.rb +22 -23
  70. data/lib/rdoc/markup/to_markdown.rb +62 -38
  71. data/lib/rdoc/markup/to_rdoc.rb +88 -28
  72. data/lib/rdoc/markup/to_table_of_contents.rb +1 -1
  73. data/lib/rdoc/markup/to_test.rb +9 -1
  74. data/lib/rdoc/markup/to_tt_only.rb +10 -23
  75. data/lib/rdoc/markup/verbatim.rb +1 -1
  76. data/lib/rdoc/markup.rb +8 -30
  77. data/lib/rdoc/options.rb +36 -51
  78. data/lib/rdoc/parser/c.rb +7 -6
  79. data/lib/rdoc/parser/changelog.rb +21 -0
  80. data/lib/rdoc/parser/rbs.rb +275 -0
  81. data/lib/rdoc/parser/ruby.rb +955 -2067
  82. data/lib/rdoc/parser/ruby_colorizer.rb +253 -0
  83. data/lib/rdoc/parser.rb +3 -2
  84. data/lib/rdoc/rbs_helper.rb +186 -0
  85. data/lib/rdoc/rdoc.rb +196 -24
  86. data/lib/rdoc/ri/driver.rb +8 -2
  87. data/lib/rdoc/ri/paths.rb +1 -1
  88. data/lib/rdoc/{servlet.rb → ri/servlet.rb} +5 -5
  89. data/lib/rdoc/ri.rb +4 -3
  90. data/lib/rdoc/rubygems_hook.rb +11 -11
  91. data/lib/rdoc/server.rb +460 -0
  92. data/lib/rdoc/stats.rb +147 -124
  93. data/lib/rdoc/store.rb +212 -4
  94. data/lib/rdoc/task.rb +16 -15
  95. data/lib/rdoc/text.rb +24 -117
  96. data/lib/rdoc/token_stream.rb +11 -33
  97. data/lib/rdoc/version.rb +1 -1
  98. data/lib/rdoc.rb +35 -7
  99. data/lib/rubygems_plugin.rb +2 -11
  100. data/rdoc-logo.svg +43 -0
  101. data/rdoc.gemspec +6 -4
  102. metadata +37 -23
  103. data/lib/rdoc/code_object/anon_class.rb +0 -10
  104. data/lib/rdoc/code_object/ghost_method.rb +0 -6
  105. data/lib/rdoc/code_object/meta_method.rb +0 -6
  106. data/lib/rdoc/markup/attr_changer.rb +0 -22
  107. data/lib/rdoc/markup/attr_span.rb +0 -35
  108. data/lib/rdoc/markup/attribute_manager.rb +0 -432
  109. data/lib/rdoc/markup/attributes.rb +0 -70
  110. data/lib/rdoc/markup/regexp_handling.rb +0 -40
  111. data/lib/rdoc/parser/prism_ruby.rb +0 -1100
  112. data/lib/rdoc/parser/ripper_state_lex.rb +0 -302
  113. data/lib/rdoc/parser/ruby_tools.rb +0 -163
@@ -26,7 +26,7 @@ class RDoc::Markup::Document
26
26
  ##
27
27
  # Creates a new Document with +parts+
28
28
 
29
- def initialize *parts
29
+ def initialize(*parts)
30
30
  @parts = []
31
31
  @parts.concat parts
32
32
 
@@ -148,7 +148,7 @@ class RDoc::Markup::Document
148
148
  ##
149
149
  # Appends +parts+ to the document
150
150
 
151
- def push *parts
151
+ def push(*parts)
152
152
  self.parts.concat parts
153
153
  end
154
154
 
@@ -10,13 +10,9 @@
10
10
  # RDoc::Markup::FormatterTestCase. If you're writing a text-output formatter
11
11
  # use RDoc::Markup::TextFormatterTestCase which provides extra test cases.
12
12
 
13
- class RDoc::Markup::Formatter
14
-
15
- ##
16
- # Tag for inline markup containing a +bit+ for the bitmask and the +on+ and
17
- # +off+ triggers.
13
+ require 'rdoc/markup/inline_parser'
18
14
 
19
- InlineTag = Struct.new(:bit, :on, :off)
15
+ class RDoc::Markup::Formatter
20
16
 
21
17
  ##
22
18
  # Converts a target url to one that is relative to a given path
@@ -45,21 +41,9 @@ class RDoc::Markup::Formatter
45
41
  ##
46
42
  # Creates a new Formatter
47
43
 
48
- def initialize(options, markup = nil)
49
- @options = options
50
-
51
- @markup = markup || RDoc::Markup.new
52
- @am = @markup.attribute_manager
53
- @am.add_regexp_handling(/<br>/, :HARD_BREAK)
44
+ def initialize
45
+ @markup = RDoc::Markup.new
54
46
 
55
- @attributes = @am.attributes
56
-
57
- @attr_tags = []
58
-
59
- @in_tt = 0
60
- @tt_bit = @attributes.bitmap_for :TT
61
-
62
- @hard_break = ''
63
47
  @from_path = '.'
64
48
  end
65
49
 
@@ -84,29 +68,6 @@ class RDoc::Markup::Formatter
84
68
  @markup.add_regexp_handling(/rdoc-[a-z]+:[^\s\]]+/, :RDOCLINK)
85
69
  end
86
70
 
87
- ##
88
- # Adds a regexp handling for links of the form {<text>}[<url>] and
89
- # <word>[<url>]
90
-
91
- def add_regexp_handling_TIDYLINK
92
- @markup.add_regexp_handling(/(?:
93
- \{[^{}]*\} | # multi-word label
94
- \b[^\s{}]+? # single-word label
95
- )
96
-
97
- \[\S+?\] # link target
98
- /x, :TIDYLINK)
99
- end
100
-
101
- ##
102
- # Add a new set of tags for an attribute. We allow separate start and end
103
- # tags for flexibility
104
-
105
- def add_tag(name, start, stop)
106
- attr = @attributes.bitmap_for name
107
- @attr_tags << InlineTag.new(attr, start, stop)
108
- end
109
-
110
71
  ##
111
72
  # Allows +tag+ to be decorated with additional information.
112
73
 
@@ -121,117 +82,169 @@ class RDoc::Markup::Formatter
121
82
  @markup.convert content, self
122
83
  end
123
84
 
124
- ##
125
- # Converts flow items +flow+
126
-
127
- def convert_flow(flow)
128
- res = []
85
+ # Applies regexp handling to +text+ and returns an array of [text, converted?] pairs.
129
86
 
130
- flow.each do |item|
131
- case item
132
- when String then
133
- res << convert_string(item)
134
- when RDoc::Markup::AttrChanger then
135
- off_tags res, item
136
- on_tags res, item
137
- when RDoc::Markup::RegexpHandling then
138
- res << convert_regexp_handling(item)
139
- else
140
- raise "Unknown flow element: #{item.inspect}"
87
+ def apply_regexp_handling(text)
88
+ matched = []
89
+ @markup.regexp_handlings.each_with_index do |(pattern, name), priority|
90
+ text.scan(pattern) do
91
+ m = Regexp.last_match
92
+ idx = m[1] ? 1 : 0
93
+ matched << [m.begin(idx), m.end(idx), m[idx], name, priority]
141
94
  end
142
95
  end
96
+ # If the start positions are the same, prefer the earlier-registered one
97
+ # (lower numeric priority from each_with_index).
98
+ matched.sort_by! {|beg_pos, _, _, _, priority| [beg_pos, priority] }
99
+
100
+ pos = 0
101
+ output = []
102
+ matched.each do |beg_pos, end_pos, s, name|
103
+ next if beg_pos < pos
104
+
105
+ output << [text[pos...beg_pos], false] if beg_pos != pos
106
+ handled = public_send(:"handle_regexp_#{name}", s)
107
+ output << [handled, true]
108
+ pos = end_pos
109
+ end
143
110
 
144
- res.join
111
+ output << [text[pos..], false] if pos < text.size
112
+ output
145
113
  end
146
114
 
147
- ##
148
- # Converts added regexp handlings. See RDoc::Markup#add_regexp_handling
115
+ # Called when processing plain text while traversing inline nodes from handle_inline.
116
+ # +text+ may need proper escaping.
117
+
118
+ def handle_PLAIN_TEXT(text)
119
+ end
149
120
 
150
- def convert_regexp_handling(target)
151
- return target.text if in_tt?
121
+ # Called when processing regexp-handling-processed text while traversing inline nodes from handle_inline.
122
+ # +text+ may contain markup tags.
152
123
 
153
- handled = false
124
+ def handle_REGEXP_HANDLING_TEXT(text)
125
+ end
154
126
 
155
- @attributes.each_name_of target.type do |name|
156
- method_name = "handle_regexp_#{name}"
127
+ # Called when processing text node while traversing inline nodes from handle_inline.
128
+ # Apply regexp handling and dispatch to the appropriate handler: handle_REGEXP_HANDLING_TEXT or handle_PLAIN_TEXT.
157
129
 
158
- if respond_to? method_name then
159
- target.text = public_send method_name, target
160
- handled = true
130
+ def handle_TEXT(text)
131
+ apply_regexp_handling(text).each do |part, converted|
132
+ if converted
133
+ handle_REGEXP_HANDLING_TEXT(part)
134
+ else
135
+ handle_PLAIN_TEXT(part)
161
136
  end
162
137
  end
138
+ end
163
139
 
164
- unless handled then
165
- target_name = @attributes.as_string target.type
140
+ # Called when processing a hard break while traversing inline nodes from handle_inline.
166
141
 
167
- raise RDoc::Error, "Unhandled regexp handling #{target_name}: #{target}"
168
- end
142
+ def handle_HARD_BREAK
143
+ end
169
144
 
170
- target.text
145
+ # Called when processing bold nodes while traversing inline nodes from handle_inline.
146
+ # Traverse the children nodes and dispatch to the appropriate handlers.
147
+
148
+ def handle_BOLD(nodes)
149
+ traverse_inline_nodes(nodes)
171
150
  end
172
151
 
173
- ##
174
- # Converts a string to be fancier if desired
152
+ # Called when processing emphasis nodes while traversing inline nodes from handle_inline.
153
+ # Traverse the children nodes and dispatch to the appropriate handlers.
175
154
 
176
- def convert_string(string)
177
- string
155
+ def handle_EM(nodes)
156
+ traverse_inline_nodes(nodes)
178
157
  end
179
158
 
180
- ##
181
- # Use ignore in your subclass to ignore the content of a node.
182
- #
183
- # ##
184
- # # We don't support raw nodes in ToNoRaw
185
- #
186
- # alias accept_raw ignore
159
+ # Called when processing bold word nodes while traversing inline nodes from handle_inline.
160
+ # +word+ may need proper escaping.
187
161
 
188
- def ignore *node
162
+ def handle_BOLD_WORD(word)
163
+ handle_PLAIN_TEXT(word)
189
164
  end
190
165
 
191
- ##
192
- # Are we currently inside tt tags?
166
+ # Called when processing emphasis word nodes while traversing inline nodes from handle_inline.
167
+ # +word+ may need proper escaping.
193
168
 
194
- def in_tt?
195
- @in_tt > 0
169
+ def handle_EM_WORD(word)
170
+ handle_PLAIN_TEXT(word)
196
171
  end
197
172
 
198
- def tt_tag?(attr_mask, reverse = false)
199
- each_attr_tag(attr_mask, reverse) do |tag|
200
- return true if tt? tag
201
- end
202
- false
173
+ # Called when processing tt nodes while traversing inline nodes from handle_inline.
174
+ # +code+ may need proper escaping.
175
+
176
+ def handle_TT(code)
177
+ handle_PLAIN_TEXT(code)
203
178
  end
204
179
 
205
- ##
206
- # Turns on tags for +item+ on +res+
180
+ # Called when processing strike nodes while traversing inline nodes from handle_inline.
181
+ # Traverse the children nodes and dispatch to the appropriate handlers.
207
182
 
208
- def on_tags(res, item)
209
- each_attr_tag(item.turn_on) do |tag|
210
- res << annotate(tag.on)
211
- @in_tt += 1 if tt? tag
212
- end
183
+ def handle_STRIKE(nodes)
184
+ traverse_inline_nodes(nodes)
213
185
  end
214
186
 
215
- ##
216
- # Turns off tags for +item+ on +res+
187
+ # Called when processing tidylink nodes while traversing inline nodes from handle_inline.
188
+ # +label_part+ is an array of strings or nodes representing the link label.
189
+ # +url+ is the link URL.
190
+ # Traverse the label_part nodes and dispatch to the appropriate handlers.
217
191
 
218
- def off_tags(res, item)
219
- each_attr_tag(item.turn_off, true) do |tag|
220
- @in_tt -= 1 if tt? tag
221
- res << annotate(tag.off)
222
- end
192
+ def handle_TIDYLINK(label_part, url)
193
+ traverse_inline_nodes(label_part)
223
194
  end
224
195
 
225
- def each_attr_tag(attr_mask, reverse = false)
226
- return if attr_mask.zero?
196
+ # Parses inline +text+, traverse the resulting nodes, and calls the appropriate handler methods.
197
+
198
+ def handle_inline(text)
199
+ nodes = RDoc::Markup::InlineParser.new(text).parse
200
+ traverse_inline_nodes(nodes)
201
+ end
227
202
 
228
- @attr_tags.public_send(reverse ? :reverse_each : :each) do |tag|
229
- if attr_mask & tag.bit != 0 then
230
- yield tag
203
+ # Traverses +nodes+ and calls the appropriate handler methods
204
+ # Nodes formats are described in RDoc::Markup::InlineParser#parse
205
+
206
+ def traverse_inline_nodes(nodes)
207
+ nodes.each do |node|
208
+ next handle_TEXT(node) if String === node
209
+ case node[:type]
210
+ when :TIDYLINK
211
+ handle_TIDYLINK(node[:children], node[:url])
212
+ when :HARD_BREAK
213
+ handle_HARD_BREAK
214
+ when :BOLD
215
+ handle_BOLD(node[:children])
216
+ when :BOLD_WORD
217
+ handle_BOLD_WORD(node[:children][0] || '')
218
+ when :EM
219
+ handle_EM(node[:children])
220
+ when :EM_WORD
221
+ handle_EM_WORD(node[:children][0] || '')
222
+ when :TT
223
+ handle_TT(node[:children][0] || '')
224
+ when :STRIKE
225
+ handle_STRIKE(node[:children])
231
226
  end
232
227
  end
233
228
  end
234
229
 
230
+ ##
231
+ # Converts a string to be fancier if desired
232
+
233
+ def convert_string(string)
234
+ string
235
+ end
236
+
237
+ ##
238
+ # Use ignore in your subclass to ignore the content of a node.
239
+ #
240
+ # ##
241
+ # # We don't support raw nodes in ToNoRaw
242
+ #
243
+ # alias accept_raw ignore
244
+
245
+ def ignore(*node)
246
+ end
247
+
235
248
  ##
236
249
  # Extracts and a scheme, url and an anchor id from +url+ and returns them.
237
250
 
@@ -52,13 +52,10 @@ module RDoc
52
52
  #: () -> RDoc::Markup::ToHtml
53
53
  def self.to_html
54
54
  @to_html ||= begin
55
- markup = Markup.new
56
- markup.add_regexp_handling CrossReference::CROSSREF_REGEXP, :CROSSREF
55
+ to_html = Markup::ToHtml.new
57
56
 
58
- to_html = Markup::ToHtml.new nil
59
-
60
- def to_html.handle_regexp_CROSSREF(target)
61
- target.text.sub(/^\\/, '')
57
+ def to_html.handle_regexp_CROSSREF(text)
58
+ text.sub(/^\\/, '')
62
59
  end
63
60
 
64
61
  to_html
@@ -13,7 +13,7 @@ class RDoc::Markup::IndentedParagraph < RDoc::Markup::Raw
13
13
  # Creates a new IndentedParagraph containing +parts+ indented with +indent+
14
14
  # spaces
15
15
 
16
- def initialize indent, *parts
16
+ def initialize(indent, *parts)
17
17
  @indent = indent
18
18
 
19
19
  super(*parts)
@@ -0,0 +1,312 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require 'strscan'
5
+
6
+ # Parses inline markup in RDoc text.
7
+ # This parser handles em, bold, strike, tt, hard break, and tidylink.
8
+ # Block-level constructs are handled in RDoc::Markup::Parser.
9
+
10
+ class RDoc::Markup::InlineParser
11
+
12
+ # TT, BOLD_WORD, EM_WORD: regexp-handling(example: crossref) is disabled
13
+ WORD_PAIRS = {
14
+ '*' => :BOLD_WORD,
15
+ '**' => :BOLD_WORD,
16
+ '_' => :EM_WORD,
17
+ '__' => :EM_WORD,
18
+ '+' => :TT,
19
+ '++' => :TT,
20
+ '`' => :TT,
21
+ '``' => :TT
22
+ } # :nodoc:
23
+
24
+ # Other types: regexp-handling(example: crossref) is enabled
25
+ TAGS = {
26
+ 'em' => :EM,
27
+ 'i' => :EM,
28
+ 'b' => :BOLD,
29
+ 's' => :STRIKE,
30
+ 'del' => :STRIKE,
31
+ } # :nodoc:
32
+
33
+ STANDALONE_TAGS = { 'br' => :HARD_BREAK } # :nodoc:
34
+
35
+ CODEBLOCK_TAGS = %w[tt code] # :nodoc:
36
+
37
+ TOKENS = {
38
+ **WORD_PAIRS.transform_values { [:word_pair, nil] },
39
+ **TAGS.keys.to_h {|tag| ["<#{tag}>", [:open_tag, tag]] },
40
+ **TAGS.keys.to_h {|tag| ["</#{tag}>", [:close_tag, tag]] },
41
+ **CODEBLOCK_TAGS.to_h {|tag| ["<#{tag}>", [:code_start, tag]] },
42
+ **STANDALONE_TAGS.keys.to_h {|tag| ["<#{tag}>", [:standalone_tag, tag]] },
43
+ '{' => [:tidylink_start, nil],
44
+ '}' => [:tidylink_mid, nil],
45
+ '\\' => [:escape, nil],
46
+ '[' => nil # To make `label[url]` scan as separate tokens
47
+ } # :nodoc:
48
+
49
+ multi_char_tokens_regexp = Regexp.union(TOKENS.keys.select {|s| s.size > 1 }).source
50
+ token_starts_regexp = TOKENS.keys.map {|s| s[0] }.uniq.map {|s| Regexp.escape(s) }.join
51
+
52
+ SCANNER_REGEXP =
53
+ /(?:
54
+ #{multi_char_tokens_regexp}
55
+ |[^#{token_starts_regexp}\sa-zA-Z0-9\.]+ # chunk of normal text
56
+ |\s+|[a-zA-Z0-9\.]+|.
57
+ )/x # :nodoc:
58
+
59
+ # Characters that can be escaped with backslash.
60
+ ESCAPING_CHARS = '\\*_+`{}[]<>' # :nodoc:
61
+
62
+ # Pattern to match code block content until <code></tt></code> or <tt></code></tt>.
63
+ CODEBLOCK_REGEXPS = CODEBLOCK_TAGS.to_h {|name| [name, /((?:\\.|[^\\])*?)<\/#{name}>/] } # :nodoc:
64
+
65
+ # Word contains alphanumeric and <tt>_./:[]-</tt> characters.
66
+ # Word may start with <tt>#</tt> and may end with any non-space character. (e.g. <tt>#eql?</tt>).
67
+ # Underscore delimiter have special rules.
68
+ WORD_REGEXPS = {
69
+ # Words including _, longest match.
70
+ # Example: `_::A_` `_-42_` `_A::B::C.foo_bar[baz]_` `_kwarg:_`
71
+ # Content must not include _ followed by non-alphanumeric character
72
+ # Example: `_host_:_port_` will be `_host_` + `:` + `_port_`
73
+ '_' => /#?([a-zA-Z0-9.\/:\[\]-]|_+[a-zA-Z0-9])+[^\s]?_(?=[^a-zA-Z0-9_]|\z)/,
74
+ # Words allowing _ but not allowing __
75
+ '__' => /#?[a-zA-Z0-9.\/:\[\]-]*(_[a-zA-Z0-9.\/:\[\]-]+)*[^\s]?__(?=[^a-zA-Z0-9]|\z)/,
76
+ **%w[* ** + ++ ` ``].to_h do |s|
77
+ # normal words that can be used within +word+ or *word*
78
+ [s, /#?[a-zA-Z0-9_.\/:\[\]-]+[^\s]?#{Regexp.escape(s)}(?=[^a-zA-Z0-9]|\z)/]
79
+ end
80
+ } # :nodoc:
81
+
82
+ def initialize(string)
83
+ @scanner = StringScanner.new(string)
84
+ @last_match = nil
85
+ @scanner_negative_cache = Set.new
86
+ @stack = []
87
+ @delimiters = {}
88
+ end
89
+
90
+ # Return the current parsing node on <tt>@stack</tt>.
91
+
92
+ def current
93
+ @stack.last
94
+ end
95
+
96
+ # Parse and return an array of nodes.
97
+ # Node format:
98
+ # {
99
+ # type: :EM | :BOLD | :BOLD_WORD | :EM_WORD | :TT | :STRIKE | :HARD_BREAK | :TIDYLINK,
100
+ # url: string # only for :TIDYLINK
101
+ # children: [string_or_node, ...]
102
+ # }
103
+
104
+ def parse
105
+ stack_push(:root, nil)
106
+ while true
107
+ type, token, value = scan_token
108
+ close = nil
109
+ tidylink_url = nil
110
+ case type
111
+ when :node
112
+ current[:children] << value
113
+ invalidate_open_tidylinks if value[:type] == :TIDYLINK
114
+ when :eof
115
+ close = :root
116
+ when :tidylink_open
117
+ stack_push(:tidylink, token)
118
+ when :tidylink_close
119
+ close = :tidylink
120
+ if value
121
+ tidylink_url = value
122
+ else
123
+ # Tidylink closing brace without URL part. Treat opening and closing braces as normal text
124
+ # `{labelnodes}...` case.
125
+ current[:children] << token
126
+ end
127
+ when :invalidated_tidylink_close
128
+ # `{...{label}[url]...}` case. Nested tidylink invalidates outer one. The last `}` closes the invalidated tidylink.
129
+ current[:children] << token
130
+ close = :invalidated_tidylink
131
+ when :text
132
+ current[:children] << token
133
+ when :open
134
+ stack_push(value, token)
135
+ when :close
136
+ if @delimiters[value]
137
+ close = value
138
+ else
139
+ # closing tag without matching opening tag. Treat as normal text.
140
+ current[:children] << token
141
+ end
142
+ end
143
+
144
+ next unless close
145
+
146
+ while current[:delimiter] != close
147
+ children = current[:children]
148
+ open_token = current[:token]
149
+ stack_pop
150
+ current[:children] << open_token if open_token
151
+ current[:children].concat(children)
152
+ end
153
+
154
+ token = current[:token]
155
+ children = compact_string(current[:children])
156
+ stack_pop
157
+
158
+ return children if close == :root
159
+
160
+ if close == :tidylink || close == :invalidated_tidylink
161
+ if tidylink_url
162
+ current[:children] << { type: :TIDYLINK, children: children, url: tidylink_url }
163
+ invalidate_open_tidylinks
164
+ else
165
+ current[:children] << token
166
+ current[:children].concat(children)
167
+ end
168
+ else
169
+ current[:children] << { type: TAGS[close], children: children }
170
+ end
171
+ end
172
+ end
173
+
174
+ private
175
+
176
+ # When a valid tidylink node is encountered, invalidate all nested tidylinks.
177
+
178
+ def invalidate_open_tidylinks
179
+ return unless @delimiters[:tidylink]
180
+
181
+ @delimiters[:invalidated_tidylink] ||= []
182
+ @delimiters[:tidylink].each do |idx|
183
+ @delimiters[:invalidated_tidylink] << idx
184
+ @stack[idx][:delimiter] = :invalidated_tidylink
185
+ end
186
+ @delimiters.delete(:tidylink)
187
+ end
188
+
189
+ # Pop the top node off the stack when node is closed by a closing delimiter or an error.
190
+
191
+ def stack_pop
192
+ delimiter = current[:delimiter]
193
+ @delimiters[delimiter].pop
194
+ @delimiters.delete(delimiter) if @delimiters[delimiter].empty?
195
+ @stack.pop
196
+ end
197
+
198
+ # Push a new node onto the stack when encountering an opening delimiter.
199
+
200
+ def stack_push(delimiter, token)
201
+ node = { delimiter: delimiter, token: token, children: [] }
202
+ (@delimiters[delimiter] ||= []) << @stack.size
203
+ @stack << node
204
+ end
205
+
206
+ # Compacts adjacent strings in +nodes+ into a single string.
207
+
208
+ def compact_string(nodes)
209
+ nodes.chunk {|e| String === e }.flat_map do |is_str, elems|
210
+ is_str ? elems.join : elems
211
+ end
212
+ end
213
+
214
+ # Scan from StringScanner with +pattern+
215
+ # If +negative_cache+ is true, caches scan failure result. <tt>scan(pattern, negative_cache: true)</tt> return nil when it is called again after a failure.
216
+ # Be careful to use +negative_cache+ with a pattern and position that does not match after previous failure.
217
+
218
+ def strscan(pattern, negative_cache: false)
219
+ return if negative_cache && @scanner_negative_cache.include?(pattern)
220
+
221
+ string = @scanner.scan(pattern)
222
+ @last_match = string if string
223
+ @scanner_negative_cache << pattern if !string && negative_cache
224
+ string
225
+ end
226
+
227
+ # Scan and return the next token for parsing.
228
+ # Returns <tt>[token_type, token_string_or_nil, extra_info]</tt>
229
+
230
+ def scan_token
231
+ last_match = @last_match
232
+ token = strscan(SCANNER_REGEXP)
233
+ type, name = TOKENS[token]
234
+
235
+ case type
236
+ when :word_pair
237
+ # If the character before word pair delimiter is alphanumeric, do not treat as word pair.
238
+ word_pair = strscan(WORD_REGEXPS[token]) unless /[a-zA-Z0-9]\z/.match?(last_match)
239
+
240
+ if word_pair.nil?
241
+ [:text, token, nil]
242
+ elsif token == '__' && word_pair.match?(/\A[a-zA-Z]+__\z/)
243
+ # Special exception: __FILE__, __LINE__, __send__ should be treated as normal text.
244
+ [:text, "#{token}#{word_pair}", nil]
245
+ else
246
+ [:node, nil, { type: WORD_PAIRS[token], children: [word_pair.delete_suffix(token)] }]
247
+ end
248
+ when :open_tag
249
+ [:open, token, name]
250
+ when :close_tag
251
+ [:close, token, name]
252
+ when :code_start
253
+ if (codeblock = strscan(CODEBLOCK_REGEXPS[name], negative_cache: true))
254
+ # Need to unescape `\\` and `\<`.
255
+ # RDoc also unescapes backslash + word separators, but this is not really necessary.
256
+ content = codeblock.delete_suffix("</#{name}>").gsub(/\\(.)/) { '\\<*+_`'.include?($1) ? $1 : $& }
257
+ [:node, nil, { type: :TT, children: content.empty? ? [] : [content] }]
258
+ else
259
+ [:text, token, nil]
260
+ end
261
+ when :standalone_tag
262
+ [:node, nil, { type: STANDALONE_TAGS[name], children: [] }]
263
+ when :tidylink_start
264
+ [:tidylink_open, token, nil]
265
+ when :tidylink_mid
266
+ if @delimiters[:tidylink]
267
+ if (url = read_tidylink_url)
268
+ [:tidylink_close, nil, url]
269
+ else
270
+ [:tidylink_close, token, nil]
271
+ end
272
+ elsif @delimiters[:invalidated_tidylink]
273
+ [:invalidated_tidylink_close, token, nil]
274
+ else
275
+ [:text, token, nil]
276
+ end
277
+ when :escape
278
+ next_char = strscan(/./)
279
+ if next_char.nil?
280
+ # backslash at end of string
281
+ [:text, '\\', nil]
282
+ elsif next_char && ESCAPING_CHARS.include?(next_char)
283
+ # escaped character
284
+ [:text, next_char, nil]
285
+ else
286
+ # If next_char not an escaping character, it is treated as text token with backslash + next_char
287
+ # For example, backslash of `\Ruby` (suppressed crossref) remains.
288
+ [:text, "\\#{next_char}", nil]
289
+ end
290
+ else
291
+ if token.nil?
292
+ [:eof, nil, nil]
293
+ elsif token.match?(/\A[A-Za-z0-9]*\z/) && (url = read_tidylink_url)
294
+ # Simplified tidylink: label[url]
295
+ [:node, nil, { type: :TIDYLINK, children: [token], url: url }]
296
+ else
297
+ [:text, token, nil]
298
+ end
299
+ end
300
+ end
301
+
302
+ # Read the URL part of a tidylink from the current position.
303
+ # Returns nil if no valid URL part is found.
304
+ # URL part is enclosed in square brackets and may contain escaped brackets.
305
+ # Example: <tt>[http://example.com/?q=\[\]]</tt> represents <tt>http://example.com/?q=[]</tt>.
306
+ # If we're accepting rdoc-style links in markdown, url may include <tt>*+<_</tt> with backslash escape.
307
+
308
+ def read_tidylink_url
309
+ bracketed_url = strscan(/\[([^\s\[\]\\]|\\[\[\]\\*+<_])+\]/)
310
+ bracketed_url[1...-1].gsub(/\\(.)/, '\1') if bracketed_url
311
+ end
312
+ end
@@ -37,7 +37,7 @@ class RDoc::Markup::List
37
37
  # Creates a new list of +type+ with +items+. Valid list types are:
38
38
  # +:BULLET+, +:LABEL+, +:LALPHA+, +:NOTE+, +:NUMBER+, +:UALPHA+
39
39
 
40
- def initialize type = nil, *items
40
+ def initialize(type = nil, *items)
41
41
  @type = type
42
42
  @items = []
43
43
  @items.concat items
@@ -94,7 +94,7 @@ class RDoc::Markup::List
94
94
  ##
95
95
  # Appends +items+ to the list
96
96
 
97
- def push *items
97
+ def push(*items)
98
98
  @items.concat items
99
99
  end
100
100