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