1 require 'rdoc/markup/formatter'
2 require 'rdoc/markup/fragments'
3 require 'rdoc/markup/inline'
7 class RDoc::Markup::ToHtml < RDoc::Markup::Formatter
10 :BULLET => %w[<ul> </ul>],
11 :NUMBER => %w[<ol> </ol>],
12 :UPPERALPHA => %w[<ol> </ol>],
13 :LOWERALPHA => %w[<ol> </ol>],
14 :LABELED => %w[<dl> </dl>],
15 :NOTE => %w[<table> </table>],
18 InlineTag = Struct.new(:bit, :on, :off)
23 # @in_tt - tt nested levels count
26 @tt_bit = RDoc::Markup::Attribute.bitmap_for :TT
29 @markup.add_special(/((link:|https?:|mailto:|ftp:|www\.)\S+\w)/, :HYPERLINK)
31 # and links of the form <text>[<url>]
32 @markup.add_special(/(((\{.*?\})|\b\S+?)\[\S+?\.\S+?\])/, :TIDYLINK)
38 # Converts a target url to one that is relative to a given path
40 def self.gen_relative_url(path, target)
41 from = File.dirname path
42 to, to_file = File.split target
47 while from.size > 0 and to.size > 0 and from[0] == to[0] do
59 # Generate a hyperlink for url, labeled with text. Handle the
60 # special cases for img: and link: described under handle_special_HYPERLINK
62 def gen_url(url, text)
63 if url =~ /([A-Za-z]+):(.*)/ then
72 if type == "link" then
73 url = if path[0, 1] == '#' then # is this meaningful?
76 self.class.gen_relative_url @from_path, path
80 if (type == "http" or type == "link") and
81 url =~ /\.(gif|png|jpg|jpeg|bmp)$/ then
82 "<img src=\"#{url}\" />"
84 "<a href=\"#{url}\">#{text.sub(%r{^#{type}:/*}, '')}</a>"
89 # And we're invoked with a potential external hyperlink mailto:
90 # just gets inserted. http: links are checked to see if they
91 # reference an image. If so, that image gets inserted using an
92 # <img> tag. Otherwise a conventional <a href> is used. We also
93 # support a special type of hyperlink, link:, which is a reference
94 # to a local file whose path is relative to the --op directory.
96 def handle_special_HYPERLINK(special)
102 # Here's a hypedlink where the label is different to the URL
103 # <label>[url] or {long label}[url]
105 def handle_special_TIDYLINK(special)
108 return text unless text =~ /\{(.*?)\}\[(.*?)\]/ or text =~ /(\S+)\[(.*?)\]/
116 # are we currently inside <tt> tags?
123 # is +tag+ a <tt> tag?
130 # Set up the standard mapping of attributes to HTML tags
134 InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:BOLD), "<b>", "</b>"),
135 InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:TT), "<tt>", "</tt>"),
136 InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:EM), "<em>", "</em>"),
141 # Add a new set of HTML tags for an attribute. We allow separate start and
142 # end tags for flexibility.
144 def add_tag(name, start, stop)
145 @attr_tags << InlineTag.new(RDoc::Markup::Attribute.bitmap_for(name), start, stop)
149 # Given an HTML tag, decorate it with class information and the like if
150 # required. This is a no-op in the base class, but is overridden in HTML
151 # output classes that implement style sheets.
158 # Here's the client side of the visitor pattern
169 def accept_paragraph(am, fragment)
170 @res << annotate("<p>") + "\n"
171 @res << wrap(convert_flow(am.flow(fragment.txt)))
172 @res << annotate("</p>") + "\n"
175 def accept_verbatim(am, fragment)
176 @res << annotate("<pre>") + "\n"
177 @res << CGI.escapeHTML(fragment.txt)
178 @res << annotate("</pre>") << "\n"
181 def accept_rule(am, fragment)
182 size = fragment.param
183 size = 10 if size > 10
184 @res << "<hr size=\"#{size}\"></hr>"
187 def accept_list_start(am, fragment)
188 @res << html_list_name(fragment.type, true) << "\n"
189 @in_list_entry.push false
192 def accept_list_end(am, fragment)
193 if tag = @in_list_entry.pop
194 @res << annotate(tag) << "\n"
196 @res << html_list_name(fragment.type, false) << "\n"
199 def accept_list_item(am, fragment)
200 if tag = @in_list_entry.last
201 @res << annotate(tag) << "\n"
204 @res << list_item_start(am, fragment)
206 @res << wrap(convert_flow(am.flow(fragment.txt))) << "\n"
208 @in_list_entry[-1] = list_end_for(fragment.type)
211 def accept_blank_line(am, fragment)
212 # @res << annotate("<p />") << "\n"
215 def accept_heading(am, fragment)
216 @res << convert_heading(fragment.head_level, am.flow(fragment.txt))
220 # This is a higher speed (if messier) version of wrap
222 def wrap(txt, line_len = 76)
227 # scan back for a space
228 p = sp + line_len - 1
232 while p > sp and txt[p] != ?\s
237 while p < ep and txt[p] != ?\s
242 res << txt[sp...p] << "\n"
244 sp += 1 while sp < ep and txt[sp] == ?\s
251 def on_tags(res, item)
252 attr_mask = item.turn_on
253 return if attr_mask.zero?
255 @attr_tags.each do |tag|
256 if attr_mask & tag.bit != 0
257 res << annotate(tag.on)
258 @in_tt += 1 if tt?(tag)
263 def off_tags(res, item)
264 attr_mask = item.turn_off
265 return if attr_mask.zero?
267 @attr_tags.reverse_each do |tag|
268 if attr_mask & tag.bit != 0
269 @in_tt -= 1 if tt?(tag)
270 res << annotate(tag.off)
275 def convert_flow(flow)
281 res << convert_string(item)
282 when RDoc::Markup::AttrChanger
285 when RDoc::Markup::Special
286 res << convert_special(item)
288 raise "Unknown flow element: #{item.inspect}"
295 def convert_string(item)
296 in_tt? ? convert_string_simple(item) : convert_string_fancy(item)
299 def convert_string_simple(item)
304 # some of these patterns are taken from SmartyPants...
306 def convert_string_fancy(item)
307 # convert ampersand before doing anything else
308 item.gsub(/&/, '&').
310 # convert -- to em-dash, (-- to en-dash)
311 gsub(/---?/, '—'). #gsub(/--/, '–').
313 # convert ... to elipsis (and make sure .... becomes .<elipsis>)
314 gsub(/\.\.\.\./, '.…').gsub(/\.\.\./, '…').
316 # convert single closing quote
317 gsub(%r{([^ \t\r\n\[\{\(])\'}, '\1’'). # }
318 gsub(%r{\'(?=\W|s\b)}, '’').
320 # convert single opening quote
321 gsub(/'/, '‘').
323 # convert double closing quote
324 gsub(%r{([^ \t\r\n\[\{\(])\"(?=\W)}, '\1”'). # }
326 # convert double opening quote
327 gsub(/"/, '“').
330 gsub(/\(c\)/, '©').
332 # convert registered trademark
333 gsub(/\(r\)/, '®')
336 def convert_special(special)
338 RDoc::Markup::Attribute.each_name_of(special.type) do |name|
339 method_name = "handle_special_#{name}"
340 if self.respond_to? method_name
341 special.text = send(method_name, special)
345 raise "Unhandled special: #{special}" unless handled
349 def convert_heading(level, flow)
351 annotate("<h#{level}>") +
353 annotate("</h#{level}>\n")
356 def html_list_name(list_type, is_open_tag)
357 tags = LIST_TYPE_TO_HTML[list_type] || raise("Invalid list type: #{list_type.inspect}")
358 annotate(tags[ is_open_tag ? 0 : 1])
361 def list_item_start(am, fragment)
363 when :BULLET, :NUMBER then
366 when :UPPERALPHA then
367 annotate("<li type=\"A\">")
369 when :LOWERALPHA then
370 annotate("<li type=\"a\">")
374 convert_flow(am.flow(fragment.param)) +
380 annotate("<td valign=\"top\">") +
381 convert_flow(am.flow(fragment.param)) +
385 raise "Invalid list type"
389 def list_end_for(fragment_type)
391 when :BULLET, :NUMBER, :UPPERALPHA, :LOWERALPHA then
398 raise "Invalid list type"