1# frozen_string_literal: true
2require 'cgi'
3
4##
5# Outputs RDoc markup as HTML.
6
7class RDoc::Markup::ToHtml < RDoc::Markup::Formatter
8
9  include RDoc::Text
10
11  # :section: Utilities
12
13  ##
14  # Maps RDoc::Markup::Parser::LIST_TOKENS types to HTML tags
15
16  LIST_TYPE_TO_HTML = {
17    :BULLET => ['<ul>',                                      '</ul>'],
18    :LABEL  => ['<dl class="rdoc-list label-list">',         '</dl>'],
19    :LALPHA => ['<ol style="list-style-type: lower-alpha">', '</ol>'],
20    :NOTE   => ['<dl class="rdoc-list note-list">',          '</dl>'],
21    :NUMBER => ['<ol>',                                      '</ol>'],
22    :UALPHA => ['<ol style="list-style-type: upper-alpha">', '</ol>'],
23  }
24
25  attr_reader :res # :nodoc:
26  attr_reader :in_list_entry # :nodoc:
27  attr_reader :list # :nodoc:
28
29  ##
30  # The RDoc::CodeObject HTML is being generated for.  This is used to
31  # generate namespaced URI fragments
32
33  attr_accessor :code_object
34
35  ##
36  # Path to this document for relative links
37
38  attr_accessor :from_path
39
40  # :section:
41
42  ##
43  # Creates a new formatter that will output HTML
44
45  def initialize options, markup = nil
46    super
47
48    @code_object = nil
49    @from_path = ''
50    @in_list_entry = nil
51    @list = nil
52    @th = nil
53    @hard_break = "<br>\n"
54
55    # external links
56    @markup.add_regexp_handling(/(?:link:|https?:|mailto:|ftp:|irc:|www\.)\S+\w/,
57                                :HYPERLINK)
58
59    add_regexp_handling_RDOCLINK
60    add_regexp_handling_TIDYLINK
61
62    init_tags
63  end
64
65  # :section: Regexp Handling
66  #
67  # These methods are used by regexp handling markup added by RDoc::Markup#add_regexp_handling.
68
69  def handle_RDOCLINK url # :nodoc:
70    case url
71    when /^rdoc-ref:/
72      $'
73    when /^rdoc-label:/
74      text = $'
75
76      text = case text
77             when /\Alabel-/    then $'
78             when /\Afootmark-/ then $'
79             when /\Afoottext-/ then $'
80             else                    text
81             end
82
83      gen_url url, text
84    when /^rdoc-image:/
85      "<img src=\"#{$'}\">"
86    else
87      url =~ /\Ardoc-[a-z]+:/
88
89      $'
90    end
91  end
92
93  ##
94  # +target+ is a <code><br></code>
95
96  def handle_regexp_HARD_BREAK target
97    '<br>'
98  end
99
100  ##
101  # +target+ is a potential link.  The following schemes are handled:
102  #
103  # <tt>mailto:</tt>::
104  #   Inserted as-is.
105  # <tt>http:</tt>::
106  #   Links are checked to see if they reference an image. If so, that image
107  #   gets inserted using an <tt><img></tt> tag. Otherwise a conventional
108  #   <tt><a href></tt> is used.
109  # <tt>link:</tt>::
110  #   Reference to a local file relative to the output directory.
111
112  def handle_regexp_HYPERLINK(target)
113    url = target.text
114
115    gen_url url, url
116  end
117
118  ##
119  # +target+ is an rdoc-schemed link that will be converted into a hyperlink.
120  #
121  # For the +rdoc-ref+ scheme the named reference will be returned without
122  # creating a link.
123  #
124  # For the +rdoc-label+ scheme the footnote and label prefixes are stripped
125  # when creating a link.  All other contents will be linked verbatim.
126
127  def handle_regexp_RDOCLINK target
128    handle_RDOCLINK target.text
129  end
130
131  ##
132  # This +target+ is a link where the label is different from the URL
133  # <tt>label[url]</tt> or <tt>{long label}[url]</tt>
134
135  def handle_regexp_TIDYLINK(target)
136    text = target.text
137
138    return text unless
139      text =~ /^\{(.*)\}\[(.*?)\]$/ or text =~ /^(\S+)\[(.*?)\]$/
140
141    label = $1
142    url   = $2
143
144    label = handle_RDOCLINK label if /^rdoc-image:/ =~ label
145
146    gen_url url, label
147  end
148
149  # :section: Visitor
150  #
151  # These methods implement the HTML visitor.
152
153  ##
154  # Prepares the visitor for HTML generation
155
156  def start_accepting
157    @res = []
158    @in_list_entry = []
159    @list = []
160  end
161
162  ##
163  # Returns the generated output
164
165  def end_accepting
166    @res.join
167  end
168
169  ##
170  # Adds +block_quote+ to the output
171
172  def accept_block_quote block_quote
173    @res << "\n<blockquote>"
174
175    block_quote.parts.each do |part|
176      part.accept self
177    end
178
179    @res << "</blockquote>\n"
180  end
181
182  ##
183  # Adds +paragraph+ to the output
184
185  def accept_paragraph paragraph
186    @res << "\n<p>"
187    text = paragraph.text @hard_break
188    text = text.gsub(/\r?\n/, ' ')
189    @res << to_html(text)
190    @res << "</p>\n"
191  end
192
193  ##
194  # Adds +verbatim+ to the output
195
196  def accept_verbatim verbatim
197    text = verbatim.text.rstrip
198
199    klass = nil
200
201    content = if verbatim.ruby? or parseable? text then
202                begin
203                  tokens = RDoc::Parser::RipperStateLex.parse text
204                  klass  = ' class="ruby"'
205
206                  result = RDoc::TokenStream.to_html tokens
207                  result = result + "\n" unless "\n" == result[-1]
208                  result
209                rescue
210                  CGI.escapeHTML text
211                end
212              else
213                CGI.escapeHTML text
214              end
215
216    if @options.pipe then
217      @res << "\n<pre><code>#{CGI.escapeHTML text}\n</code></pre>\n"
218    else
219      @res << "\n<pre#{klass}>#{content}</pre>\n"
220    end
221  end
222
223  ##
224  # Adds +rule+ to the output
225
226  def accept_rule rule
227    @res << "<hr>\n"
228  end
229
230  ##
231  # Prepares the visitor for consuming +list+
232
233  def accept_list_start(list)
234    @list << list.type
235    @res << html_list_name(list.type, true)
236    @in_list_entry.push false
237  end
238
239  ##
240  # Finishes consumption of +list+
241
242  def accept_list_end(list)
243    @list.pop
244    if tag = @in_list_entry.pop
245      @res << tag
246    end
247    @res << html_list_name(list.type, false) << "\n"
248  end
249
250  ##
251  # Prepares the visitor for consuming +list_item+
252
253  def accept_list_item_start(list_item)
254    if tag = @in_list_entry.last
255      @res << tag
256    end
257
258    @res << list_item_start(list_item, @list.last)
259  end
260
261  ##
262  # Finishes consumption of +list_item+
263
264  def accept_list_item_end(list_item)
265    @in_list_entry[-1] = list_end_for(@list.last)
266  end
267
268  ##
269  # Adds +blank_line+ to the output
270
271  def accept_blank_line(blank_line)
272    # @res << annotate("<p />") << "\n"
273  end
274
275  ##
276  # Adds +heading+ to the output.  The headings greater than 6 are trimmed to
277  # level 6.
278
279  def accept_heading heading
280    level = [6, heading.level].min
281
282    label = heading.label @code_object
283
284    @res << if @options.output_decoration
285              "\n<h#{level} id=\"#{label}\">"
286            else
287              "\n<h#{level}>"
288            end
289    @res << to_html(heading.text)
290    unless @options.pipe then
291      @res << "<span><a href=\"##{label}\">&para;</a>"
292      @res << " <a href=\"#top\">&uarr;</a></span>"
293    end
294    @res << "</h#{level}>\n"
295  end
296
297  ##
298  # Adds +raw+ to the output
299
300  def accept_raw raw
301    @res << raw.parts.join("\n")
302  end
303
304  # :section: Utilities
305
306  ##
307  # CGI-escapes +text+
308
309  def convert_string(text)
310    CGI.escapeHTML text
311  end
312
313  ##
314  # Generate a link to +url+ with content +text+.  Handles the special cases
315  # for img: and link: described under handle_regexp_HYPERLINK
316
317  def gen_url url, text
318    scheme, url, id = parse_url url
319
320    if %w[http https link].include?(scheme) and
321       url =~ /\.(gif|png|jpg|jpeg|bmp)$/ then
322      "<img src=\"#{url}\" />"
323    else
324      text = text.sub %r%^#{scheme}:/*%i, ''
325      text = text.sub %r%^[*\^](\d+)$%,   '\1'
326
327      link = "<a#{id} href=\"#{url}\">#{text}</a>"
328
329      link = "<sup>#{link}</sup>" if /"foot/ =~ id
330
331      link
332    end
333  end
334
335  ##
336  # Determines the HTML list element for +list_type+ and +open_tag+
337
338  def html_list_name(list_type, open_tag)
339    tags = LIST_TYPE_TO_HTML[list_type]
340    raise RDoc::Error, "Invalid list type: #{list_type.inspect}" unless tags
341    tags[open_tag ? 0 : 1]
342  end
343
344  ##
345  # Maps attributes to HTML tags
346
347  def init_tags
348    add_tag :BOLD, "<strong>", "</strong>"
349    add_tag :TT,   "<code>",   "</code>"
350    add_tag :EM,   "<em>",     "</em>"
351  end
352
353  ##
354  # Returns the HTML tag for +list_type+, possible using a label from
355  # +list_item+
356
357  def list_item_start(list_item, list_type)
358    case list_type
359    when :BULLET, :LALPHA, :NUMBER, :UALPHA then
360      "<li>"
361    when :LABEL, :NOTE then
362      Array(list_item.label).map do |label|
363        "<dt>#{to_html label}\n"
364      end.join << "<dd>"
365    else
366      raise RDoc::Error, "Invalid list type: #{list_type.inspect}"
367    end
368  end
369
370  ##
371  # Returns the HTML end-tag for +list_type+
372
373  def list_end_for(list_type)
374    case list_type
375    when :BULLET, :LALPHA, :NUMBER, :UALPHA then
376      "</li>"
377    when :LABEL, :NOTE then
378      "</dd>"
379    else
380      raise RDoc::Error, "Invalid list type: #{list_type.inspect}"
381    end
382  end
383
384  ##
385  # Returns true if text is valid ruby syntax
386
387  def parseable? text
388    verbose, $VERBOSE = $VERBOSE, nil
389    eval("BEGIN {return true}\n#{text}")
390  rescue SyntaxError
391    false
392  ensure
393    $VERBOSE = verbose
394  end
395
396  ##
397  # Converts +item+ to HTML using RDoc::Text#to_html
398
399  def to_html item
400    super convert_flow @am.flow item
401  end
402
403end
404
405