Imported File#ftype spec from rubyspecs.
[rbx.git] / lib / rdoc / markup / to_html.rb
blob3c08d7bf6ad79575151d8d7a8b08c02f3b4b79b5
1 require 'rdoc/markup/formatter'
2 require 'rdoc/markup/fragments'
3 require 'rdoc/markup/inline'
4 require 'rdoc/generator'
6 require 'cgi'
8 class RDoc::Markup::ToHtml < RDoc::Markup::Formatter
10   LIST_TYPE_TO_HTML = {
11     :BULLET =>     %w[<ul> </ul>],
12     :NUMBER =>     %w[<ol> </ol>],
13     :UPPERALPHA => %w[<ol> </ol>],
14     :LOWERALPHA => %w[<ol> </ol>],
15     :LABELED =>    %w[<dl> </dl>],
16     :NOTE    =>    %w[<table> </table>],
17   }
19   InlineTag = Struct.new(:bit, :on, :off)
21   def initialize
22     super
24     # external hyperlinks
25     @markup.add_special(/((link:|https?:|mailto:|ftp:|www\.)\S+\w)/, :HYPERLINK)
27     # and links of the form  <text>[<url>]
28     @markup.add_special(/(((\{.*?\})|\b\S+?)\[\S+?\.\S+?\])/, :TIDYLINK)
30     init_tags
31   end
33   ##
34   # Generate a hyperlink for url, labeled with text. Handle the
35   # special cases for img: and link: described under handle_special_HYPEDLINK
37   def gen_url(url, text)
38     if url =~ /([A-Za-z]+):(.*)/ then
39       type = $1
40       path = $2
41     else
42       type = "http"
43       path = url
44       url  = "http://#{url}"
45     end
47     if type == "link" then
48       url = if path[0, 1] == '#' then # is this meaningful?
49               path
50             else
51               RDoc::Generator.gen_url @from_path, path
52             end
53     end
55     if (type == "http" or type == "link") and
56        url =~ /\.(gif|png|jpg|jpeg|bmp)$/ then
57       "<img src=\"#{url}\" />"
58     else
59       "<a href=\"#{url}\">#{text.sub(%r{^#{type}:/*}, '')}</a>"
60     end
61   end
63   ##
64   # And we're invoked with a potential external hyperlink mailto:
65   # just gets inserted. http: links are checked to see if they
66   # reference an image. If so, that image gets inserted using an
67   # <img> tag. Otherwise a conventional <a href> is used.  We also
68   # support a special type of hyperlink, link:, which is a reference
69   # to a local file whose path is relative to the --op directory.
71   def handle_special_HYPERLINK(special)
72     url = special.text
73     gen_url url, url
74   end
76   ##
77   # Here's a hypedlink where the label is different to the URL
78   #  <label>[url] or {long label}[url]
80   def handle_special_TIDYLINK(special)
81     text = special.text
83     return text unless text =~ /\{(.*?)\}\[(.*?)\]/ or text =~ /(\S+)\[(.*?)\]/
85     label = $1
86     url   = $2
87     gen_url url, label
88   end
90   ##
91   # Set up the standard mapping of attributes to HTML tags
93   def init_tags
94     @attr_tags = [
95       InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:BOLD), "<b>", "</b>"),
96       InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:TT),   "<tt>", "</tt>"),
97       InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:EM),   "<em>", "</em>"),
98     ]
99   end
101   ##
102   # Add a new set of HTML tags for an attribute. We allow separate start and
103   # end tags for flexibility.
105   def add_tag(name, start, stop)
106     @attr_tags << InlineTag.new(RDoc::Markup::Attribute.bitmap_for(name), start, stop)
107   end
109   ##
110   # Given an HTML tag, decorate it with class information and the like if
111   # required. This is a no-op in the base class, but is overridden in HTML
112   # output classes that implement style sheets.
114   def annotate(tag)
115     tag
116   end
118   ##
119   # Here's the client side of the visitor pattern
121   def start_accepting
122     @res = ""
123     @in_list_entry = []
124   end
126   def end_accepting
127     @res
128   end
130   def accept_paragraph(am, fragment)
131     @res << annotate("<p>") + "\n"
132     @res << wrap(convert_flow(am.flow(fragment.txt)))
133     @res << annotate("</p>") + "\n"
134   end
136   def accept_verbatim(am, fragment)
137     @res << annotate("<pre>") + "\n"
138     @res << CGI.escapeHTML(fragment.txt)
139     @res << annotate("</pre>") << "\n"
140   end
142   def accept_rule(am, fragment)
143     size = fragment.param
144     size = 10 if size > 10
145     @res << "<hr size=\"#{size}\"></hr>"
146   end
148   def accept_list_start(am, fragment)
149     @res << html_list_name(fragment.type, true) << "\n"
150     @in_list_entry.push false
151   end
153   def accept_list_end(am, fragment)
154     if tag = @in_list_entry.pop
155       @res << annotate(tag) << "\n"
156     end
157     @res << html_list_name(fragment.type, false) << "\n"
158   end
160   def accept_list_item(am, fragment)
161     if tag = @in_list_entry.last
162       @res << annotate(tag) << "\n"
163     end
165     @res << list_item_start(am, fragment)
167     @res << wrap(convert_flow(am.flow(fragment.txt))) << "\n"
169     @in_list_entry[-1] = list_end_for(fragment.type)
170   end
172   def accept_blank_line(am, fragment)
173     # @res << annotate("<p />") << "\n"
174   end
176   def accept_heading(am, fragment)
177     @res << convert_heading(fragment.head_level, am.flow(fragment.txt))
178   end
180   ##
181   # This is a higher speed (if messier) version of wrap
183   def wrap(txt, line_len = 76)
184     res = ""
185     sp = 0
186     ep = txt.length
187     while sp < ep
188       # scan back for a space
189       p = sp + line_len - 1
190       if p >= ep
191         p = ep
192       else
193         while p > sp and txt[p] != ?\s
194           p -= 1
195         end
196         if p <= sp
197           p = sp + line_len
198           while p < ep and txt[p] != ?\s
199             p += 1
200           end
201         end
202       end
203       res << txt[sp...p] << "\n"
204       sp = p
205       sp += 1 while sp < ep and txt[sp] == ?\s
206     end
207     res
208   end
210   private
212   def on_tags(res, item)
213     attr_mask = item.turn_on
214     return if attr_mask.zero?
216     @attr_tags.each do |tag|
217       if attr_mask & tag.bit != 0
218         res << annotate(tag.on)
219       end
220     end
221   end
223   def off_tags(res, item)
224     attr_mask = item.turn_off
225     return if attr_mask.zero?
227     @attr_tags.reverse_each do |tag|
228       if attr_mask & tag.bit != 0
229         res << annotate(tag.off)
230       end
231     end
232   end
234   def convert_flow(flow)
235     res = ""
237     flow.each do |item|
238       case item
239       when String
240         res << convert_string(item)
241       when RDoc::Markup::AttrChanger
242         off_tags(res, item)
243         on_tags(res,  item)
244       when RDoc::Markup::Special
245         res << convert_special(item)
246       else
247         raise "Unknown flow element: #{item.inspect}"
248       end
249     end
251     res
252   end
254   ##
255   # some of these patterns are taken from SmartyPants...
257   def convert_string(item)
258     CGI.escapeHTML(item).
260     # convert -- to em-dash, (-- to en-dash)
261       gsub(/---?/, '&#8212;'). #gsub(/--/, '&#8211;').
263     # convert ... to elipsis (and make sure .... becomes .<elipsis>)
264       gsub(/\.\.\.\./, '.&#8230;').gsub(/\.\.\./, '&#8230;').
266     # convert single closing quote
267       gsub(%r{([^ \t\r\n\[\{\(])\'}, '\1&#8217;').
268       gsub(%r{\'(?=\W|s\b)}, '&#8217;').
270     # convert single opening quote
271       gsub(/'/, '&#8216;').
273     # convert double closing quote
274       gsub(%r{([^ \t\r\n\[\{\(])\'(?=\W)}, '\1&#8221;').
276     # convert double opening quote
277       gsub(/'/, '&#8220;').
279     # convert copyright
280       gsub(/\(c\)/, '&#169;').
282     # convert and registered trademark
283       gsub(/\(r\)/, '&#174;')
285   end
287   def convert_special(special)
288     handled = false
289     RDoc::Markup::Attribute.each_name_of(special.type) do |name|
290       method_name = "handle_special_#{name}"
291       if self.respond_to? method_name
292         special.text = send(method_name, special)
293         handled = true
294       end
295     end
296     raise "Unhandled special: #{special}" unless handled
297     special.text
298   end
300   def convert_heading(level, flow)
301     res =
302       annotate("<h#{level}>") +
303       convert_flow(flow) +
304       annotate("</h#{level}>\n")
305   end
307   def html_list_name(list_type, is_open_tag)
308     tags = LIST_TYPE_TO_HTML[list_type] || raise("Invalid list type: #{list_type.inspect}")
309     annotate(tags[ is_open_tag ? 0 : 1])
310   end
312   def list_item_start(am, fragment)
313     case fragment.type
314     when :BULLET, :NUMBER then
315       annotate("<li>")
317     when :UPPERALPHA then
318       annotate("<li type=\"A\">")
320     when :LOWERALPHA then
321       annotate("<li type=\"a\">")
323     when :LABELED then
324       annotate("<dt>") +
325         convert_flow(am.flow(fragment.param)) +
326         annotate("</dt>") +
327         annotate("<dd>")
329     when :NOTE then
330       annotate("<tr>") +
331         annotate("<td valign=\"top\">") +
332         convert_flow(am.flow(fragment.param)) +
333         annotate("</td>") +
334         annotate("<td>")
335     else
336       raise "Invalid list type"
337     end
338   end
340   def list_end_for(fragment_type)
341     case fragment_type
342     when :BULLET, :NUMBER, :UPPERALPHA, :LOWERALPHA then
343       "</li>"
344     when :LABELED then
345       "</dd>"
346     when :NOTE then
347       "</td></tr>"
348     else
349       raise "Invalid list type"
350     end
351   end