1 require 'rdoc/markup/formatter'
2 require 'rdoc/markup/fragments'
3 require 'rdoc/markup/inline'
4 require 'rdoc/generator'
8 class RDoc::Markup::ToHtml < RDoc::Markup::Formatter
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>],
19 InlineTag = Struct.new(:bit, :on, :off)
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)
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
47 if type == "link" then
48 url = if path[0, 1] == '#' then # is this meaningful?
51 RDoc::Generator.gen_url @from_path, path
55 if (type == "http" or type == "link") and
56 url =~ /\.(gif|png|jpg|jpeg|bmp)$/ then
57 "<img src=\"#{url}\" />"
59 "<a href=\"#{url}\">#{text.sub(%r{^#{type}:/*}, '')}</a>"
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)
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)
83 return text unless text =~ /\{(.*?)\}\[(.*?)\]/ or text =~ /(\S+)\[(.*?)\]/
91 # Set up the standard mapping of attributes to HTML 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>"),
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)
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.
119 # Here's the client side of the visitor pattern
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"
136 def accept_verbatim(am, fragment)
137 @res << annotate("<pre>") + "\n"
138 @res << CGI.escapeHTML(fragment.txt)
139 @res << annotate("</pre>") << "\n"
142 def accept_rule(am, fragment)
143 size = fragment.param
144 size = 10 if size > 10
145 @res << "<hr size=\"#{size}\"></hr>"
148 def accept_list_start(am, fragment)
149 @res << html_list_name(fragment.type, true) << "\n"
150 @in_list_entry.push false
153 def accept_list_end(am, fragment)
154 if tag = @in_list_entry.pop
155 @res << annotate(tag) << "\n"
157 @res << html_list_name(fragment.type, false) << "\n"
160 def accept_list_item(am, fragment)
161 if tag = @in_list_entry.last
162 @res << annotate(tag) << "\n"
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)
172 def accept_blank_line(am, fragment)
173 # @res << annotate("<p />") << "\n"
176 def accept_heading(am, fragment)
177 @res << convert_heading(fragment.head_level, am.flow(fragment.txt))
181 # This is a higher speed (if messier) version of wrap
183 def wrap(txt, line_len = 76)
188 # scan back for a space
189 p = sp + line_len - 1
193 while p > sp and txt[p] != ?\s
198 while p < ep and txt[p] != ?\s
203 res << txt[sp...p] << "\n"
205 sp += 1 while sp < ep and txt[sp] == ?\s
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)
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)
234 def convert_flow(flow)
240 res << convert_string(item)
241 when RDoc::Markup::AttrChanger
244 when RDoc::Markup::Special
245 res << convert_special(item)
247 raise "Unknown flow element: #{item.inspect}"
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(/---?/, '—'). #gsub(/--/, '–').
263 # convert ... to elipsis (and make sure .... becomes .<elipsis>)
264 gsub(/\.\.\.\./, '.…').gsub(/\.\.\./, '…').
266 # convert single closing quote
267 gsub(%r{([^ \t\r\n\[\{\(])\'}, '\1’').
268 gsub(%r{\'(?=\W|s\b)}, '’').
270 # convert single opening quote
271 gsub(/'/, '‘').
273 # convert double closing quote
274 gsub(%r{([^ \t\r\n\[\{\(])\'(?=\W)}, '\1”').
276 # convert double opening quote
277 gsub(/'/, '“').
280 gsub(/\(c\)/, '©').
282 # convert and registered trademark
283 gsub(/\(r\)/, '®')
287 def convert_special(special)
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)
296 raise "Unhandled special: #{special}" unless handled
300 def convert_heading(level, flow)
302 annotate("<h#{level}>") +
304 annotate("</h#{level}>\n")
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])
312 def list_item_start(am, fragment)
314 when :BULLET, :NUMBER then
317 when :UPPERALPHA then
318 annotate("<li type=\"A\">")
320 when :LOWERALPHA then
321 annotate("<li type=\"a\">")
325 convert_flow(am.flow(fragment.param)) +
331 annotate("<td valign=\"top\">") +
332 convert_flow(am.flow(fragment.param)) +
336 raise "Invalid list type"
340 def list_end_for(fragment_type)
342 when :BULLET, :NUMBER, :UPPERALPHA, :LOWERALPHA then
349 raise "Invalid list type"