Re-enable spec/library for full CI runs.
[rbx.git] / lib / rdoc / ri / formatter.rb
blob0a0c3f7380214bb17eebbd652b35c055e41cec98
1 require 'rdoc/ri'
2 require 'rdoc/markup'
4 class RDoc::RI::Formatter
6   attr_writer :indent
7   attr_accessor :output
9   FORMATTERS = { }
11   def self.for(name)
12     FORMATTERS[name.downcase]
13   end
15   def self.list
16     FORMATTERS.keys.sort.join ", "
17   end
19   def initialize(output, width, indent)
20     @output = output
21     @width  = width
22     @indent = indent
23     @original_indent = indent.dup
24   end
26   def draw_line(label=nil)
27     len = @width
28     len -= (label.size + 1) if label
30     if len > 0 then
31       @output.print '-' * len
32       if label
33         @output.print ' '
34         bold_print label
35       end
37       @output.puts
38     else
39       @output.print '-' * @width
40       @output.puts
42       @output.puts label
43     end
44   end
46   def indent
47     return @indent unless block_given?
49     begin
50       indent = @indent.dup
51       @indent += @original_indent
52       yield
53     ensure
54       @indent = indent
55     end
56   end
58   def wrap(txt, prefix=@indent, linelen=@width)
59     return unless txt && !txt.empty?
61     work = conv_markup(txt)
62     textLen = linelen - prefix.length
63     patt = Regexp.new("^(.{0,#{textLen}})[ \n]")
64     next_prefix = prefix.tr("^ ", " ")
66     res = []
68     while work.length > textLen
69       if work =~ patt
70         res << $1
71         work.slice!(0, $&.length)
72       else
73         res << work.slice!(0, textLen)
74       end
75     end
76     res << work if work.length.nonzero?
77     @output.puts(prefix + res.join("\n" + next_prefix))
78   end
80   def blankline
81     @output.puts
82   end
84   ##
85   # Called when we want to ensure a new 'wrap' starts on a newline.  Only
86   # needed for HtmlFormatter, because the rest do their own line breaking.
88   def break_to_newline
89   end
91   def bold_print(txt)
92     @output.print txt
93   end
95   def raw_print_line(txt)
96     @output.puts txt
97   end
99   ##
100   # Convert HTML entities back to ASCII
102   def conv_html(txt)
103     txt = txt.gsub(/&gt;/, '>')
104     txt.gsub!(/&lt;/, '<')
105     txt.gsub!(/&quot;/, '"')
106     txt.gsub!(/&amp;/, '&')
107     txt
108   end
110   ##
111   # Convert markup into display form
113   def conv_markup(txt)
114     txt = txt.gsub(%r{<tt>(.*?)</tt>}, '+\1+')
115     txt.gsub!(%r{<code>(.*?)</code>}, '+\1+')
116     txt.gsub!(%r{<b>(.*?)</b>}, '*\1*')
117     txt.gsub!(%r{<em>(.*?)</em>}, '_\1_') 
118     txt
119   end
121   def display_list(list)
122     case list.type
123     when :BULLET
124       prefixer = proc { |ignored| @indent + "*   " }
126     when :NUMBER, :UPPERALPHA, :LOWERALPHA then
127       start = case list.type
128               when :NUMBER     then 1
129               when :UPPERALPHA then 'A'
130               when :LOWERALPHA then 'a'
131               end
133       prefixer = proc do |ignored|
134         res = @indent + "#{start}.".ljust(4)
135         start = start.succ
136         res
137       end
139     when :LABELED, :NOTE then
140       longest = 0
142       list.contents.each do |item|
143         if RDoc::Markup::Flow::LI === item and item.label.length > longest then
144           longest = item.label.length
145         end
146       end
148       longest += 1
150       prefixer = proc { |li| @indent + li.label.ljust(longest) }
152     else
153       raise ArgumentError, "unknown list type #{list.type}"
154     end
156     list.contents.each do |item|
157       if RDoc::Markup::Flow::LI === item then
158         prefix = prefixer.call item
159         display_flow_item item, prefix
160       else
161         display_flow_item item
162       end
163     end
164   end
166   def display_flow_item(item, prefix = @indent)
167     case item
168     when RDoc::Markup::Flow::P, RDoc::Markup::Flow::LI
169       wrap(conv_html(item.body), prefix)
170       blankline
172     when RDoc::Markup::Flow::LIST
173       display_list(item)
175     when RDoc::Markup::Flow::VERB
176       display_verbatim_flow_item(item, @indent)
178     when RDoc::Markup::Flow::H
179       display_heading(conv_html(item.text), item.level, @indent)
181     when RDoc::Markup::Flow::RULE
182       draw_line
184     else
185       raise RDoc::Error, "Unknown flow element: #{item.class}"
186     end
187   end
189   def display_verbatim_flow_item(item, prefix=@indent)
190     item.body.split(/\n/).each do |line|
191       @output.print @indent, conv_html(line), "\n"
192     end
193     blankline
194   end
196   def display_heading(text, level, indent)
197     text = strip_attributes text
199     case level
200     when 1 then
201       ul = "=" * text.length
202       @output.puts
203       @output.puts text.upcase
204       @output.puts ul
206     when 2 then
207       ul = "-" * text.length
208       @output.puts
209       @output.puts text
210       @output.puts ul
211     else
212       @output.print indent, text, "\n"
213     end
215     @output.puts
216   end
218   def display_flow(flow)
219     flow.each do |f|
220       display_flow_item(f)
221     end
222   end
224   def strip_attributes(text)
225     text.gsub(/(<\/?(?:b|code|em|i|tt)>)/, '')
226   end
231 # Handle text with attributes. We're a base class: there are different
232 # presentation classes (one, for example, uses overstrikes to handle bold and
233 # underlining, while another using ANSI escape sequences.
235 class RDoc::RI::AttributeFormatter < RDoc::RI::Formatter
237   BOLD      = 1
238   ITALIC    = 2
239   CODE      = 4
241   ATTR_MAP = {
242     "b"    => BOLD,
243     "code" => CODE,
244     "em"   => ITALIC,
245     "i"    => ITALIC,
246     "tt"   => CODE
247   }
249   AttrChar = Struct.new :char, :attr
251   class AttributeString
252     attr_reader :txt
254     def initialize
255       @txt = []
256       @optr = 0
257     end
259     def <<(char)
260       @txt << char
261     end
263     def empty?
264       @optr >= @txt.length
265     end
267     # accept non space, then all following spaces
268     def next_word
269       start = @optr
270       len = @txt.length
272       while @optr < len && @txt[@optr].char != " "
273         @optr += 1
274       end
276       while @optr < len && @txt[@optr].char == " "
277         @optr += 1
278       end
280       @txt[start...@optr]
281     end
282   end
284   ##
285   # Overrides base class.  Looks for <tt>...</tt> etc sequences and generates
286   # an array of AttrChars.  This array is then used as the basis for the
287   # split.
289   def wrap(txt, prefix=@indent, linelen=@width)
290     return unless txt && !txt.empty?
292     txt = add_attributes_to(txt)
293     next_prefix = prefix.tr("^ ", " ")
294     linelen -= prefix.size
296     line = []
298     until txt.empty?
299       word = txt.next_word
300       if word.size + line.size > linelen
301         write_attribute_text(prefix, line)
302         prefix = next_prefix
303         line = []
304       end
305       line.concat(word)
306     end
308     write_attribute_text(prefix, line) if line.length > 0
309   end
311   protected
313   def write_attribute_text(prefix, line)
314     @output.print prefix
315     line.each do |achar|
316       @output.print achar.char
317     end
318     @output.puts
319   end
321   def bold_print(txt)
322     @output.print txt
323   end
325   private
327   def add_attributes_to(txt)
328     tokens = txt.split(%r{(</?(?:b|code|em|i|tt)>)})
329     text = AttributeString.new
330     attributes = 0
331     tokens.each do |tok|
332       case tok
333       when %r{^</(\w+)>$} then attributes &= ~(ATTR_MAP[$1]||0)
334       when %r{^<(\w+)>$}  then attributes  |= (ATTR_MAP[$1]||0)
335       else
336         tok.split(//).each {|ch| text << AttrChar.new(ch, attributes)}
337       end
338     end
339     text
340   end
345 # This formatter generates overstrike-style formatting, which works with
346 # pagers such as man and less.
348 class RDoc::RI::OverstrikeFormatter < RDoc::RI::AttributeFormatter
350   BS = "\C-h"
352   def write_attribute_text(prefix, line)
353     @output.print prefix
355     line.each do |achar|
356       attr = achar.attr
357       @output.print "_", BS if (attr & (ITALIC + CODE)) != 0
358       @output.print achar.char, BS if (attr & BOLD) != 0
359       @output.print achar.char
360     end
362     @output.puts
363   end
365   ##
366   # Draw a string in bold
368   def bold_print(text)
369     text.split(//).each do |ch|
370       @output.print ch, BS, ch
371     end
372   end
377 # This formatter uses ANSI escape sequences to colorize stuff works with
378 # pagers such as man and less.
380 class RDoc::RI::AnsiFormatter < RDoc::RI::AttributeFormatter
382   def initialize(*args)
383     super
384     @output.print "\033[0m"
385   end
387   def write_attribute_text(prefix, line)
388     @output.print prefix
389     curr_attr = 0
390     line.each do |achar|
391       attr = achar.attr
392       if achar.attr != curr_attr
393         update_attributes(achar.attr)
394         curr_attr = achar.attr
395       end
396       @output.print achar.char
397     end
398     update_attributes(0) unless curr_attr.zero?
399     @output.puts
400   end
402   def bold_print(txt)
403     @output.print "\033[1m#{txt}\033[m"
404   end
406   HEADINGS = {
407     1 => ["\033[1;32m", "\033[m"],
408     2 => ["\033[4;32m", "\033[m"],
409     3 => ["\033[32m",   "\033[m"],
410   }
412   def display_heading(text, level, indent)
413     level = 3 if level > 3
414     heading = HEADINGS[level]
415     @output.print indent
416     @output.print heading[0]
417     @output.print strip_attributes(text)
418     @output.puts heading[1]
419   end
421   private
423   ATTR_MAP = {
424     BOLD   => "1",
425     ITALIC => "33",
426     CODE   => "36"
427   }
429   def update_attributes(attr)
430     str = "\033["
431     for quality in [ BOLD, ITALIC, CODE]
432       unless (attr & quality).zero?
433         str << ATTR_MAP[quality]
434       end
435     end
436     @output.print str, "m"
437   end
442 # This formatter uses HTML.
444 class RDoc::RI::HtmlFormatter < RDoc::RI::AttributeFormatter
446   def write_attribute_text(prefix, line)
447     curr_attr = 0
448     line.each do |achar|
449       attr = achar.attr
450       if achar.attr != curr_attr
451         update_attributes(curr_attr, achar.attr)
452         curr_attr = achar.attr
453       end
454       @output.print(escape(achar.char))
455     end
456     update_attributes(curr_attr, 0) unless curr_attr.zero?
457   end
459   def draw_line(label=nil)
460     if label != nil
461       bold_print(label)
462     end
463     @output.puts("<hr>")
464   end
466   def bold_print(txt)
467     tag("b") { txt }
468   end
470   def blankline()
471     @output.puts("<p>")
472   end
474   def break_to_newline
475     @output.puts("<br>")
476   end
478   def display_heading(text, level, indent)
479     level = 4 if level > 4
480     tag("h#{level}") { text }
481     @output.puts
482   end
484   def display_list(list)
485     case list.type
486     when :BULLET then
487       list_type = "ul"
488       prefixer = proc { |ignored| "<li>" }
490     when :NUMBER, :UPPERALPHA, :LOWERALPHA then
491       list_type = "ol"
492       prefixer = proc { |ignored| "<li>" }
494     when :LABELED then
495       list_type = "dl"
496       prefixer = proc do |li|
497         "<dt><b>" + escape(li.label) + "</b><dd>"
498       end
500     when :NOTE then
501       list_type = "table"
502       prefixer = proc do |li|
503         %{<tr valign="top"><td>#{li.label.gsub(/ /, '&nbsp;')}</td><td>}
504       end
505     else
506       fail "unknown list type"
507     end
509     @output.print "<#{list_type}>"
510     list.contents.each do |item|
511       if item.kind_of? RDoc::Markup::Flow::LI
512         prefix = prefixer.call(item)
513         @output.print prefix
514         display_flow_item(item, prefix)
515       else
516         display_flow_item(item)
517       end
518     end
519     @output.print "</#{list_type}>"
520   end
522   def display_verbatim_flow_item(item, prefix=@indent)
523     @output.print("<pre>")
524     item.body.split(/\n/).each do |line|
525       @output.puts conv_html(line)
526     end
527     @output.puts("</pre>")
528   end
530   private
532   ATTR_MAP = {
533     BOLD   => "b>",
534     ITALIC => "i>",
535     CODE   => "tt>"
536   }
538   def update_attributes(current, wanted)
539     str = ""
540     # first turn off unwanted ones
541     off = current & ~wanted
542     for quality in [ BOLD, ITALIC, CODE]
543       if (off & quality) > 0
544         str << "</" + ATTR_MAP[quality]
545       end
546     end
548     # now turn on wanted
549     for quality in [ BOLD, ITALIC, CODE]
550       unless (wanted & quality).zero?
551         str << "<" << ATTR_MAP[quality]
552       end
553     end
554     @output.print str
555   end
557   def tag(code)
558     @output.print("<#{code}>")
559     @output.print(yield)
560     @output.print("</#{code}>")
561   end
563   def escape(str)
564     str = str.gsub(/&/n, '&amp;')
565     str.gsub!(/\"/n, '&quot;')
566     str.gsub!(/>/n, '&gt;')
567     str.gsub!(/</n, '&lt;')
568     str
569   end
574 # This formatter reduces extra lines for a simpler output.  It improves way
575 # output looks for tools like IRC bots.
577 class RDoc::RI::SimpleFormatter < RDoc::RI::Formatter
579   ##
580   # No extra blank lines
582   def blankline
583   end
585   ##
586   # Display labels only, no lines
588   def draw_line(label=nil)
589     unless label.nil? then
590       bold_print(label)
591       @output.puts
592     end
593   end
595   ##
596   # Place heading level indicators inline with heading.
598   def display_heading(text, level, indent)
599     text = strip_attributes(text)
600     case level
601     when 1
602       @output.puts "= " + text.upcase
603     when 2
604       @output.puts "-- " + text
605     else
606       @output.print indent, text, "\n"
607     end
608   end
612 RDoc::RI::Formatter::FORMATTERS['plain']  = RDoc::RI::Formatter
613 RDoc::RI::Formatter::FORMATTERS['simple'] = RDoc::RI::SimpleFormatter
614 RDoc::RI::Formatter::FORMATTERS['bs']     = RDoc::RI::OverstrikeFormatter
615 RDoc::RI::Formatter::FORMATTERS['ansi']   = RDoc::RI::AnsiFormatter
616 RDoc::RI::Formatter::FORMATTERS['html']   = RDoc::RI::HtmlFormatter