diff options
Diffstat (limited to 'trunk/lib/rdoc/ri/formatter.rb')
-rw-r--r-- | trunk/lib/rdoc/ri/formatter.rb | 616 |
1 files changed, 616 insertions, 0 deletions
diff --git a/trunk/lib/rdoc/ri/formatter.rb b/trunk/lib/rdoc/ri/formatter.rb new file mode 100644 index 0000000000..0a0c3f7380 --- /dev/null +++ b/trunk/lib/rdoc/ri/formatter.rb @@ -0,0 +1,616 @@ +require 'rdoc/ri' +require 'rdoc/markup' + +class RDoc::RI::Formatter + + attr_writer :indent + attr_accessor :output + + FORMATTERS = { } + + def self.for(name) + FORMATTERS[name.downcase] + end + + def self.list + FORMATTERS.keys.sort.join ", " + end + + def initialize(output, width, indent) + @output = output + @width = width + @indent = indent + @original_indent = indent.dup + end + + def draw_line(label=nil) + len = @width + len -= (label.size + 1) if label + + if len > 0 then + @output.print '-' * len + if label + @output.print ' ' + bold_print label + end + + @output.puts + else + @output.print '-' * @width + @output.puts + + @output.puts label + end + end + + def indent + return @indent unless block_given? + + begin + indent = @indent.dup + @indent += @original_indent + yield + ensure + @indent = indent + end + end + + def wrap(txt, prefix=@indent, linelen=@width) + return unless txt && !txt.empty? + + work = conv_markup(txt) + textLen = linelen - prefix.length + patt = Regexp.new("^(.{0,#{textLen}})[ \n]") + next_prefix = prefix.tr("^ ", " ") + + res = [] + + while work.length > textLen + if work =~ patt + res << $1 + work.slice!(0, $&.length) + else + res << work.slice!(0, textLen) + end + end + res << work if work.length.nonzero? + @output.puts(prefix + res.join("\n" + next_prefix)) + end + + def blankline + @output.puts + end + + ## + # Called when we want to ensure a new 'wrap' starts on a newline. Only + # needed for HtmlFormatter, because the rest do their own line breaking. + + def break_to_newline + end + + def bold_print(txt) + @output.print txt + end + + def raw_print_line(txt) + @output.puts txt + end + + ## + # Convert HTML entities back to ASCII + + def conv_html(txt) + txt = txt.gsub(/>/, '>') + txt.gsub!(/</, '<') + txt.gsub!(/"/, '"') + txt.gsub!(/&/, '&') + txt + end + + ## + # Convert markup into display form + + def conv_markup(txt) + txt = txt.gsub(%r{<tt>(.*?)</tt>}, '+\1+') + txt.gsub!(%r{<code>(.*?)</code>}, '+\1+') + txt.gsub!(%r{<b>(.*?)</b>}, '*\1*') + txt.gsub!(%r{<em>(.*?)</em>}, '_\1_') + txt + end + + def display_list(list) + case list.type + when :BULLET + prefixer = proc { |ignored| @indent + "* " } + + when :NUMBER, :UPPERALPHA, :LOWERALPHA then + start = case list.type + when :NUMBER then 1 + when :UPPERALPHA then 'A' + when :LOWERALPHA then 'a' + end + + prefixer = proc do |ignored| + res = @indent + "#{start}.".ljust(4) + start = start.succ + res + end + + when :LABELED, :NOTE then + longest = 0 + + list.contents.each do |item| + if RDoc::Markup::Flow::LI === item and item.label.length > longest then + longest = item.label.length + end + end + + longest += 1 + + prefixer = proc { |li| @indent + li.label.ljust(longest) } + + else + raise ArgumentError, "unknown list type #{list.type}" + end + + list.contents.each do |item| + if RDoc::Markup::Flow::LI === item then + prefix = prefixer.call item + display_flow_item item, prefix + else + display_flow_item item + end + end + end + + def display_flow_item(item, prefix = @indent) + case item + when RDoc::Markup::Flow::P, RDoc::Markup::Flow::LI + wrap(conv_html(item.body), prefix) + blankline + + when RDoc::Markup::Flow::LIST + display_list(item) + + when RDoc::Markup::Flow::VERB + display_verbatim_flow_item(item, @indent) + + when RDoc::Markup::Flow::H + display_heading(conv_html(item.text), item.level, @indent) + + when RDoc::Markup::Flow::RULE + draw_line + + else + raise RDoc::Error, "Unknown flow element: #{item.class}" + end + end + + def display_verbatim_flow_item(item, prefix=@indent) + item.body.split(/\n/).each do |line| + @output.print @indent, conv_html(line), "\n" + end + blankline + end + + def display_heading(text, level, indent) + text = strip_attributes text + + case level + when 1 then + ul = "=" * text.length + @output.puts + @output.puts text.upcase + @output.puts ul + + when 2 then + ul = "-" * text.length + @output.puts + @output.puts text + @output.puts ul + else + @output.print indent, text, "\n" + end + + @output.puts + end + + def display_flow(flow) + flow.each do |f| + display_flow_item(f) + end + end + + def strip_attributes(text) + text.gsub(/(<\/?(?:b|code|em|i|tt)>)/, '') + end + +end + +## +# Handle text with attributes. We're a base class: there are different +# presentation classes (one, for example, uses overstrikes to handle bold and +# underlining, while another using ANSI escape sequences. + +class RDoc::RI::AttributeFormatter < RDoc::RI::Formatter + + BOLD = 1 + ITALIC = 2 + CODE = 4 + + ATTR_MAP = { + "b" => BOLD, + "code" => CODE, + "em" => ITALIC, + "i" => ITALIC, + "tt" => CODE + } + + AttrChar = Struct.new :char, :attr + + class AttributeString + attr_reader :txt + + def initialize + @txt = [] + @optr = 0 + end + + def <<(char) + @txt << char + end + + def empty? + @optr >= @txt.length + end + + # accept non space, then all following spaces + def next_word + start = @optr + len = @txt.length + + while @optr < len && @txt[@optr].char != " " + @optr += 1 + end + + while @optr < len && @txt[@optr].char == " " + @optr += 1 + end + + @txt[start...@optr] + end + end + + ## + # Overrides base class. Looks for <tt>...</tt> etc sequences and generates + # an array of AttrChars. This array is then used as the basis for the + # split. + + def wrap(txt, prefix=@indent, linelen=@width) + return unless txt && !txt.empty? + + txt = add_attributes_to(txt) + next_prefix = prefix.tr("^ ", " ") + linelen -= prefix.size + + line = [] + + until txt.empty? + word = txt.next_word + if word.size + line.size > linelen + write_attribute_text(prefix, line) + prefix = next_prefix + line = [] + end + line.concat(word) + end + + write_attribute_text(prefix, line) if line.length > 0 + end + + protected + + def write_attribute_text(prefix, line) + @output.print prefix + line.each do |achar| + @output.print achar.char + end + @output.puts + end + + def bold_print(txt) + @output.print txt + end + + private + + def add_attributes_to(txt) + tokens = txt.split(%r{(</?(?:b|code|em|i|tt)>)}) + text = AttributeString.new + attributes = 0 + tokens.each do |tok| + case tok + when %r{^</(\w+)>$} then attributes &= ~(ATTR_MAP[$1]||0) + when %r{^<(\w+)>$} then attributes |= (ATTR_MAP[$1]||0) + else + tok.split(//).each {|ch| text << AttrChar.new(ch, attributes)} + end + end + text + end + +end + +## +# This formatter generates overstrike-style formatting, which works with +# pagers such as man and less. + +class RDoc::RI::OverstrikeFormatter < RDoc::RI::AttributeFormatter + + BS = "\C-h" + + def write_attribute_text(prefix, line) + @output.print prefix + + line.each do |achar| + attr = achar.attr + @output.print "_", BS if (attr & (ITALIC + CODE)) != 0 + @output.print achar.char, BS if (attr & BOLD) != 0 + @output.print achar.char + end + + @output.puts + end + + ## + # Draw a string in bold + + def bold_print(text) + text.split(//).each do |ch| + @output.print ch, BS, ch + end + end + +end + +## +# This formatter uses ANSI escape sequences to colorize stuff works with +# pagers such as man and less. + +class RDoc::RI::AnsiFormatter < RDoc::RI::AttributeFormatter + + def initialize(*args) + super + @output.print "\033[0m" + end + + def write_attribute_text(prefix, line) + @output.print prefix + curr_attr = 0 + line.each do |achar| + attr = achar.attr + if achar.attr != curr_attr + update_attributes(achar.attr) + curr_attr = achar.attr + end + @output.print achar.char + end + update_attributes(0) unless curr_attr.zero? + @output.puts + end + + def bold_print(txt) + @output.print "\033[1m#{txt}\033[m" + end + + HEADINGS = { + 1 => ["\033[1;32m", "\033[m"], + 2 => ["\033[4;32m", "\033[m"], + 3 => ["\033[32m", "\033[m"], + } + + def display_heading(text, level, indent) + level = 3 if level > 3 + heading = HEADINGS[level] + @output.print indent + @output.print heading[0] + @output.print strip_attributes(text) + @output.puts heading[1] + end + + private + + ATTR_MAP = { + BOLD => "1", + ITALIC => "33", + CODE => "36" + } + + def update_attributes(attr) + str = "\033[" + for quality in [ BOLD, ITALIC, CODE] + unless (attr & quality).zero? + str << ATTR_MAP[quality] + end + end + @output.print str, "m" + end + +end + +## +# This formatter uses HTML. + +class RDoc::RI::HtmlFormatter < RDoc::RI::AttributeFormatter + + def write_attribute_text(prefix, line) + curr_attr = 0 + line.each do |achar| + attr = achar.attr + if achar.attr != curr_attr + update_attributes(curr_attr, achar.attr) + curr_attr = achar.attr + end + @output.print(escape(achar.char)) + end + update_attributes(curr_attr, 0) unless curr_attr.zero? + end + + def draw_line(label=nil) + if label != nil + bold_print(label) + end + @output.puts("<hr>") + end + + def bold_print(txt) + tag("b") { txt } + end + + def blankline() + @output.puts("<p>") + end + + def break_to_newline + @output.puts("<br>") + end + + def display_heading(text, level, indent) + level = 4 if level > 4 + tag("h#{level}") { text } + @output.puts + end + + def display_list(list) + case list.type + when :BULLET then + list_type = "ul" + prefixer = proc { |ignored| "<li>" } + + when :NUMBER, :UPPERALPHA, :LOWERALPHA then + list_type = "ol" + prefixer = proc { |ignored| "<li>" } + + when :LABELED then + list_type = "dl" + prefixer = proc do |li| + "<dt><b>" + escape(li.label) + "</b><dd>" + end + + when :NOTE then + list_type = "table" + prefixer = proc do |li| + %{<tr valign="top"><td>#{li.label.gsub(/ /, ' ')}</td><td>} + end + else + fail "unknown list type" + end + + @output.print "<#{list_type}>" + list.contents.each do |item| + if item.kind_of? RDoc::Markup::Flow::LI + prefix = prefixer.call(item) + @output.print prefix + display_flow_item(item, prefix) + else + display_flow_item(item) + end + end + @output.print "</#{list_type}>" + end + + def display_verbatim_flow_item(item, prefix=@indent) + @output.print("<pre>") + item.body.split(/\n/).each do |line| + @output.puts conv_html(line) + end + @output.puts("</pre>") + end + + private + + ATTR_MAP = { + BOLD => "b>", + ITALIC => "i>", + CODE => "tt>" + } + + def update_attributes(current, wanted) + str = "" + # first turn off unwanted ones + off = current & ~wanted + for quality in [ BOLD, ITALIC, CODE] + if (off & quality) > 0 + str << "</" + ATTR_MAP[quality] + end + end + + # now turn on wanted + for quality in [ BOLD, ITALIC, CODE] + unless (wanted & quality).zero? + str << "<" << ATTR_MAP[quality] + end + end + @output.print str + end + + def tag(code) + @output.print("<#{code}>") + @output.print(yield) + @output.print("</#{code}>") + end + + def escape(str) + str = str.gsub(/&/n, '&') + str.gsub!(/\"/n, '"') + str.gsub!(/>/n, '>') + str.gsub!(/</n, '<') + str + end + +end + +## +# This formatter reduces extra lines for a simpler output. It improves way +# output looks for tools like IRC bots. + +class RDoc::RI::SimpleFormatter < RDoc::RI::Formatter + + ## + # No extra blank lines + + def blankline + end + + ## + # Display labels only, no lines + + def draw_line(label=nil) + unless label.nil? then + bold_print(label) + @output.puts + end + end + + ## + # Place heading level indicators inline with heading. + + def display_heading(text, level, indent) + text = strip_attributes(text) + case level + when 1 + @output.puts "= " + text.upcase + when 2 + @output.puts "-- " + text + else + @output.print indent, text, "\n" + end + end + +end + +RDoc::RI::Formatter::FORMATTERS['plain'] = RDoc::RI::Formatter +RDoc::RI::Formatter::FORMATTERS['simple'] = RDoc::RI::SimpleFormatter +RDoc::RI::Formatter::FORMATTERS['bs'] = RDoc::RI::OverstrikeFormatter +RDoc::RI::Formatter::FORMATTERS['ansi'] = RDoc::RI::AnsiFormatter +RDoc::RI::Formatter::FORMATTERS['html'] = RDoc::RI::HtmlFormatter |