rdoc 8.0.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +1 -3
  3. data/RI.md +75 -75
  4. data/exe/rdoc +2 -2
  5. data/lib/rdoc/code_object/alias.rb +71 -69
  6. data/lib/rdoc/code_object/any_method.rb +305 -303
  7. data/lib/rdoc/code_object/attr.rb +150 -148
  8. data/lib/rdoc/code_object/class_module.rb +798 -792
  9. data/lib/rdoc/code_object/constant.rb +175 -173
  10. data/lib/rdoc/code_object/context/section.rb +142 -138
  11. data/lib/rdoc/code_object/context.rb +926 -958
  12. data/lib/rdoc/code_object/extend.rb +7 -5
  13. data/lib/rdoc/code_object/include.rb +7 -5
  14. data/lib/rdoc/code_object/method_attr.rb +326 -319
  15. data/lib/rdoc/code_object/mixin.rb +97 -95
  16. data/lib/rdoc/code_object/normal_class.rb +77 -78
  17. data/lib/rdoc/code_object/normal_module.rb +61 -59
  18. data/lib/rdoc/code_object/require.rb +23 -39
  19. data/lib/rdoc/code_object/single_class.rb +21 -19
  20. data/lib/rdoc/code_object/top_level.rb +212 -219
  21. data/lib/rdoc/code_object.rb +305 -303
  22. data/lib/rdoc/comment.rb +275 -273
  23. data/lib/rdoc/cross_reference.rb +192 -190
  24. data/lib/rdoc/encoding.rb +105 -103
  25. data/lib/rdoc/erb_partial.rb +13 -11
  26. data/lib/rdoc/erbio.rb +29 -27
  27. data/lib/rdoc/generator/aliki.rb +161 -153
  28. data/lib/rdoc/generator/darkfish.rb +645 -635
  29. data/lib/rdoc/generator/json_index.rb +233 -229
  30. data/lib/rdoc/generator/markup.rb +164 -146
  31. data/lib/rdoc/generator/pot/message_extractor.rb +57 -51
  32. data/lib/rdoc/generator/pot/po.rb +52 -51
  33. data/lib/rdoc/generator/pot/po_entry.rb +138 -132
  34. data/lib/rdoc/generator/pot.rb +85 -81
  35. data/lib/rdoc/generator/ri.rb +23 -19
  36. data/lib/rdoc/generator/template/aliki/DESIGN.md +6 -4
  37. data/lib/rdoc/generator/template/aliki/_footer.rhtml +1 -1
  38. data/lib/rdoc/generator/template/aliki/_head.rhtml +10 -10
  39. data/lib/rdoc/generator/template/aliki/_header.rhtml +29 -44
  40. data/lib/rdoc/generator/template/aliki/_sidebar_search.rhtml +4 -4
  41. data/lib/rdoc/generator/template/aliki/css/rdoc.css +207 -178
  42. data/lib/rdoc/generator/template/aliki/js/aliki.js +60 -84
  43. data/lib/rdoc/generator/template/darkfish/_footer.rhtml +1 -1
  44. data/lib/rdoc/generator.rb +48 -46
  45. data/lib/rdoc/i18n/locale.rb +99 -95
  46. data/lib/rdoc/i18n/text.rb +109 -105
  47. data/lib/rdoc/i18n.rb +7 -5
  48. data/lib/rdoc/markdown/byte_runtime.rb +80 -0
  49. data/lib/rdoc/markdown.kpeg +15 -11
  50. data/lib/rdoc/markdown.rb +40 -47
  51. data/lib/rdoc/markup/block_quote.rb +12 -8
  52. data/lib/rdoc/markup/document.rb +127 -123
  53. data/lib/rdoc/markup/formatter.rb +219 -215
  54. data/lib/rdoc/markup/include.rb +33 -29
  55. data/lib/rdoc/markup/indented_paragraph.rb +37 -33
  56. data/lib/rdoc/markup/inline_parser.rb +281 -277
  57. data/lib/rdoc/markup/list.rb +80 -88
  58. data/lib/rdoc/markup/list_item.rb +73 -85
  59. data/lib/rdoc/markup/paragraph.rb +23 -19
  60. data/lib/rdoc/markup/parser.rb +501 -497
  61. data/lib/rdoc/markup/pre_process.rb +283 -279
  62. data/lib/rdoc/markup/raw.rb +2 -2
  63. data/lib/rdoc/markup/rule.rb +16 -12
  64. data/lib/rdoc/markup/to_ansi.rb +143 -139
  65. data/lib/rdoc/markup/to_bs.rb +72 -68
  66. data/lib/rdoc/markup/to_html.rb +594 -565
  67. data/lib/rdoc/markup/to_html_crossref.rb +234 -230
  68. data/lib/rdoc/markup/to_html_snippet.rb +232 -227
  69. data/lib/rdoc/markup/to_joined_paragraph.rb +36 -32
  70. data/lib/rdoc/markup/to_label.rb +63 -59
  71. data/lib/rdoc/markup/to_markdown.rb +212 -208
  72. data/lib/rdoc/markup/to_rdoc.rb +336 -332
  73. data/lib/rdoc/markup/to_table_of_contents.rb +66 -62
  74. data/lib/rdoc/markup/to_test.rb +60 -56
  75. data/lib/rdoc/markup/to_tt_only.rb +84 -80
  76. data/lib/rdoc/markup/verbatim.rb +62 -58
  77. data/lib/rdoc/markup.rb +198 -196
  78. data/lib/rdoc/options.rb +1063 -1061
  79. data/lib/rdoc/parser/c.rb +1039 -1037
  80. data/lib/rdoc/parser/changelog.rb +319 -315
  81. data/lib/rdoc/parser/markdown.rb +17 -13
  82. data/lib/rdoc/parser/rbs.rb +239 -235
  83. data/lib/rdoc/parser/rd.rb +17 -13
  84. data/lib/rdoc/parser/ruby.rb +1245 -1124
  85. data/lib/rdoc/parser/ruby_colorizer.rb +263 -213
  86. data/lib/rdoc/parser/simple.rb +31 -27
  87. data/lib/rdoc/parser/text.rb +12 -8
  88. data/lib/rdoc/parser.rb +228 -220
  89. data/lib/rdoc/rbs_helper.rb +1 -1
  90. data/lib/rdoc/rd/inline.rb +57 -53
  91. data/lib/rdoc/rd.rb +90 -88
  92. data/lib/rdoc/rdoc.rb +500 -491
  93. data/lib/rdoc/ri/driver.rb +1140 -1135
  94. data/lib/rdoc/ri/formatter.rb +7 -3
  95. data/lib/rdoc/ri/paths.rb +140 -136
  96. data/lib/rdoc/ri/servlet.rb +354 -350
  97. data/lib/rdoc/ri/store.rb +4 -2
  98. data/lib/rdoc/ri/task.rb +55 -51
  99. data/lib/rdoc/ri.rb +14 -12
  100. data/lib/rdoc/rubygems_hook.rb +183 -181
  101. data/lib/rdoc/server.rb +349 -347
  102. data/lib/rdoc/stats/normal.rb +46 -42
  103. data/lib/rdoc/stats/quiet.rb +39 -35
  104. data/lib/rdoc/stats/verbose.rb +35 -31
  105. data/lib/rdoc/stats.rb +365 -363
  106. data/lib/rdoc/store.rb +888 -902
  107. data/lib/rdoc/task.rb +260 -256
  108. data/lib/rdoc/text.rb +135 -133
  109. data/lib/rdoc/token_stream.rb +101 -93
  110. data/lib/rdoc/tom_doc.rb +203 -201
  111. data/lib/rdoc/version.rb +1 -1
  112. metadata +4 -5
  113. data/lib/rdoc/markdown/literals.kpeg +0 -21
  114. data/lib/rdoc/markdown/literals.rb +0 -454
@@ -3,680 +3,709 @@ require 'cgi/escape'
3
3
  require 'cgi/util' unless defined?(CGI::EscapeExt)
4
4
  require 'prism'
5
5
 
6
- ##
7
- # Outputs RDoc markup as HTML.
8
-
9
- class RDoc::Markup::ToHtml < RDoc::Markup::Formatter
10
-
11
- include RDoc::Text
12
-
13
- # :section: Utilities
14
-
15
- ##
16
- # Maps RDoc::Markup::Parser::LIST_TOKENS types to HTML tags
17
-
18
- LIST_TYPE_TO_HTML = {
19
- :BULLET => ['<ul>', '</ul>'],
20
- :LABEL => ['<dl class="rdoc-list label-list">', '</dl>'],
21
- :LALPHA => ['<ol style="list-style-type: lower-alpha">', '</ol>'],
22
- :NOTE => ['<dl class="rdoc-list note-list">', '</dl>'],
23
- :NUMBER => ['<ol>', '</ol>'],
24
- :UALPHA => ['<ol style="list-style-type: upper-alpha">', '</ol>'],
25
- }
26
-
27
- attr_reader :res # :nodoc:
28
- attr_reader :in_list_entry # :nodoc:
29
- attr_reader :list # :nodoc:
30
-
31
- ##
32
- # The RDoc::CodeObject HTML is being generated for. This is used to
33
- # generate namespaced URI fragments
34
-
35
- attr_accessor :code_object
36
-
37
- ##
38
- # Path to this document for relative links
39
-
40
- attr_accessor :from_path
41
-
42
- # :section:
43
-
44
- # Maps an encoding to a Hash of characters properly transcoded for that
45
- # encoding.
46
- #
47
- # See also encode_fallback.
48
-
49
- TO_HTML_CHARACTERS = Hash.new do |h, encoding|
50
- h[encoding] = {
51
- close_dquote: encode_fallback('”', encoding, '"'),
52
- close_squote: encode_fallback('’', encoding, '\''),
53
- copyright: encode_fallback('©', encoding, '(c)'),
54
- ellipsis: encode_fallback('…', encoding, '...'),
55
- dot_ellipsis: encode_fallback('.…', encoding, '....'),
56
- em_dash: encode_fallback('—', encoding, '---'),
57
- en_dash: encode_fallback('–', encoding, '--'),
58
- open_dquote: encode_fallback('“', encoding, '"'),
59
- open_squote: encode_fallback('‘', encoding, '\''),
60
- trademark: encode_fallback('®', encoding, '(r)'),
61
- }
62
- end
63
-
64
- HTML_CHARACTER_ALIASES = {
65
- '(c)' => :copyright,
66
- '(C)' => :copyright,
67
- '(r)' => :trademark,
68
- '(R)' => :trademark,
69
- '---' => :em_dash,
70
- '--' => :en_dash,
71
- '....' => :dot_ellipsis,
72
- '...' => :ellipsis,
73
- '``' => :open_dquote,
74
- "''" => :close_dquote,
75
- }
76
-
77
- # Transcodes +character+ to +encoding+ with a +fallback+ character.
78
-
79
- def self.encode_fallback(character, encoding, fallback)
80
- character.encode(
81
- encoding,
82
- fallback: { character => fallback },
83
- undef: :replace,
84
- replace: fallback
85
- )
86
- end
6
+ module RDoc
7
+ class Markup
8
+ ##
9
+ # Outputs RDoc markup as HTML.
10
+
11
+ class ToHtml < Markup::Formatter
12
+
13
+ include Text
14
+
15
+ # :section: Utilities
16
+
17
+ ##
18
+ # Maps RDoc::Markup::Parser::LIST_TOKENS types to HTML tags
19
+
20
+ LIST_TYPE_TO_HTML = {
21
+ :BULLET => ['<ul>', '</ul>'],
22
+ :LABEL => ['<dl class="rdoc-list label-list">', '</dl>'],
23
+ :LALPHA => ['<ol style="list-style-type: lower-alpha">', '</ol>'],
24
+ :NOTE => ['<dl class="rdoc-list note-list">', '</dl>'],
25
+ :NUMBER => ['<ol>', '</ol>'],
26
+ :UALPHA => ['<ol style="list-style-type: upper-alpha">', '</ol>'],
27
+ }
28
+
29
+ attr_reader :res # :nodoc:
30
+ attr_reader :in_list_entry # :nodoc:
31
+ attr_reader :list # :nodoc:
32
+
33
+ ##
34
+ # The RDoc::CodeObject HTML is being generated for. This is used to
35
+ # generate namespaced URI fragments
36
+
37
+ attr_accessor :code_object
38
+
39
+ ##
40
+ # Path to this document for relative links
41
+
42
+ attr_accessor :from_path
43
+
44
+ # :section:
45
+
46
+ # Maps an encoding to a Hash of characters properly transcoded for that
47
+ # encoding.
48
+ #
49
+ # See also encode_fallback.
50
+
51
+ TO_HTML_CHARACTERS = Hash.new do |h, encoding|
52
+ h[encoding] = {
53
+ close_dquote: encode_fallback('”', encoding, '"'),
54
+ close_squote: encode_fallback('’', encoding, '\''),
55
+ copyright: encode_fallback('©', encoding, '(c)'),
56
+ ellipsis: encode_fallback('…', encoding, '...'),
57
+ dot_ellipsis: encode_fallback('.…', encoding, '....'),
58
+ em_dash: encode_fallback('—', encoding, '---'),
59
+ en_dash: encode_fallback('–', encoding, '--'),
60
+ open_dquote: encode_fallback('“', encoding, '"'),
61
+ open_squote: encode_fallback('‘', encoding, '\''),
62
+ trademark: encode_fallback('®', encoding, '(r)'),
63
+ }
64
+ end
87
65
 
88
- # Converts ascii quote pairs to multibyte quote characters
89
- class QuoteConverter
66
+ HTML_CHARACTER_ALIASES = {
67
+ '(c)' => :copyright,
68
+ '(C)' => :copyright,
69
+ '(r)' => :trademark,
70
+ '(R)' => :trademark,
71
+ '---' => :em_dash,
72
+ '--' => :en_dash,
73
+ '....' => :dot_ellipsis,
74
+ '...' => :ellipsis,
75
+ '``' => :open_dquote,
76
+ "''" => :close_dquote,
77
+ }
78
+ HTML_CHARACTERS_REGEXP = Regexp.union(HTML_CHARACTER_ALIASES.keys)
79
+
80
+ # Transcodes +character+ to +encoding+ with a +fallback+ character.
81
+
82
+ def self.encode_fallback(character, encoding, fallback)
83
+ character.encode(
84
+ encoding,
85
+ fallback: { character => fallback },
86
+ undef: :replace,
87
+ replace: fallback
88
+ )
89
+ end
90
90
 
91
- def initialize
92
- @in_dquote = false
93
- @in_squote = false
94
- end
91
+ # Converts ascii quote pairs to multibyte quote characters
92
+ class QuoteConverter
95
93
 
96
- def convert(quote, after_word:)
97
- case quote
98
- when '"'
99
- type = @in_dquote ? :close_dquote : :open_dquote
100
- @in_dquote = !@in_dquote
101
- when "'"
102
- if @in_squote
103
- type = :close_squote
94
+ def initialize
95
+ @in_dquote = false
104
96
  @in_squote = false
105
- elsif after_word
106
- # Mary's dog, my parents' house: do not start paired quotes
107
- type = :close_squote
108
- else
109
- type = :open_squote
110
- @in_squote = true
111
97
  end
112
- when '`'
113
- # Opening quote of <tt>`quoted sentence'</tt>.
114
- # This will conflict with code blocks <tt>`puts('hello')`</tt> in the future.
115
- if !@in_squote && !after_word
116
- type = :open_squote
117
- @in_squote = true
98
+
99
+ def convert(quote, after_word:)
100
+ case quote
101
+ when '"'
102
+ type = @in_dquote ? :close_dquote : :open_dquote
103
+ @in_dquote = !@in_dquote
104
+ when "'"
105
+ if @in_squote
106
+ type = :close_squote
107
+ @in_squote = false
108
+ elsif after_word
109
+ # Mary's dog, my parents' house: do not start paired quotes
110
+ type = :close_squote
111
+ else
112
+ type = :open_squote
113
+ @in_squote = true
114
+ end
115
+ when '`'
116
+ # Opening quote of <tt>`quoted sentence'</tt>.
117
+ # This will conflict with code blocks <tt>`puts('hello')`</tt> in the future.
118
+ if !@in_squote && !after_word
119
+ type = :open_squote
120
+ @in_squote = true
121
+ end
122
+ end
123
+ TO_HTML_CHARACTERS[quote.encoding][type] if type
118
124
  end
119
125
  end
120
- TO_HTML_CHARACTERS[quote.encoding][type] if type
121
- end
122
- end
123
126
 
124
- ##
125
- # Creates a new formatter that will output HTML
127
+ ##
128
+ # Creates a new formatter that will output HTML
126
129
 
127
- def initialize(pipe: false, output_decoration: true)
128
- super()
130
+ def initialize(pipe: false, output_decoration: true)
131
+ super()
129
132
 
130
- @pipe = pipe
131
- @output_decoration = output_decoration
132
- @code_object = nil
133
- @from_path = ''
134
- @in_list_entry = nil
135
- @list = nil
136
- @th = nil
137
- @quote_converter = nil
138
- @in_tidylink_label = false
139
- @hard_break = "<br>\n"
140
-
141
- init_regexp_handlings
142
- end
133
+ @pipe = pipe
134
+ @output_decoration = output_decoration
135
+ @code_object = nil
136
+ @from_path = ''
137
+ @in_list_entry = nil
138
+ @list = nil
139
+ @th = nil
140
+ @quote_converter = nil
141
+ @in_tidylink_label = false
142
+ @hard_break = "<br>\n"
143
143
 
144
- # :section: Regexp Handling
145
- #
146
- # These methods are used by regexp handling markup added by RDoc::Markup#add_regexp_handling.
144
+ init_regexp_handlings
145
+ end
147
146
 
148
- # :nodoc:
149
- URL_CHARACTERS_REGEXP_STR = /[A-Za-z0-9\-._~:\/\?#\[\]@!$&'\(\)*+,;%=]/.source
147
+ # :section: Regexp Handling
148
+ #
149
+ # These methods are used by regexp handling markup added by RDoc::Markup#add_regexp_handling.
150
150
 
151
- ##
152
- # Adds regexp handlings.
151
+ ##
152
+ # URL schemes that are not linked because they can run scripts or open
153
+ # local files. Links to them are rendered as their text only.
153
154
 
154
- def init_regexp_handlings
155
- # external links
156
- @markup.add_regexp_handling(/(?:link:|https?:|mailto:|ftp:|irc:|www\.)#{URL_CHARACTERS_REGEXP_STR}+\w/,
157
- :HYPERLINK)
155
+ UNSAFE_URL_SCHEME_REGEXP = /\A(?:javascript|vbscript|data|file):/i
158
156
 
159
- # suppress crossref: \#method \::method \ClassName \method_with_underscores
160
- @markup.add_regexp_handling(/\\(?:[#:A-Z]|[a-z]+_[a-z0-9])/, :SUPPRESSED_CROSSREF)
157
+ # :nodoc:
158
+ URL_CHARACTERS_REGEXP_STR = /[A-Za-z0-9\-._~:\/\?#\[\]@!$&'\(\)*+,;%=]/.source
161
159
 
162
- @markup.add_regexp_handling(Regexp.union(HTML_CHARACTER_ALIASES.keys), :HTML_CHARACTERS)
160
+ ##
161
+ # Adds regexp handlings.
163
162
 
164
- @markup.add_regexp_handling(/\b['"`]/, :QUOTE_AFTER_WORD)
165
- @markup.add_regexp_handling(/\B['"`]/, :QUOTE_NOT_AFTER_WORD)
163
+ def init_regexp_handlings
164
+ # external links
165
+ @markup.add_regexp_handling(/(?:link:|https?:|mailto:|ftp:|irc:|www\.)#{URL_CHARACTERS_REGEXP_STR}+\w/o,
166
+ :HYPERLINK)
166
167
 
167
- init_link_notation_regexp_handlings
168
- end
168
+ # suppress crossref: \#method \::method \ClassName \method_with_underscores
169
+ @markup.add_regexp_handling(/\\(?:[#:A-Z]|[a-z]+_[a-z0-9])/, :SUPPRESSED_CROSSREF)
169
170
 
170
- ##
171
- # Adds regexp handlings about link notations.
171
+ @markup.add_regexp_handling(HTML_CHARACTERS_REGEXP, :HTML_CHARACTERS)
172
172
 
173
- def init_link_notation_regexp_handlings
174
- add_regexp_handling_RDOCLINK
175
- end
173
+ @markup.add_regexp_handling(/\b['"`]/, :QUOTE_AFTER_WORD)
174
+ @markup.add_regexp_handling(/\B['"`]/, :QUOTE_NOT_AFTER_WORD)
176
175
 
177
- def handle_RDOCLINK(url) # :nodoc:
178
- case url
179
- when /^rdoc-ref:/
180
- CGI.escapeHTML($')
181
- when /^rdoc-label:/
182
- return CGI.escapeHTML(url) if in_tidylink_label?
183
- text = $'
184
-
185
- text = case text
186
- when /\Alabel-/ then $'
187
- when /\Afootmark-/ then $'
188
- when /\Afoottext-/ then $'
189
- else text
190
- end
191
-
192
- gen_url CGI.escapeHTML(url), CGI.escapeHTML(text)
193
- when /^rdoc-image:/
194
- # Split the string after "rdoc-image:" into url and alt.
195
- # "path/to/image.jpg:alt text" => ["path/to/image.jpg", "alt text"]
196
- # "http://example.com/path/to/image.jpg:alt text" => ["http://example.com/path/to/image.jpg", "alt text"]
197
- url, alt = $'.split(/:(?!\/)/, 2)
198
- if alt && !alt.empty?
199
- %[<img src="#{CGI.escapeHTML(url)}" alt="#{CGI.escapeHTML(alt)}">]
200
- else
201
- %[<img src="#{CGI.escapeHTML(url)}">]
202
- end
203
- when /\Ardoc-[a-z]+:/
204
- CGI.escapeHTML($')
205
- end
206
- end
176
+ init_link_notation_regexp_handlings
177
+ end
207
178
 
208
- def handle_PLAIN_TEXT(text)
209
- emit_inline(convert_string(text))
210
- end
179
+ ##
180
+ # Adds regexp handlings about link notations.
211
181
 
212
- def handle_REGEXP_HANDLING_TEXT(text)
213
- emit_inline(text)
214
- end
182
+ def init_link_notation_regexp_handlings
183
+ add_regexp_handling_RDOCLINK
184
+ end
215
185
 
216
- def handle_BOLD(nodes)
217
- emit_inline('<strong>')
218
- super
219
- emit_inline('</strong>')
220
- end
186
+ def handle_RDOCLINK(url) # :nodoc:
187
+ case url
188
+ when /^rdoc-ref:/
189
+ CGI.escapeHTML($')
190
+ when /^rdoc-label:/
191
+ return CGI.escapeHTML(url) if in_tidylink_label?
192
+ text = $'
193
+
194
+ text = case text
195
+ when /\Alabel-/ then $'
196
+ when /\Afootmark-/ then $'
197
+ when /\Afoottext-/ then $'
198
+ else text
199
+ end
200
+
201
+ gen_url CGI.escapeHTML(url), CGI.escapeHTML(text)
202
+ when /^rdoc-image:/
203
+ # Split the string after "rdoc-image:" into url and alt.
204
+ # "path/to/image.jpg:alt text" => ["path/to/image.jpg", "alt text"]
205
+ # "http://example.com/path/to/image.jpg:alt text" => ["http://example.com/path/to/image.jpg", "alt text"]
206
+ url, alt = $'.split(/:(?!\/)/, 2)
207
+ if alt && !alt.empty?
208
+ %[<img src="#{CGI.escapeHTML(url)}" alt="#{CGI.escapeHTML(alt)}">]
209
+ else
210
+ %[<img src="#{CGI.escapeHTML(url)}">]
211
+ end
212
+ when /\Ardoc-[a-z]+:/
213
+ CGI.escapeHTML($')
214
+ end
215
+ end
221
216
 
222
- def handle_EM(nodes)
223
- emit_inline('<em>')
224
- super
225
- emit_inline('</em>')
226
- end
217
+ def handle_PLAIN_TEXT(text)
218
+ emit_inline(convert_string(text))
219
+ end
227
220
 
228
- def handle_BOLD_WORD(word)
229
- emit_inline('<strong>')
230
- super
231
- emit_inline('</strong>')
232
- end
221
+ def handle_REGEXP_HANDLING_TEXT(text)
222
+ emit_inline(text)
223
+ end
233
224
 
234
- def handle_EM_WORD(word)
235
- emit_inline('<em>')
236
- super
237
- emit_inline('</em>')
238
- end
225
+ def handle_BOLD(nodes)
226
+ emit_inline('<strong>')
227
+ super
228
+ emit_inline('</strong>')
229
+ end
239
230
 
240
- def handle_TT(code)
241
- emit_inline('<code>')
242
- super
243
- emit_inline('</code>')
244
- end
231
+ def handle_EM(nodes)
232
+ emit_inline('<em>')
233
+ super
234
+ emit_inline('</em>')
235
+ end
245
236
 
246
- def handle_STRIKE(nodes)
247
- emit_inline('<del>')
248
- super
249
- emit_inline('</del>')
250
- end
237
+ def handle_BOLD_WORD(word)
238
+ emit_inline('<strong>')
239
+ super
240
+ emit_inline('</strong>')
241
+ end
251
242
 
252
- def handle_HARD_BREAK
253
- emit_inline('<br>')
254
- end
243
+ def handle_EM_WORD(word)
244
+ emit_inline('<em>')
245
+ super
246
+ emit_inline('</em>')
247
+ end
255
248
 
256
- def emit_inline(text)
257
- @inline_output << text
258
- end
249
+ def handle_TT(code)
250
+ emit_inline('<code>')
251
+ super
252
+ emit_inline('</code>')
253
+ end
259
254
 
260
- # Returns true if we are processing inside a tidy link label.
255
+ def handle_STRIKE(nodes)
256
+ emit_inline('<del>')
257
+ super
258
+ emit_inline('</del>')
259
+ end
261
260
 
262
- def in_tidylink_label?
263
- @in_tidylink_label
264
- end
261
+ def handle_HARD_BREAK
262
+ emit_inline('<br>')
263
+ end
265
264
 
266
- # Special handling for tidy link labels.
267
- # When a tidy link is <tt>{rdoc-image:path/to/image.jpg:alt text}[http://example.com]</tt>,
268
- # label part is normally considered RDOCLINK <tt>rdoc-image:path/to/image.jpg:alt</tt> and a text <tt>" text"</tt>
269
- # but RDoc's test code expects the whole label part to be treated as RDOCLINK only in tidy link label.
270
- # When a tidy link is <tt>{^1}[url]</tt> or <tt>{*1}[url]</tt>, the label part needs to drop leading * or ^.
271
- # TODO: reconsider this workaround.
265
+ def emit_inline(text)
266
+ @inline_output << text
267
+ end
272
268
 
273
- def apply_tidylink_label_special_handling(label, url)
274
- # ^1 *1 will be converted to just 1 in tidy link label.
275
- return label[1..] if label.match?(/\A[*^]\d+\z/)
269
+ # Returns true if we are processing inside a tidy link label.
276
270
 
277
- # rdoc-image in label specially allows spaces in alt text.
278
- return handle_RDOCLINK(label) if label.start_with?('rdoc-image:')
279
- end
271
+ def in_tidylink_label?
272
+ @in_tidylink_label
273
+ end
280
274
 
281
- def handle_TIDYLINK(label_part, url)
282
- # When url is an image, ignore label part (maybe bug?) and just generate img tag.
283
- if url.match?(/\Ahttps?:\/\/.+\.(png|gif|jpg|jpeg|bmp)\z/)
284
- emit_inline("<img src=\"#{CGI.escapeHTML(url)}\" />")
285
- return
286
- elsif url.match?(/\Ardoc-image:/)
287
- emit_inline(handle_RDOCLINK(url))
288
- return
289
- end
275
+ # Special handling for tidy link labels.
276
+ # When a tidy link is <tt>{rdoc-image:path/to/image.jpg:alt text}[http://example.com]</tt>,
277
+ # label part is normally considered RDOCLINK <tt>rdoc-image:path/to/image.jpg:alt</tt> and a text <tt>" text"</tt>
278
+ # but RDoc's test code expects the whole label part to be treated as RDOCLINK only in tidy link label.
279
+ # When a tidy link is <tt>{^1}[url]</tt> or <tt>{*1}[url]</tt>, the label part needs to drop leading * or ^.
280
+ # TODO: reconsider this workaround.
290
281
 
291
- if label_part.size == 1 && String === label_part[0]
292
- raw_label = label_part[0]
282
+ def apply_tidylink_label_special_handling(label, url)
283
+ # ^1 *1 will be converted to just 1 in tidy link label.
284
+ return label[1..] if label.match?(/\A[*^]\d+\z/)
293
285
 
294
- @in_tidylink_label = true
295
- special = apply_tidylink_label_special_handling(raw_label, url)
296
- @in_tidylink_label = false
286
+ # rdoc-image in label specially allows spaces in alt text.
287
+ return handle_RDOCLINK(label) if label.start_with?('rdoc-image:')
288
+ end
297
289
 
298
- if special
299
- tag = gen_url(CGI.escapeHTML(url), special)
300
- unless tag.empty?
301
- emit_inline(tag)
290
+ def handle_TIDYLINK(label_part, url)
291
+ # When url is an image, ignore label part (maybe bug?) and just generate img tag.
292
+ if url.match?(/\Ahttps?:\/\/.+\.(png|gif|jpg|jpeg|bmp)\z/)
293
+ emit_inline("<img src=\"#{CGI.escapeHTML(url)}\" />")
294
+ return
295
+ elsif url.match?(/\Ardoc-image:/)
296
+ emit_inline(handle_RDOCLINK(url))
302
297
  return
303
298
  end
304
- end
305
- end
306
299
 
307
- tag = gen_url(CGI.escapeHTML(url), '')
308
- open_tag, close_tag = tag.split(/(?=<\/a>)/, 2)
309
- valid_tag = open_tag && close_tag
310
- emit_inline(open_tag) if valid_tag
311
- @in_tidylink_label = true
312
- traverse_inline_nodes(label_part)
313
- @in_tidylink_label = false
314
- emit_inline(close_tag) if valid_tag
315
- end
300
+ if label_part.size == 1 && String === label_part[0]
301
+ raw_label = label_part[0]
316
302
 
317
- def handle_inline(text) # :nodoc:
318
- @inline_output = +''
319
- @quote_converter = QuoteConverter.new
320
- super
321
- out = @inline_output
322
- @inline_output = nil
323
- @quote_converter = nil
324
- out
325
- end
303
+ @in_tidylink_label = true
304
+ special = apply_tidylink_label_special_handling(raw_label, url)
305
+ @in_tidylink_label = false
326
306
 
327
- # Converts <tt>(c), (r), --, --- , ..., ...., ``, ''</tt> to HTML characters.
328
- def handle_regexp_HTML_CHARACTERS(text)
329
- name = HTML_CHARACTER_ALIASES[text]
330
- TO_HTML_CHARACTERS[text.encoding][name] if name
331
- end
307
+ if special
308
+ tag = gen_url(CGI.escapeHTML(url), special)
309
+ unless tag.empty?
310
+ emit_inline(tag)
311
+ return
312
+ end
313
+ end
314
+ end
332
315
 
333
- def handle_regexp_QUOTE_NOT_AFTER_WORD(text)
334
- @quote_converter.convert(text, after_word: false) || convert_string(text)
335
- end
316
+ tag = gen_url(CGI.escapeHTML(url), '')
317
+ open_tag, close_tag = tag.split(/(?=<\/a>)/, 2)
318
+ valid_tag = open_tag && close_tag
319
+ emit_inline(open_tag) if valid_tag
320
+ @in_tidylink_label = true
321
+ traverse_inline_nodes(label_part)
322
+ @in_tidylink_label = false
323
+ emit_inline(close_tag) if valid_tag
324
+ end
336
325
 
337
- def handle_regexp_QUOTE_AFTER_WORD(text)
338
- @quote_converter.convert(text, after_word: true) || convert_string(text)
339
- end
326
+ def handle_inline(text) # :nodoc:
327
+ @inline_output = +''
328
+ @quote_converter = QuoteConverter.new
329
+ super
330
+ out = @inline_output
331
+ @inline_output = nil
332
+ @quote_converter = nil
333
+ out
334
+ end
340
335
 
341
- # Converts suppressed cross-reference +text+ to HTML by removing the leading backslash.
336
+ # Converts <tt>(c), (r), --, --- , ..., ...., ``, ''</tt> to HTML characters.
337
+ def handle_regexp_HTML_CHARACTERS(text)
338
+ name = HTML_CHARACTER_ALIASES[text]
339
+ TO_HTML_CHARACTERS[text.encoding][name] if name
340
+ end
342
341
 
343
- def handle_regexp_SUPPRESSED_CROSSREF(text)
344
- convert_string(text.delete_prefix('\\'))
345
- end
342
+ def handle_regexp_QUOTE_NOT_AFTER_WORD(text)
343
+ @quote_converter.convert(text, after_word: false) || convert_string(text)
344
+ end
346
345
 
347
- ##
348
- # +target+ is a potential link. The following schemes are handled:
349
- #
350
- # <tt>mailto:</tt>::
351
- # Inserted as-is.
352
- # <tt>http:</tt>::
353
- # Links are checked to see if they reference an image. If so, that image
354
- # gets inserted using an <tt><img></tt> tag. Otherwise a conventional
355
- # <tt><a href></tt> is used.
356
- # <tt>link:</tt>::
357
- # Reference to a local file relative to the output directory.
358
-
359
- def handle_regexp_HYPERLINK(text)
360
- return convert_string(text) if in_tidylink_label?
361
-
362
- url = CGI.escapeHTML(text)
363
- gen_url url, url
364
- end
346
+ def handle_regexp_QUOTE_AFTER_WORD(text)
347
+ @quote_converter.convert(text, after_word: true) || convert_string(text)
348
+ end
365
349
 
366
- ##
367
- # +target+ is an rdoc-schemed link that will be converted into a hyperlink.
368
- #
369
- # For the +rdoc-ref+ scheme the named reference will be returned without
370
- # creating a link.
371
- #
372
- # For the +rdoc-label+ scheme the footnote and label prefixes are stripped
373
- # when creating a link. All other contents will be linked verbatim.
374
-
375
- def handle_regexp_RDOCLINK(text)
376
- handle_RDOCLINK text
377
- end
350
+ # Converts suppressed cross-reference +text+ to HTML by removing the leading backslash.
378
351
 
379
- # :section: Visitor
380
- #
381
- # These methods implement the HTML visitor.
352
+ def handle_regexp_SUPPRESSED_CROSSREF(text)
353
+ convert_string(text.delete_prefix('\\'))
354
+ end
382
355
 
383
- ##
384
- # Prepares the visitor for HTML generation
356
+ ##
357
+ # +target+ is a potential link. The following schemes are handled:
358
+ #
359
+ # <tt>mailto:</tt>::
360
+ # Inserted as-is.
361
+ # <tt>http:</tt>::
362
+ # Links are checked to see if they reference an image. If so, that image
363
+ # gets inserted using an <tt><img></tt> tag. Otherwise a conventional
364
+ # <tt><a href></tt> is used.
365
+ # <tt>link:</tt>::
366
+ # Reference to a local file relative to the output directory.
367
+
368
+ def handle_regexp_HYPERLINK(text)
369
+ return convert_string(text) if in_tidylink_label?
370
+
371
+ url = CGI.escapeHTML(text)
372
+ gen_url url, url
373
+ end
385
374
 
386
- def start_accepting
387
- @res = []
388
- @in_list_entry = []
389
- @list = []
390
- @heading_ids = {}
391
- end
375
+ ##
376
+ # +target+ is an rdoc-schemed link that will be converted into a hyperlink.
377
+ #
378
+ # For the +rdoc-ref+ scheme the named reference will be returned without
379
+ # creating a link.
380
+ #
381
+ # For the +rdoc-label+ scheme the footnote and label prefixes are stripped
382
+ # when creating a link. All other contents will be linked verbatim.
383
+
384
+ def handle_regexp_RDOCLINK(text)
385
+ handle_RDOCLINK text
386
+ end
392
387
 
393
- ##
394
- # Returns the generated output
388
+ # :section: Visitor
389
+ #
390
+ # These methods implement the HTML visitor.
395
391
 
396
- def end_accepting
397
- @res.join
398
- end
392
+ ##
393
+ # Prepares the visitor for HTML generation
399
394
 
400
- ##
401
- # Adds +block_quote+ to the output
395
+ def start_accepting
396
+ @res = []
397
+ @in_list_entry = []
398
+ @list = []
399
+ @heading_ids = {}
400
+ end
402
401
 
403
- def accept_block_quote(block_quote)
404
- @res << "\n<blockquote>"
402
+ ##
403
+ # Returns the generated output
405
404
 
406
- block_quote.parts.each do |part|
407
- part.accept self
408
- end
405
+ def end_accepting
406
+ @res.join
407
+ end
409
408
 
410
- @res << "</blockquote>\n"
411
- end
409
+ ##
410
+ # Adds +block_quote+ to the output
412
411
 
413
- ##
414
- # Adds +paragraph+ to the output
415
-
416
- def accept_paragraph(paragraph)
417
- @res << "\n<p>"
418
- text = paragraph.text @hard_break
419
- text = text.gsub(/(#{SPACE_SEPARATED_LETTER_CLASS})?\K(?:\r?\n)+(?=(?(1)(#{SPACE_SEPARATED_LETTER_CLASS})?))/o) {
420
- defined?($2) && ' '
421
- }
422
- @res << to_html(text)
423
- @res << "</p>\n"
424
- end
412
+ def accept_block_quote(block_quote)
413
+ @res << "\n<blockquote>"
425
414
 
426
- # Generate syntax highlighted html for ruby-like text.
415
+ block_quote.parts.each do |part|
416
+ part.accept self
417
+ end
427
418
 
428
- def parsable_text_to_html(text)
429
- tokens = RDoc::Parser::RubyColorizer.colorize(text)
430
- result = RDoc::TokenStream.to_html tokens
431
- result = result + "\n" unless result.end_with?("\n")
432
- result
433
- end
419
+ @res << "</blockquote>\n"
420
+ end
434
421
 
435
- ##
436
- # Adds +verbatim+ to the output
437
-
438
- def accept_verbatim(verbatim)
439
- text = verbatim.text.rstrip
440
- format = verbatim.format
441
-
442
- # Apply Ruby syntax highlighting if
443
- # - explicitly marked as Ruby (via ruby? which accepts :ruby or :rb)
444
- # - no format specified but the text is parseable as Ruby
445
- # Otherwise, add language class when applicable and skip Ruby highlighting
446
- if verbatim.ruby? || (format.nil? && parseable?(text))
447
- content = parsable_text_to_html(text)
448
- klass = ' class="ruby"'
449
- else
450
- content = CGI.escapeHTML text
451
- klass = " class=\"#{format}\"" if format
452
- end
422
+ ##
423
+ # Adds +paragraph+ to the output
424
+
425
+ def accept_paragraph(paragraph)
426
+ @res << "\n<p>"
427
+ text = paragraph.text @hard_break
428
+ text = text.gsub(/(#{SPACE_SEPARATED_LETTER_CLASS})?\K(?:\r?\n)+(?=(?(1)(#{SPACE_SEPARATED_LETTER_CLASS})?))/o) {
429
+ defined?($2) && ' '
430
+ }
431
+ @res << to_html(text)
432
+ @res << "</p>\n"
433
+ end
453
434
 
454
- if @pipe
455
- @res << "\n<pre><code>#{CGI.escapeHTML text}\n</code></pre>\n"
456
- else
457
- @res << "\n<pre#{klass}>#{content}</pre>\n"
458
- end
459
- end
435
+ # Generate syntax highlighted html for ruby-like text.
460
436
 
461
- ##
462
- # Adds +rule+ to the output
437
+ def parsable_text_to_html(text)
438
+ tokens = ::RDoc::Parser::RubyColorizer.colorize(text)
439
+ result = TokenStream.to_html tokens
440
+ result = result + "\n" unless result.end_with?("\n")
441
+ result
442
+ end
463
443
 
464
- def accept_rule(rule)
465
- @res << "<hr>\n"
466
- end
444
+ ##
445
+ # Adds +verbatim+ to the output
467
446
 
468
- ##
469
- # Prepares the visitor for consuming +list+
447
+ def accept_verbatim(verbatim)
448
+ text = verbatim.text.rstrip
449
+ format = verbatim.format
470
450
 
471
- def accept_list_start(list)
472
- @list << list.type
473
- @res << html_list_name(list.type, true)
474
- @in_list_entry.push false
475
- end
451
+ # Apply Ruby syntax highlighting if
452
+ # - explicitly marked as Ruby (via ruby? which accepts :ruby or :rb)
453
+ # - no format specified but the text is parseable as Ruby
454
+ # Otherwise, add language class when applicable and skip Ruby highlighting
455
+ if verbatim.ruby? || (format.nil? && parseable?(text))
456
+ content = parsable_text_to_html(text)
457
+ klass = ' class="ruby"'
458
+ else
459
+ content = CGI.escapeHTML text
460
+ klass = " class=\"#{format}\"" if format
461
+ end
476
462
 
477
- ##
478
- # Finishes consumption of +list+
463
+ if @pipe
464
+ @res << "\n<pre><code>#{CGI.escapeHTML text}\n</code></pre>\n"
465
+ else
466
+ @res << "\n<pre#{klass}>#{content}</pre>\n"
467
+ end
468
+ end
479
469
 
480
- def accept_list_end(list)
481
- @list.pop
482
- if tag = @in_list_entry.pop
483
- @res << tag
484
- end
485
- @res << html_list_name(list.type, false) << "\n"
486
- end
470
+ ##
471
+ # Adds +rule+ to the output
487
472
 
488
- ##
489
- # Prepares the visitor for consuming +list_item+
473
+ def accept_rule(rule)
474
+ @res << "<hr>\n"
475
+ end
490
476
 
491
- def accept_list_item_start(list_item)
492
- if tag = @in_list_entry.last
493
- @res << tag
494
- end
477
+ ##
478
+ # Prepares the visitor for consuming +list+
495
479
 
496
- @res << list_item_start(list_item, @list.last)
497
- end
480
+ def accept_list_start(list)
481
+ @list << list.type
482
+ @res << html_list_name(list.type, true)
483
+ @in_list_entry.push false
484
+ end
498
485
 
499
- ##
500
- # Finishes consumption of +list_item+
486
+ ##
487
+ # Finishes consumption of +list+
501
488
 
502
- def accept_list_item_end(list_item)
503
- @in_list_entry[-1] = list_end_for(@list.last)
504
- end
489
+ def accept_list_end(list)
490
+ @list.pop
491
+ if tag = @in_list_entry.pop
492
+ @res << tag
493
+ end
494
+ @res << html_list_name(list.type, false) << "\n"
495
+ end
505
496
 
506
- ##
507
- # Adds +blank_line+ to the output
497
+ ##
498
+ # Prepares the visitor for consuming +list_item+
508
499
 
509
- def accept_blank_line(blank_line)
510
- # @res << annotate("<p />") << "\n"
511
- end
500
+ def accept_list_item_start(list_item)
501
+ if tag = @in_list_entry.last
502
+ @res << tag
503
+ end
512
504
 
513
- ##
514
- # Adds +heading+ to the output. The headings greater than 6 are trimmed to
515
- # level 6.
505
+ @res << list_item_start(list_item, @list.last)
506
+ end
516
507
 
517
- def accept_heading(heading)
518
- level = [6, heading.level].min
508
+ ##
509
+ # Finishes consumption of +list_item+
519
510
 
520
- label = deduplicate_heading_id(heading.label(@code_object))
521
- legacy_label = deduplicate_heading_id(heading.legacy_label(@code_object))
511
+ def accept_list_item_end(list_item)
512
+ @in_list_entry[-1] = list_end_for(@list.last)
513
+ end
522
514
 
523
- # Add legacy anchor before the heading for backward compatibility.
524
- # This allows old links with label- prefix to still work.
525
- if @output_decoration && !@pipe
526
- @res << "\n<span id=\"#{legacy_label}\" class=\"legacy-anchor\"></span>"
527
- end
515
+ ##
516
+ # Adds +blank_line+ to the output
528
517
 
529
- @res << if @output_decoration
530
- "\n<h#{level} id=\"#{label}\">"
531
- else
532
- "\n<h#{level}>"
533
- end
518
+ def accept_blank_line(blank_line)
519
+ # @res << annotate("<p />") << "\n"
520
+ end
534
521
 
535
- if @pipe
536
- @res << to_html(heading.text)
537
- else
538
- @res << "<a href=\"##{label}\">#{to_html(heading.text)}</a>"
539
- end
522
+ ##
523
+ # Adds +heading+ to the output. The headings greater than 6 are trimmed to
524
+ # level 6.
540
525
 
541
- @res << "</h#{level}>\n"
542
- end
526
+ def accept_heading(heading)
527
+ level = [6, heading.level].min
543
528
 
544
- ##
545
- # Adds +raw+ to the output
529
+ label = deduplicate_heading_id(heading.label(@code_object))
530
+ legacy_label = deduplicate_heading_id(heading.legacy_label(@code_object))
546
531
 
547
- def accept_raw(raw)
548
- @res << raw.parts.join("\n")
549
- end
532
+ # Add legacy anchor before the heading for backward compatibility.
533
+ # This allows old links with label- prefix to still work.
534
+ if @output_decoration && !@pipe
535
+ @res << "\n<span id=\"#{legacy_label}\" class=\"legacy-anchor\"></span>"
536
+ end
550
537
 
551
- ##
552
- # Adds +table+ to the output
538
+ @res << if @output_decoration
539
+ "\n<h#{level} id=\"#{label}\">"
540
+ else
541
+ "\n<h#{level}>"
542
+ end
553
543
 
554
- def accept_table(header, body, aligns)
555
- @res << "\n<table role=\"table\">\n<thead>\n<tr>\n"
556
- header.zip(aligns) do |text, align|
557
- @res << '<th'
558
- @res << ' align="' << align << '"' if align
559
- @res << '>' << to_html(text) << "</th>\n"
560
- end
561
- @res << "</tr>\n</thead>\n<tbody>\n"
562
- body.each do |row|
563
- @res << "<tr>\n"
564
- row.zip(aligns) do |text, align|
565
- @res << '<td'
566
- @res << ' align="' << align << '"' if align
567
- @res << '>' << to_html(text) << "</td>\n"
568
- end
569
- @res << "</tr>\n"
570
- end
571
- @res << "</tbody>\n</table>\n"
572
- end
544
+ if @pipe
545
+ @res << to_html(heading.text)
546
+ else
547
+ @res << "<a href=\"##{label}\">#{to_html(heading.text)}</a>"
548
+ end
573
549
 
574
- # :section: Utilities
550
+ @res << "</h#{level}>\n"
551
+ end
575
552
 
576
- ##
577
- # Returns a unique heading ID, appending -1, -2, etc. for duplicates.
578
- # Matches GitHub's behavior for duplicate heading anchors.
553
+ ##
554
+ # Adds +raw+ to the output
579
555
 
580
- def deduplicate_heading_id(id)
581
- if @heading_ids.key?(id)
582
- @heading_ids[id] += 1
583
- "#{id}-#{@heading_ids[id]}"
584
- else
585
- @heading_ids[id] = 0
586
- id
587
- end
588
- end
556
+ def accept_raw(raw)
557
+ @res << raw.parts.join("\n")
558
+ end
589
559
 
590
- ##
591
- # CGI-escapes +text+
560
+ ##
561
+ # Adds +table+ to the output
592
562
 
593
- def convert_string(text)
594
- CGI.escapeHTML text
595
- end
563
+ def accept_table(header, body, aligns)
564
+ @res << "\n<table role=\"table\">\n<thead>\n<tr>\n"
565
+ header.zip(aligns) do |text, align|
566
+ @res << '<th'
567
+ @res << ' align="' << align << '"' if align
568
+ @res << '>' << to_html(text) << "</th>\n"
569
+ end
570
+ @res << "</tr>\n</thead>\n<tbody>\n"
571
+ body.each do |row|
572
+ @res << "<tr>\n"
573
+ row.zip(aligns) do |text, align|
574
+ @res << '<td'
575
+ @res << ' align="' << align << '"' if align
576
+ @res << '>' << to_html(text) << "</td>\n"
577
+ end
578
+ @res << "</tr>\n"
579
+ end
580
+ @res << "</tbody>\n</table>\n"
581
+ end
596
582
 
597
- ##
598
- # Generates an HTML link or image tag for the given +url+ and +text+.
599
- #
600
- # - Image URLs (http/https/link ending in .gif, .png, .jpg, .jpeg, .bmp)
601
- # become <img> tags
602
- # - File references (.rb, .rdoc, .md) are converted to .html paths
603
- # - Anchor URLs (#foo) pass through unchanged for GitHub-style header linking
604
- # - Footnote links get wrapped in <sup> tags
583
+ # :section: Utilities
605
584
 
606
- def gen_url(url, text)
607
- scheme, url, id = parse_url url
585
+ ##
586
+ # Returns a unique heading ID, appending -1, -2, etc. for duplicates.
587
+ # Matches GitHub's behavior for duplicate heading anchors.
608
588
 
609
- if %w[http https link].include?(scheme) && url =~ /\.(gif|png|jpg|jpeg|bmp)\z/
610
- "<img src=\"#{url}\" />"
611
- else
612
- if scheme != 'link' and %r%\A((?!https?:)(?:[^/#]*/)*+)([^/#]+)\.(rb|rdoc|md)(?=\z|#)%i =~ url
613
- url = "#$1#{$2.tr('.', '_')}_#$3.html#$'"
589
+ def deduplicate_heading_id(id)
590
+ if @heading_ids.key?(id)
591
+ @heading_ids[id] += 1
592
+ "#{id}-#{@heading_ids[id]}"
593
+ else
594
+ @heading_ids[id] = 0
595
+ id
596
+ end
614
597
  end
615
598
 
616
- text = text.sub %r%^#{scheme}:/*%i, ''
617
- text = text.sub %r%^[*\^](\d+)$%, '\1'
599
+ ##
600
+ # CGI-escapes +text+
601
+
602
+ def convert_string(text)
603
+ CGI.escapeHTML text
604
+ end
605
+
606
+ ##
607
+ # Generates an HTML link or image tag for the given +url+ and +text+.
608
+ #
609
+ # - Image URLs (http/https/link ending in .gif, .png, .jpg, .jpeg, .bmp)
610
+ # become <img> tags
611
+ # - File references (.rb, .rdoc, .md) are converted to .html paths
612
+ # - Anchor URLs (#foo) pass through unchanged for GitHub-style header linking
613
+ # - Footnote links get wrapped in <sup> tags
614
+ # - URLs with an unsafe scheme (javascript:, vbscript:, data:, file:) are not
615
+ # linked and only +text+ is returned
616
+
617
+ def gen_url(url, text)
618
+ scheme, url, id = parse_url url
619
+
620
+ # Check the parsed URL because link: URLs are used as href without it
621
+ return text if unsafe_url?(url)
622
+
623
+ if %w[http https link].include?(scheme) && url =~ /\.(gif|png|jpg|jpeg|bmp)\z/
624
+ "<img src=\"#{url}\" />"
625
+ else
626
+ if scheme != 'link' and %r%\A((?!https?:)(?:[^/#]*/)*+)([^/#]+)\.(rb|rdoc|md)(?=\z|#)%i =~ url
627
+ url = "#$1#{$2.tr('.', '_')}_#$3.html#$'"
628
+ end
618
629
 
619
- link = "<a#{id} href=\"#{url}\">#{text}</a>"
630
+ text = text.sub %r%^#{scheme}:/*%i, ''
631
+ text = text.sub %r%^[*\^](\d+)$%, '\1'
620
632
 
621
- if /"foot/.match?(id)
622
- "<sup>#{link}</sup>"
623
- else
624
- link
633
+ link = "<a#{id} href=\"#{url}\">#{text}</a>"
634
+
635
+ if /"foot/.match?(id)
636
+ "<sup>#{link}</sup>"
637
+ else
638
+ link
639
+ end
640
+ end
625
641
  end
626
- end
627
- end
628
642
 
629
- ##
630
- # Determines the HTML list element for +list_type+ and +open_tag+
643
+ ##
644
+ # Does +url+ have a scheme in UNSAFE_URL_SCHEME_REGEXP?
645
+ #
646
+ # Browsers ignore leading C0 controls and spaces and all ASCII tabs and
647
+ # newlines in a URL, so they are removed before checking the scheme.
648
+ # See https://url.spec.whatwg.org/#concept-basic-url-parser
649
+ #
650
+ #: (String) -> bool
651
+ def unsafe_url?(url)
652
+ normalized = url.sub(/\A[\x00-\x20]+/, '').delete("\t\n\r")
653
+ UNSAFE_URL_SCHEME_REGEXP.match?(normalized)
654
+ end
631
655
 
632
- def html_list_name(list_type, open_tag)
633
- tags = LIST_TYPE_TO_HTML[list_type]
634
- raise RDoc::Error, "Invalid list type: #{list_type.inspect}" unless tags
635
- tags[open_tag ? 0 : 1]
636
- end
656
+ ##
657
+ # Determines the HTML list element for +list_type+ and +open_tag+
637
658
 
638
- ##
639
- # Returns the HTML tag for +list_type+, possible using a label from
640
- # +list_item+
641
-
642
- def list_item_start(list_item, list_type)
643
- case list_type
644
- when :BULLET, :LALPHA, :NUMBER, :UALPHA then
645
- "<li>"
646
- when :LABEL, :NOTE then
647
- Array(list_item.label).map do |label|
648
- "<dt>#{to_html label}</dt>\n"
649
- end.join << "<dd>"
650
- else
651
- raise RDoc::Error, "Invalid list type: #{list_type.inspect}"
652
- end
653
- end
659
+ def html_list_name(list_type, open_tag)
660
+ tags = LIST_TYPE_TO_HTML[list_type]
661
+ raise Error, "Invalid list type: #{list_type.inspect}" unless tags
662
+ tags[open_tag ? 0 : 1]
663
+ end
654
664
 
655
- ##
656
- # Returns the HTML end-tag for +list_type+
657
-
658
- def list_end_for(list_type)
659
- case list_type
660
- when :BULLET, :LALPHA, :NUMBER, :UALPHA then
661
- "</li>"
662
- when :LABEL, :NOTE then
663
- "</dd>"
664
- else
665
- raise RDoc::Error, "Invalid list type: #{list_type.inspect}"
666
- end
667
- end
665
+ ##
666
+ # Returns the HTML tag for +list_type+, possible using a label from
667
+ # +list_item+
668
+
669
+ def list_item_start(list_item, list_type)
670
+ case list_type
671
+ when :BULLET, :LALPHA, :NUMBER, :UALPHA
672
+ "<li>"
673
+ when :LABEL, :NOTE
674
+ Array(list_item.label).map do |label|
675
+ "<dt>#{to_html label}</dt>\n"
676
+ end.join << "<dd>"
677
+ else
678
+ raise Error, "Invalid list type: #{list_type.inspect}"
679
+ end
680
+ end
668
681
 
669
- ##
670
- # Returns true if text is valid ruby syntax
682
+ ##
683
+ # Returns the HTML end-tag for +list_type+
671
684
 
672
- def parseable?(text)
673
- Prism.parse_success?(text)
674
- end
685
+ def list_end_for(list_type)
686
+ case list_type
687
+ when :BULLET, :LALPHA, :NUMBER, :UALPHA
688
+ "</li>"
689
+ when :LABEL, :NOTE
690
+ "</dd>"
691
+ else
692
+ raise Error, "Invalid list type: #{list_type.inspect}"
693
+ end
694
+ end
675
695
 
676
- ##
677
- # Converts +item+ to HTML using RDoc::Text#to_html
696
+ ##
697
+ # Returns true if text is valid ruby syntax
678
698
 
679
- def to_html(item)
680
- handle_inline(item)
699
+ def parseable?(text)
700
+ Prism.parse_success?(text)
701
+ end
702
+
703
+ ##
704
+ # Converts +item+ to HTML using RDoc::Text#to_html
705
+
706
+ def to_html(item)
707
+ handle_inline(item)
708
+ end
709
+ end
681
710
  end
682
711
  end