diff options
author | drbrain <drbrain@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2010-04-01 07:45:16 +0000 |
---|---|---|
committer | drbrain <drbrain@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2010-04-01 07:45:16 +0000 |
commit | 46580b51477355fece514573c88cb67030f4a502 (patch) | |
tree | 779c1a64466643461b3daa4cd9a3548b84f0fd55 /lib/rdoc/markup | |
parent | 9b40cdfe8c973a061c5683ad78c283b9ddb8b2e9 (diff) | |
download | ruby-46580b51477355fece514573c88cb67030f4a502.tar.gz |
Import RDoc 2.5
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@27147 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'lib/rdoc/markup')
25 files changed, 2123 insertions, 1380 deletions
diff --git a/lib/rdoc/markup/attribute_manager.rb b/lib/rdoc/markup/attribute_manager.rb index d13b79376c..5b9e070efb 100644 --- a/lib/rdoc/markup/attribute_manager.rb +++ b/lib/rdoc/markup/attribute_manager.rb @@ -1,41 +1,76 @@ -require 'rdoc/markup/inline' +## +# Manages changes of attributes in a block of text class RDoc::Markup::AttributeManager + ## + # The NUL character + NULL = "\000".freeze - ## + #-- # We work by substituting non-printing characters in to the text. For now # I'm assuming that I can substitute a character in the range 0..8 for a 7 # bit character without damaging the encoded string, but this might be # optimistic + #++ - A_PROTECT = 004 - PROTECT_ATTR = A_PROTECT.chr + A_PROTECT = 004 # :nodoc: + + PROTECT_ATTR = A_PROTECT.chr # :nodoc: ## # This maps delimiters that occur around words (such as *bold* or +tt+) # where the start and end delimiters and the same. This lets us optimize # the regexp - MATCHING_WORD_PAIRS = {} + attr_reader :matching_word_pairs ## # And this is used when the delimiters aren't the same. In this case the # hash maps a pattern to the attribute character - WORD_PAIR_MAP = {} + attr_reader :word_pair_map ## # This maps HTML tags to the corresponding attribute char - HTML_TAGS = {} + attr_reader :html_tags + + ## + # A \ in front of a character that would normally be processed turns off + # processing. We do this by turning \< into <#{PROTECT} + + attr_reader :protectable ## # And this maps _special_ sequences to a name. A special sequence is # something like a WikiWord - SPECIAL = {} + attr_reader :special + + ## + # Creates a new attribute manager that understands bold, emphasized and + # teletype text. + + def initialize + @html_tags = {} + @matching_word_pairs = {} + @protectable = %w[<\\] + @special = {} + @word_pair_map = {} + + add_word_pair "*", "*", :BOLD + add_word_pair "_", "_", :EM + add_word_pair "+", "+", :TT + + add_html "em", :EM + add_html "i", :EM + add_html "b", :BOLD + add_html "tt", :TT + add_html "code", :TT + end + ## # Return an attribute object with the given turn_on and turn_off bits set @@ -75,19 +110,19 @@ class RDoc::Markup::AttributeManager def convert_attrs(str, attrs) # first do matching ones - tags = MATCHING_WORD_PAIRS.keys.join("") + tags = @matching_word_pairs.keys.join("") re = /(^|\W)([#{tags}])([#:\\]?[\w.\/-]+?\S?)\2(\W|$)/ 1 while str.gsub!(re) do - attr = MATCHING_WORD_PAIRS[$2] + attr = @matching_word_pairs[$2] attrs.set_attrs($`.length + $1.length + $2.length, $3.length, attr) $1 + NULL * $2.length + $3 + NULL * $2.length + $4 end # then non-matching - unless WORD_PAIR_MAP.empty? then - WORD_PAIR_MAP.each do |regexp, attr| + unless @word_pair_map.empty? then + @word_pair_map.each do |regexp, attr| str.gsub!(regexp) { attrs.set_attrs($`.length + $1.length, $2.length, attr) NULL * $1.length + $2 + NULL * $3.length @@ -96,11 +131,14 @@ class RDoc::Markup::AttributeManager end end + ## + # Converts HTML tags to RDoc attributes + def convert_html(str, attrs) - tags = HTML_TAGS.keys.join '|' + tags = @html_tags.keys.join '|' 1 while str.gsub!(/<(#{tags})>(.*?)<\/\1>/i) { - attr = HTML_TAGS[$1.downcase] + attr = @html_tags[$1.downcase] html_length = $1.length + 2 seq = NULL * html_length attrs.set_attrs($`.length + html_length, $2.length, attr) @@ -108,9 +146,12 @@ class RDoc::Markup::AttributeManager } end + ## + # Converts special sequences to RDoc attributes + def convert_specials(str, attrs) - unless SPECIAL.empty? - SPECIAL.each do |regexp, attr| + unless @special.empty? + @special.each do |regexp, attr| str.scan(regexp) do attrs.set_attrs($`.length, $&.length, attr | RDoc::Markup::Attribute::SPECIAL) @@ -120,31 +161,25 @@ class RDoc::Markup::AttributeManager end ## - # A \ in front of a character that would normally be processed turns off - # processing. We do this by turning \< into <#{PROTECT} - - PROTECTABLE = %w[<\\] + # Escapes special sequences of text to prevent conversion to RDoc def mask_protected_sequences - protect_pattern = Regexp.new("\\\\([#{Regexp.escape(PROTECTABLE.join(''))}])") - @str.gsub!(protect_pattern, "\\1#{PROTECT_ATTR}") + @str.gsub!(/\\([#{Regexp.escape @protectable.join('')}])/, + "\\1#{PROTECT_ATTR}") end + ## + # Unescapes special sequences of text + def unmask_protected_sequences @str.gsub!(/(.)#{PROTECT_ATTR}/, "\\1\000") end - def initialize - add_word_pair("*", "*", :BOLD) - add_word_pair("_", "_", :EM) - add_word_pair("+", "+", :TT) - - add_html("em", :EM) - add_html("i", :EM) - add_html("b", :BOLD) - add_html("tt", :TT) - add_html("code", :TT) - end + ## + # Adds a markup class with +name+ for words wrapped in the +start+ and + # +stop+ character. To make words wrapped with "*" bold: + # + # am.add_word_pair '*', '*', :BOLD def add_word_pair(start, stop, name) raise ArgumentError, "Word flags may not start with '<'" if @@ -153,24 +188,39 @@ class RDoc::Markup::AttributeManager bitmap = RDoc::Markup::Attribute.bitmap_for name if start == stop then - MATCHING_WORD_PAIRS[start] = bitmap + @matching_word_pairs[start] = bitmap else pattern = /(#{Regexp.escape start})(\S+)(#{Regexp.escape stop})/ - WORD_PAIR_MAP[pattern] = bitmap + @word_pair_map[pattern] = bitmap end - PROTECTABLE << start[0,1] - PROTECTABLE.uniq! + @protectable << start[0,1] + @protectable.uniq! end + ## + # Adds a markup class with +name+ for words surrounded by HTML tag +tag+. + # To process emphasis tags: + # + # am.add_html 'em', :EM + def add_html(tag, name) - HTML_TAGS[tag.downcase] = RDoc::Markup::Attribute.bitmap_for name + @html_tags[tag.downcase] = RDoc::Markup::Attribute.bitmap_for name end + ## + # Adds a special handler for +pattern+ with +name+. A simple URL handler + # would be: + # + # @am.add_special(/((https?:)\S+\w)/, :HYPERLINK) + def add_special(pattern, name) - SPECIAL[pattern] = RDoc::Markup::Attribute.bitmap_for name + @special[pattern] = RDoc::Markup::Attribute.bitmap_for name end + ## + # Processes +str+ converting attributes, HTML and specials + def flow(str) @str = str @@ -178,15 +228,18 @@ class RDoc::Markup::AttributeManager @attrs = RDoc::Markup::AttrSpan.new @str.length - convert_attrs(@str, @attrs) - convert_html(@str, @attrs) - convert_specials(str, @attrs) + convert_attrs @str, @attrs + convert_html @str, @attrs + convert_specials @str, @attrs unmask_protected_sequences - return split_into_flow + split_into_flow end + ## + # Debug method that prints a string along with its attributes + def display_attributes puts puts @str.tr(NULL, "!") @@ -258,7 +311,7 @@ class RDoc::Markup::AttributeManager # and reset to all attributes off res << change_attribute(current_attr, 0) if current_attr != 0 - return res + res end end diff --git a/lib/rdoc/markup/blank_line.rb b/lib/rdoc/markup/blank_line.rb new file mode 100644 index 0000000000..a8c07c8e57 --- /dev/null +++ b/lib/rdoc/markup/blank_line.rb @@ -0,0 +1,19 @@ +## +# An empty line + +class RDoc::Markup::BlankLine + + def == other # :nodoc: + self.class == other.class + end + + def accept visitor + visitor.accept_blank_line self + end + + def pretty_print q # :nodoc: + q.text 'blankline' + end + +end + diff --git a/lib/rdoc/markup/document.rb b/lib/rdoc/markup/document.rb new file mode 100644 index 0000000000..7963e9afe1 --- /dev/null +++ b/lib/rdoc/markup/document.rb @@ -0,0 +1,72 @@ +## +# A Document containing lists, headings, paragraphs, etc. + +class RDoc::Markup::Document + + ## + # The parts of the Document + + attr_reader :parts + + ## + # Creates a new Document with +parts+ + + def initialize *parts + @parts = [] + @parts.push(*parts) + end + + ## + # Appends +part+ to the document + + def << part + case part + when RDoc::Markup::Document then + unless part.empty? then + parts.push(*part.parts) + parts << RDoc::Markup::BlankLine.new + end + when String then + raise ArgumentError, + "expected RDoc::Markup::Document and friends, got String" unless + part.empty? + else + parts << part + end + end + + def == other # :nodoc: + self.class == other.class and @parts == other.parts + end + + def accept visitor + visitor.start_accepting + + @parts.each do |item| + item.accept visitor + end + + visitor.end_accepting + end + + def empty? + @parts.empty? + end + + def pretty_print q # :nodoc: + q.group 2, '[doc: ', ']' do + q.seplist @parts do |part| + q.pp part + end + end + end + + ## + # Appends +parts+ to the document + + def push *parts + self.parts.push(*parts) + end + +end + diff --git a/lib/rdoc/markup/formatter.rb b/lib/rdoc/markup/formatter.rb index 14cbae59f9..993e523f0c 100644 --- a/lib/rdoc/markup/formatter.rb +++ b/lib/rdoc/markup/formatter.rb @@ -1,14 +1,143 @@ require 'rdoc/markup' +## +# Base class for RDoc markup formatters +# +# Formatters use a visitor pattern to convert content into output. + class RDoc::Markup::Formatter + InlineTag = Struct.new(:bit, :on, :off) + + ## + # Creates a new Formatter + def initialize @markup = RDoc::Markup.new + @am = @markup.attribute_manager + @attr_tags = [] + + @in_tt = 0 + @tt_bit = RDoc::Markup::Attribute.bitmap_for :TT + end + + ## + # Add a new set of tags for an attribute. We allow separate start and end + # tags for flexibility + + def add_tag(name, start, stop) + attr = RDoc::Markup::Attribute.bitmap_for name + @attr_tags << InlineTag.new(attr, start, stop) + end + + ## + # Allows +tag+ to be decorated with additional information. + + def annotate(tag) + tag end + ## + # Marks up +content+ + def convert(content) @markup.convert content, self end + ## + # Converts flow items +flow+ + + def convert_flow(flow) + res = [] + + flow.each do |item| + case item + when String then + res << convert_string(item) + when RDoc::Markup::AttrChanger then + off_tags res, item + on_tags res, item + when RDoc::Markup::Special then + res << convert_special(item) + else + raise "Unknown flow element: #{item.inspect}" + end + end + + res.join + end + + ## + # Converts added specials. See RDoc::Markup#add_special + + def convert_special(special) + handled = false + + RDoc::Markup::Attribute.each_name_of special.type do |name| + method_name = "handle_special_#{name}" + + if respond_to? method_name then + special.text = send method_name, special + handled = true + end + end + + raise "Unhandled special: #{special}" unless handled + + special.text + end + + ## + # Converts a string to be fancier if desired + + def convert_string string + string + end + + ## + # Are we currently inside tt tags? + + def in_tt? + @in_tt > 0 + end + + def on_tags res, item + attr_mask = item.turn_on + return if attr_mask.zero? + + @attr_tags.each do |tag| + if attr_mask & tag.bit != 0 then + res << annotate(tag.on) + @in_tt += 1 if tt? tag + end + end + end + + def off_tags res, item + attr_mask = item.turn_off + return if attr_mask.zero? + + @attr_tags.reverse_each do |tag| + if attr_mask & tag.bit != 0 then + @in_tt -= 1 if tt? tag + res << annotate(tag.off) + end + end + end + + ## + # Is +tag+ a tt tag? + + def tt? tag + tag.bit == @tt_bit + end + end +class RDoc::Markup + autoload :ToAnsi, 'rdoc/markup/to_ansi' + autoload :ToBs, 'rdoc/markup/to_bs' + autoload :ToHtml, 'rdoc/markup/to_html' + autoload :ToHtmlCrossref, 'rdoc/markup/to_html_crossref' + autoload :ToRdoc, 'rdoc/markup/to_rdoc' +end diff --git a/lib/rdoc/markup/formatter_test_case.rb b/lib/rdoc/markup/formatter_test_case.rb new file mode 100644 index 0000000000..9b9d7cf000 --- /dev/null +++ b/lib/rdoc/markup/formatter_test_case.rb @@ -0,0 +1,341 @@ +require 'minitest/unit' +require 'rdoc/markup/formatter' + +## +# Test case for creating new RDoc::Markup formatters. See +# test/test_rdoc_markup_to_*.rb for examples. + +class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase + + def setup + super + + @m = RDoc::Markup.new + @am = RDoc::Markup::AttributeManager.new + @RM = RDoc::Markup + + @bullet_list = @RM::List.new(:BULLET, + @RM::ListItem.new(nil, @RM::Paragraph.new('l1')), + @RM::ListItem.new(nil, @RM::Paragraph.new('l2'))) + + @label_list = @RM::List.new(:LABEL, + @RM::ListItem.new('cat', @RM::Paragraph.new('cats are cool')), + @RM::ListItem.new('dog', @RM::Paragraph.new('dogs are cool too'))) + + @lalpha_list = @RM::List.new(:LALPHA, + @RM::ListItem.new(nil, @RM::Paragraph.new('l1')), + @RM::ListItem.new(nil, @RM::Paragraph.new('l2'))) + + @note_list = @RM::List.new(:NOTE, + @RM::ListItem.new('cat', @RM::Paragraph.new('cats are cool')), + @RM::ListItem.new('dog', @RM::Paragraph.new('dogs are cool too'))) + + @number_list = @RM::List.new(:NUMBER, + @RM::ListItem.new(nil, @RM::Paragraph.new('l1')), + @RM::ListItem.new(nil, @RM::Paragraph.new('l2'))) + + @ualpha_list = @RM::List.new(:UALPHA, + @RM::ListItem.new(nil, @RM::Paragraph.new('l1')), + @RM::ListItem.new(nil, @RM::Paragraph.new('l2'))) + end + + def self.add_visitor_tests + self.class_eval do + def test_start_accepting + @to.start_accepting + + start_accepting + end + + def test_end_accepting + @to.start_accepting + @to.res << 'hi' + + end_accepting + end + + def test_accept_blank_line + @to.start_accepting + + @to.accept_blank_line @RM::BlankLine.new + + accept_blank_line + end + + def test_accept_heading + @to.start_accepting + + @to.accept_heading @RM::Heading.new(5, 'Hello') + + accept_heading + end + + def test_accept_paragraph + @to.start_accepting + + @to.accept_paragraph @RM::Paragraph.new('hi') + + accept_paragraph + end + + def test_accept_verbatim + @to.start_accepting + + @to.accept_verbatim @RM::Verbatim.new(' ', 'hi', "\n", + ' ', 'world', "\n") + + accept_verbatim + end + + def test_accept_rule + @to.start_accepting + + @to.accept_rule @RM::Rule.new(4) + + accept_rule + end + + def test_accept_list_item_start_bullet + @to.start_accepting + + @to.accept_list_start @bullet_list + + @to.accept_list_item_start @bullet_list.items.first + + accept_list_item_start_bullet + end + + def test_accept_list_item_start_label + @to.start_accepting + + @to.accept_list_start @label_list + + @to.accept_list_item_start @label_list.items.first + + accept_list_item_start_label + end + + def test_accept_list_item_start_lalpha + @to.start_accepting + + @to.accept_list_start @lalpha_list + + @to.accept_list_item_start @lalpha_list.items.first + + accept_list_item_start_lalpha + end + + def test_accept_list_item_start_note + @to.start_accepting + + @to.accept_list_start @note_list + + @to.accept_list_item_start @note_list.items.first + + accept_list_item_start_note + end + + def test_accept_list_item_start_number + @to.start_accepting + + @to.accept_list_start @number_list + + @to.accept_list_item_start @number_list.items.first + + accept_list_item_start_number + end + + def test_accept_list_item_start_ualpha + @to.start_accepting + + @to.accept_list_start @ualpha_list + + @to.accept_list_item_start @ualpha_list.items.first + + accept_list_item_start_ualpha + end + + def test_accept_list_item_end_bullet + @to.start_accepting + + @to.accept_list_start @bullet_list + + @to.accept_list_item_start @bullet_list.items.first + + @to.accept_list_item_end @bullet_list.items.first + + accept_list_item_end_bullet + end + + def test_accept_list_item_end_label + @to.start_accepting + + @to.accept_list_start @label_list + + @to.accept_list_item_start @label_list.items.first + + @to.accept_list_item_end @label_list.items.first + + accept_list_item_end_label + end + + def test_accept_list_item_end_lalpha + @to.start_accepting + + @to.accept_list_start @lalpha_list + + @to.accept_list_item_start @lalpha_list.items.first + + @to.accept_list_item_end @lalpha_list.items.first + + accept_list_item_end_lalpha + end + + def test_accept_list_item_end_note + @to.start_accepting + + @to.accept_list_start @note_list + + @to.accept_list_item_start @note_list.items.first + + @to.accept_list_item_end @note_list.items.first + + accept_list_item_end_note + end + + def test_accept_list_item_end_number + @to.start_accepting + + @to.accept_list_start @number_list + + @to.accept_list_item_start @number_list.items.first + + @to.accept_list_item_end @number_list.items.first + + accept_list_item_end_number + end + + def test_accept_list_item_end_ualpha + @to.start_accepting + + @to.accept_list_start @ualpha_list + + @to.accept_list_item_start @ualpha_list.items.first + + @to.accept_list_item_end @ualpha_list.items.first + + accept_list_item_end_ualpha + end + + def test_accept_list_start_bullet + @to.start_accepting + + @to.accept_list_start @bullet_list + + accept_list_start_bullet + end + + def test_accept_list_start_label + @to.start_accepting + + @to.accept_list_start @label_list + + accept_list_start_label + end + + def test_accept_list_start_lalpha + @to.start_accepting + + @to.accept_list_start @lalpha_list + + accept_list_start_lalpha + end + + def test_accept_list_start_note + @to.start_accepting + + @to.accept_list_start @note_list + + accept_list_start_note + end + + def test_accept_list_start_number + @to.start_accepting + + @to.accept_list_start @number_list + + accept_list_start_number + end + + def test_accept_list_start_ualpha + @to.start_accepting + + @to.accept_list_start @ualpha_list + + accept_list_start_ualpha + end + + def test_accept_list_end_bullet + @to.start_accepting + + @to.accept_list_start @bullet_list + + @to.accept_list_end @bullet_list + + accept_list_end_bullet + end + + def test_accept_list_end_label + @to.start_accepting + + @to.accept_list_start @label_list + + @to.accept_list_end @label_list + + accept_list_end_label + end + + def test_accept_list_end_lalpha + @to.start_accepting + + @to.accept_list_start @lalpha_list + + @to.accept_list_end @lalpha_list + + accept_list_end_lalpha + end + + def test_accept_list_end_number + @to.start_accepting + + @to.accept_list_start @number_list + + @to.accept_list_end @number_list + + accept_list_end_number + end + + def test_accept_list_end_note + @to.start_accepting + + @to.accept_list_start @note_list + + @to.accept_list_end @note_list + + accept_list_end_note + end + + def test_accept_list_end_ualpha + @to.start_accepting + + @to.accept_list_start @ualpha_list + + @to.accept_list_end @ualpha_list + + accept_list_end_ualpha + end + end + end + +end + diff --git a/lib/rdoc/markup/fragments.rb b/lib/rdoc/markup/fragments.rb deleted file mode 100644 index 0031b809b4..0000000000 --- a/lib/rdoc/markup/fragments.rb +++ /dev/null @@ -1,337 +0,0 @@ -require 'rdoc/markup' -require 'rdoc/markup/lines' - -class RDoc::Markup - - ## - # A Fragment is a chunk of text, subclassed as a paragraph, a list - # entry, or verbatim text. - - class Fragment - attr_reader :level, :param, :txt - attr_accessor :type - - ## - # This is a simple factory system that lets us associate fragement - # types (a string) with a subclass of fragment - - TYPE_MAP = {} - - def self.type_name(name) - TYPE_MAP[name] = self - end - - def self.for(line) - klass = TYPE_MAP[line.type] || - raise("Unknown line type: '#{line.type.inspect}:' '#{line.text}'") - return klass.new(line.level, line.param, line.flag, line.text) - end - - def initialize(level, param, type, txt) - @level = level - @param = param - @type = type - @txt = "" - add_text(txt) if txt - end - - def add_text(txt) - @txt << " " if @txt.length > 0 - @txt << txt.tr_s("\n ", " ").strip - end - - def to_s - "L#@level: #{self.class.name.split('::')[-1]}\n#@txt" - end - - end - - ## - # A paragraph is a fragment which gets wrapped to fit. We remove all - # newlines when we're created, and have them put back on output. - - class Paragraph < Fragment - type_name :PARAGRAPH - end - - class BlankLine < Paragraph - type_name :BLANK - end - - class Heading < Paragraph - type_name :HEADING - - def head_level - @param.to_i - end - end - - ## - # A List is a fragment with some kind of label - - class ListBase < Paragraph - LIST_TYPES = [ - :BULLET, - :NUMBER, - :UPPERALPHA, - :LOWERALPHA, - :LABELED, - :NOTE, - ] - end - - class ListItem < ListBase - type_name :LIST - - def to_s - text = if [:NOTE, :LABELED].include? type then - "#{@param}: #{@txt}" - else - @txt - end - - "L#@level: #{type} #{self.class.name.split('::')[-1]}\n#{text}" - end - - end - - class ListStart < ListBase - def initialize(level, param, type) - super(level, param, type, nil) - end - end - - class ListEnd < ListBase - def initialize(level, type) - super(level, "", type, nil) - end - end - - ## - # Verbatim code contains lines that don't get wrapped. - - class Verbatim < Fragment - type_name :VERBATIM - - def add_text(txt) - @txt << txt.chomp << "\n" - end - - end - - ## - # A horizontal rule - - class Rule < Fragment - type_name :RULE - end - - ## - # Collect groups of lines together. Each group will end up containing a flow - # of text. - - class LineCollection - - def initialize - @fragments = [] - end - - def add(fragment) - @fragments << fragment - end - - def each(&b) - @fragments.each(&b) - end - - def to_a # :nodoc: - @fragments.map {|fragment| fragment.to_s} - end - - ## - # Factory for different fragment types - - def fragment_for(*args) - Fragment.for(*args) - end - - ## - # Tidy up at the end - - def normalize - change_verbatim_blank_lines - add_list_start_and_ends - add_list_breaks - tidy_blank_lines - end - - def to_s - @fragments.join("\n----\n") - end - - def accept(am, visitor) - visitor.start_accepting - - @fragments.each do |fragment| - case fragment - when Verbatim - visitor.accept_verbatim(am, fragment) - when Rule - visitor.accept_rule(am, fragment) - when ListStart - visitor.accept_list_start(am, fragment) - when ListEnd - visitor.accept_list_end(am, fragment) - when ListItem - visitor.accept_list_item(am, fragment) - when BlankLine - visitor.accept_blank_line(am, fragment) - when Heading - visitor.accept_heading(am, fragment) - when Paragraph - visitor.accept_paragraph(am, fragment) - end - end - - visitor.end_accepting - end - - private - - # If you have: - # - # normal paragraph text. - # - # this is code - # - # and more code - # - # You'll end up with the fragments Paragraph, BlankLine, Verbatim, - # BlankLine, Verbatim, BlankLine, etc. - # - # The BlankLine in the middle of the verbatim chunk needs to be changed to - # a real verbatim newline, and the two verbatim blocks merged - - def change_verbatim_blank_lines - frag_block = nil - blank_count = 0 - @fragments.each_with_index do |frag, i| - if frag_block.nil? - frag_block = frag if Verbatim === frag - else - case frag - when Verbatim - blank_count.times { frag_block.add_text("\n") } - blank_count = 0 - frag_block.add_text(frag.txt) - @fragments[i] = nil # remove out current fragment - when BlankLine - if frag_block - blank_count += 1 - @fragments[i] = nil - end - else - frag_block = nil - blank_count = 0 - end - end - end - @fragments.compact! - end - - ## - # List nesting is implicit given the level of indentation. Make it - # explicit, just to make life a tad easier for the output processors - - def add_list_start_and_ends - level = 0 - res = [] - type_stack = [] - - @fragments.each do |fragment| - # $stderr.puts "#{level} : #{fragment.class.name} : #{fragment.level}" - new_level = fragment.level - while (level < new_level) - level += 1 - type = fragment.type - res << ListStart.new(level, fragment.param, type) if type - type_stack.push type - # $stderr.puts "Start: #{level}" - end - - while level > new_level - type = type_stack.pop - res << ListEnd.new(level, type) if type - level -= 1 - # $stderr.puts "End: #{level}, #{type}" - end - - res << fragment - level = fragment.level - end - level.downto(1) do |i| - type = type_stack.pop - res << ListEnd.new(i, type) if type - end - - @fragments = res - end - - ## - # Inserts start/ends between list entries at the same level that have - # different element types - - def add_list_breaks - res = @fragments - - @fragments = [] - list_stack = [] - - res.each do |fragment| - case fragment - when ListStart - list_stack.push fragment - when ListEnd - start = list_stack.pop - fragment.type = start.type - when ListItem - l = list_stack.last - if fragment.type != l.type - @fragments << ListEnd.new(l.level, l.type) - start = ListStart.new(l.level, fragment.param, fragment.type) - @fragments << start - list_stack.pop - list_stack.push start - end - else - ; - end - @fragments << fragment - end - end - - ## - # Tidy up the blank lines: - # * change Blank/ListEnd into ListEnd/Blank - # * remove blank lines at the front - - def tidy_blank_lines - (@fragments.size - 1).times do |i| - if BlankLine === @fragments[i] and ListEnd === @fragments[i+1] then - @fragments[i], @fragments[i+1] = @fragments[i+1], @fragments[i] - end - end - - # remove leading blanks - @fragments.each_with_index do |f, i| - break unless f.kind_of? BlankLine - @fragments[i] = nil - end - - @fragments.compact! - end - - end - -end - diff --git a/lib/rdoc/markup/heading.rb b/lib/rdoc/markup/heading.rb new file mode 100644 index 0000000000..21e2574d68 --- /dev/null +++ b/lib/rdoc/markup/heading.rb @@ -0,0 +1,17 @@ +## +# A heading with a level (1-6) and text + +class RDoc::Markup::Heading < Struct.new :level, :text + + def accept visitor + visitor.accept_heading self + end + + def pretty_print q # :nodoc: + q.group 2, "[head: #{level} ", ']' do + q.pp text + end + end + +end + diff --git a/lib/rdoc/markup/inline.rb b/lib/rdoc/markup/inline.rb index 46c9b5822c..1b5eac45ae 100644 --- a/lib/rdoc/markup/inline.rb +++ b/lib/rdoc/markup/inline.rb @@ -1,5 +1,3 @@ -require 'rdoc/markup' - class RDoc::Markup ## @@ -7,6 +5,10 @@ class RDoc::Markup # value. class Attribute + + ## + # Special attribute type. See RDoc::Markup#add_special + SPECIAL = 1 @@name_to_bitmap = { :_SPECIAL_ => SPECIAL } @@ -37,17 +39,18 @@ class RDoc::Markup yield name.to_s if (bitmap & bit) != 0 end end + end - AttrChanger = Struct.new(:turn_on, :turn_off) + AttrChanger = Struct.new :turn_on, :turn_off # :nodoc: ## # An AttrChanger records a change in attributes. It contains a bitmap of the # attributes to turn on, and a bitmap of those to turn off. class AttrChanger - def to_s - "Attr: +#{Attribute.as_string(turn_on)}/-#{Attribute.as_string(turn_on)}" + def to_s # :nodoc: + "Attr: +#{Attribute.as_string turn_on}/-#{Attribute.as_string turn_on}" end end @@ -55,42 +58,66 @@ class RDoc::Markup # An array of attributes which parallels the characters in a string. class AttrSpan + + ## + # Creates a new AttrSpan for +length+ characters + def initialize(length) @attrs = Array.new(length, 0) end + ## + # Toggles +bits+ from +start+ to +length+ def set_attrs(start, length, bits) for i in start ... (start+length) @attrs[i] |= bits end end + ## + # Acccesses flags for character +n+ + def [](n) @attrs[n] end + end ## # Hold details of a special sequence class Special + + ## + # Special type + attr_reader :type + + ## + # Special text + attr_accessor :text + ## + # Creates a new special sequence of +type+ with +text+ + def initialize(type, text) @type, @text = type, text end + ## + # Specials are equal when the have the same text and type + def ==(o) self.text == o.text && self.type == o.type end - def inspect + def inspect # :nodoc: "#<RDoc::Markup::Special:0x%x @type=%p, name=%p @text=%p>" % [ object_id, @type, RDoc::Markup::Attribute.as_string(type), text.dump] end - def to_s + def to_s # :nodoc: "Special: type=#{type}, name=#{RDoc::Markup::Attribute.as_string type}, text=#{text.dump}" end @@ -98,4 +125,3 @@ class RDoc::Markup end -require 'rdoc/markup/attribute_manager' diff --git a/lib/rdoc/markup/lines.rb b/lib/rdoc/markup/lines.rb deleted file mode 100644 index 069492122f..0000000000 --- a/lib/rdoc/markup/lines.rb +++ /dev/null @@ -1,152 +0,0 @@ -class RDoc::Markup - - ## - # We store the lines we're working on as objects of class Line. These - # contain the text of the line, along with a flag indicating the line type, - # and an indentation level. - - class Line - INFINITY = 9999 - - LINE_TYPES = [ - :BLANK, - :HEADING, - :LIST, - :PARAGRAPH, - :RULE, - :VERBATIM, - ] - - # line type - attr_accessor :type - - # The indentation nesting level - attr_accessor :level - - # The contents - attr_accessor :text - - # A prefix or parameter. For LIST lines, this is - # the text that introduced the list item (the label) - attr_accessor :param - - # A flag. For list lines, this is the type of the list - attr_accessor :flag - - # the number of leading spaces - attr_accessor :leading_spaces - - # true if this line has been deleted from the list of lines - attr_accessor :deleted - - def initialize(text) - @text = text.dup - @deleted = false - - # expand tabs - 1 while @text.gsub!(/\t+/) { ' ' * (8*$&.length - $`.length % 8)} && $~ #` - - # Strip trailing whitespace - @text.sub!(/\s+$/, '') - - # and look for leading whitespace - if @text.length > 0 - @text =~ /^(\s*)/ - @leading_spaces = $1.length - else - @leading_spaces = INFINITY - end - end - - # Return true if this line is blank - def blank? - @text.empty? - end - - # stamp a line with a type, a level, a prefix, and a flag - def stamp(type, level, param="", flag=nil) - @type, @level, @param, @flag = type, level, param, flag - end - - ## - # Strip off the leading margin - - def strip_leading(size) - if @text.size > size - @text[0,size] = "" - else - @text = "" - end - end - - def to_s - "#@type#@level: #@text" - end - end - - ## - # A container for all the lines. - - class Lines - - include Enumerable - - attr_reader :lines # :nodoc: - - def initialize(lines) - @lines = lines - rewind - end - - def empty? - @lines.size.zero? - end - - def each - @lines.each do |line| - yield line unless line.deleted - end - end - -# def [](index) -# @lines[index] -# end - - def rewind - @nextline = 0 - end - - def next - begin - res = @lines[@nextline] - @nextline += 1 if @nextline < @lines.size - end while res and res.deleted and @nextline < @lines.size - res - end - - def unget - @nextline -= 1 - end - - def delete(a_line) - a_line.deleted = true - end - - def normalize - margin = @lines.collect{|l| l.leading_spaces}.min - margin = 0 if margin == :INFINITY - @lines.each {|line| line.strip_leading(margin) } if margin > 0 - end - - def as_text - @lines.map {|l| l.text}.join("\n") - end - - def line_types - @lines.map {|l| l.type } - end - - end - -end - diff --git a/lib/rdoc/markup/list.rb b/lib/rdoc/markup/list.rb new file mode 100644 index 0000000000..75326ed836 --- /dev/null +++ b/lib/rdoc/markup/list.rb @@ -0,0 +1,78 @@ +## +# A List of ListItems + +class RDoc::Markup::List + + ## + # The list's type + + attr_accessor :type + + ## + # Items in the list + + attr_reader :items + + ## + # Creates a new list of +type+ with +items+ + + def initialize type = nil, *items + @type = type + @items = [] + @items.push(*items) + end + + ## + # Appends +item+ to the list + + def << item + @items << item + end + + def == other # :nodoc: + self.class == other.class and + @type == other.type and + @items == other.items + end + + def accept visitor + visitor.accept_list_start self + + @items.each do |item| + item.accept visitor + end + + visitor.accept_list_end self + end + + ## + # Is the list empty? + + def empty? + @items.empty? + end + + ## + # Returns the last item in the list + + def last + @items.last + end + + def pretty_print q # :nodoc: + q.group 2, "[list: #{@type} ", ']' do + q.seplist @items do |item| + q.pp item + end + end + end + + ## + # Appends +items+ to the list + + def push *items + @items.push(*items) + end + +end + diff --git a/lib/rdoc/markup/list_item.rb b/lib/rdoc/markup/list_item.rb new file mode 100644 index 0000000000..500e814fe1 --- /dev/null +++ b/lib/rdoc/markup/list_item.rb @@ -0,0 +1,83 @@ +## +# An item within a List that contains paragraphs, headings, etc. + +class RDoc::Markup::ListItem + + ## + # The label for the ListItem + + attr_accessor :label + + ## + # Parts of the ListItem + + attr_reader :parts + + ## + # Creates a new ListItem with an optional +label+ containing +parts+ + + def initialize label = nil, *parts + @label = label + @parts = [] + @parts.push(*parts) + end + + ## + # Appends +part+ to the ListItem + + def << part + @parts << part + end + + def == other # :nodoc: + self.class == other.class and + @label == other.label and + @parts == other.parts + end + + def accept visitor + visitor.accept_list_item_start self + + @parts.each do |part| + part.accept visitor + end + + visitor.accept_list_item_end self + end + + ## + # Is the ListItem empty? + + def empty? + @parts.empty? + end + + ## + # Length of parts in the ListItem + + def length + @parts.length + end + + def pretty_print q # :nodoc: + q.group 2, '[item: ', ']' do + if @label then + q.text @label + q.breakable + end + + q.seplist @parts do |part| + q.pp part + end + end + end + + ## + # Adds +parts+ to the ListItem + + def push *parts + @parts.push(*parts) + end + +end + diff --git a/lib/rdoc/markup/paragraph.rb b/lib/rdoc/markup/paragraph.rb new file mode 100644 index 0000000000..bc23423dfc --- /dev/null +++ b/lib/rdoc/markup/paragraph.rb @@ -0,0 +1,66 @@ +## +# A Paragraph of text + +class RDoc::Markup::Paragraph + + ## + # The component parts of the list + + attr_reader :parts + + ## + # Creates a new Paragraph containing +parts+ + + def initialize *parts + @parts = [] + @parts.push(*parts) + end + + ## + # Appends +text+ to the Paragraph + + def << text + @parts << text + end + + def == other # :nodoc: + self.class == other.class and text == other.text + end + + def accept visitor + visitor.accept_paragraph self + end + + ## + # Appends +other+'s parts into this Paragraph + + def merge other + @parts.push(*other.parts) + end + + def pretty_print q # :nodoc: + self.class.name =~ /.*::(\w{4})/i + + q.group 2, "[#{$1.downcase}: ", ']' do + q.seplist @parts do |part| + q.pp part + end + end + end + + ## + # Appends +texts+ onto this Paragraph + + def push *texts + self.parts.push(*texts) + end + + ## + # The text of this paragraph + + def text + @parts.join ' ' + end + +end + diff --git a/lib/rdoc/markup/parser.rb b/lib/rdoc/markup/parser.rb new file mode 100644 index 0000000000..c0d6519fd5 --- /dev/null +++ b/lib/rdoc/markup/parser.rb @@ -0,0 +1,528 @@ +require 'strscan' +require 'rdoc/text' + +## +# A recursive-descent parser for RDoc markup. +# +# The parser tokenizes an input string then parses the tokens into a Document. +# Documents can be converted into output formats by writing a visitor like +# RDoc::Markup::ToHTML. +# +# The parser only handles the block-level constructs Paragraph, List, +# ListItem, Heading, Verbatim, BlankLine and Rule. Inline markup such as +# <tt>\+blah\+</tt> is handled separately by RDoc::Markup::AttributeManager. +# +# To see what markup the Parser implements read RDoc. To see how to use +# RDoc markup to format text in your program read RDoc::Markup. + +class RDoc::Markup::Parser + + include RDoc::Text + + ## + # List token types + + LIST_TOKENS = [ + :BULLET, + :LABEL, + :LALPHA, + :NOTE, + :NUMBER, + :UALPHA, + ] + + ## + # Parser error subclass + + class Error < RuntimeError; end + + ## + # Raised when the parser is unable to handle the given markup + + class ParseError < Error; end + + ## + # Enables display of debugging information + + attr_accessor :debug + + ## + # Token accessor + + attr_reader :tokens + + ## + # Parsers +str+ into a Document + + def self.parse str + parser = new + #parser.debug = true + parser.tokenize str + RDoc::Markup::Document.new(*parser.parse) + end + + ## + # Returns a token stream for +str+, for testing + + def self.tokenize str + parser = new + parser.tokenize str + parser.tokens + end + + ## + # Creates a new Parser. See also ::parse + + def initialize + @tokens = [] + @current_token = nil + @debug = false + + @line = 0 + @line_pos = 0 + end + + ## + # Builds a Heading of +level+ + + def build_heading level + heading = RDoc::Markup::Heading.new level, text + skip :NEWLINE + + heading + end + + ## + # Builds a List flush to +margin+ + + def build_list margin + p :list_start => margin if @debug + + list = RDoc::Markup::List.new + + until @tokens.empty? do + type, data, column, = get + + case type + when :BULLET, :LABEL, :LALPHA, :NOTE, :NUMBER, :UALPHA then + list_type = type + + if column < margin then + unget + break + end + + if list.type and list.type != list_type then + unget + break + end + + list.type = list_type + + case type + when :NOTE, :LABEL then + _, indent, = get # SPACE + if :NEWLINE == peek_token.first then + get + peek_type, new_indent, peek_column, = peek_token + indent = new_indent if + peek_type == :INDENT and peek_column >= column + unget + end + else + data = nil + _, indent, = get + end + + list_item = build_list_item(margin + indent, data) + + list << list_item if list_item + else + unget + break + end + end + + p :list_end => margin if @debug + + return nil if list.empty? + + list + end + + ## + # Builds a ListItem that is flush to +indent+ with type +item_type+ + + def build_list_item indent, item_type = nil + p :list_item_start => [indent, item_type] if @debug + + list_item = RDoc::Markup::ListItem.new item_type + + until @tokens.empty? do + type, data, column = get + + if column < indent and + not type == :NEWLINE and + (type != :INDENT or data < indent) then + unget + break + end + + case type + when :INDENT then + unget + list_item.push(*parse(indent)) + when :TEXT then + unget + list_item << build_paragraph(indent) + when :HEADER then + list_item << build_heading(data) + when :NEWLINE then + list_item << RDoc::Markup::BlankLine.new + when *LIST_TOKENS then + unget + list_item << build_list(column) + else + raise ParseError, "Unhandled token #{@current_token.inspect}" + end + end + + p :list_item_end => [indent, item_type] if @debug + + return nil if list_item.empty? + + list_item.parts.shift if + RDoc::Markup::BlankLine === list_item.parts.first and + list_item.length > 1 + + list_item + end + + ## + # Builds a Paragraph that is flush to +margin+ + + def build_paragraph margin + p :paragraph_start => margin if @debug + + paragraph = RDoc::Markup::Paragraph.new + + until @tokens.empty? do + type, data, column, = get + + case type + when :INDENT then + next if data == margin and peek_token[0] == :TEXT + + unget + break + when :TEXT then + if column != margin then + unget + break + end + + paragraph << data + skip :NEWLINE + else + unget + break + end + end + + p :paragraph_end => margin if @debug + + paragraph + end + + ## + # Builds a Verbatim that is flush to +margin+ + + def build_verbatim margin + p :verbatim_begin => margin if @debug + verbatim = RDoc::Markup::Verbatim.new + + until @tokens.empty? do + type, data, column, = get + + case type + when :INDENT then + if margin >= data then + unget + break + end + + indent = data - margin + + verbatim << ' ' * indent + when :HEADER then + verbatim << '=' * data + + _, _, peek_column, = peek_token + peek_column ||= column + data + verbatim << ' ' * (peek_column - column - data) + when :RULE then + width = 2 + data + verbatim << '-' * width + + _, _, peek_column, = peek_token + peek_column ||= column + data + 2 + verbatim << ' ' * (peek_column - column - width) + when :TEXT then + verbatim << data + when *LIST_TOKENS then + if column <= margin then + unget + break + end + + list_marker = case type + when :BULLET then '*' + when :LABEL then "[#{data}]" + when :LALPHA, :NUMBER, :UALPHA then "#{data}." + when :NOTE then "#{data}::" + end + + verbatim << list_marker + + _, data, = get + + verbatim << ' ' * (data - list_marker.length) + when :NEWLINE then + verbatim << data + break unless [:INDENT, :NEWLINE].include? peek_token[0] + else + unget + break + end + end + + verbatim.normalize + + p :verbatim_end => margin if @debug + + verbatim + end + + ## + # Pulls the next token from the stream. + + def get + @current_token = @tokens.shift + p :get => @current_token if @debug + @current_token + end + + ## + # Parses the tokens into a Document + + def parse indent = 0 + p :parse_start => indent if @debug + + document = [] + + until @tokens.empty? do + type, data, column, = get + + if type != :INDENT and column < indent then + unget + break + end + + case type + when :HEADER then + document << build_heading(data) + when :INDENT then + if indent > data then + unget + break + elsif indent == data then + next + end + + unget + document << build_verbatim(indent) + when :NEWLINE then + document << RDoc::Markup::BlankLine.new + skip :NEWLINE, false + when :RULE then + document << RDoc::Markup::Rule.new(data) + skip :NEWLINE + when :TEXT then + unget + document << build_paragraph(indent) + + # we're done with this paragraph (indent mismatch) + break if peek_token[0] == :TEXT + when *LIST_TOKENS then + unget + + list = build_list(indent) + + document << list if list + + # we're done with this list (indent mismatch) + break if LIST_TOKENS.include? peek_token.first and indent > 0 + else + type, data, column, line = @current_token + raise ParseError, + "Unhandled token #{type} (#{data.inspect}) at #{line}:#{column}" + end + end + + p :parse_end => indent if @debug + + document + end + + ## + # Returns the next token on the stream without modifying the stream + + def peek_token + token = @tokens.first || [] + p :peek => token if @debug + token + end + + ## + # Skips a token of +token_type+, optionally raising an error. + + def skip token_type, error = true + type, data, = get + + return unless type # end of stream + + return @current_token if token_type == type + + unget + + raise ParseError, "expected #{token_type} got #{@current_token.inspect}" if + error + end + + ## + # Consumes tokens until NEWLINE and turns them back into text + + def text + text = '' + + loop do + type, data, = get + + text << case type + when :BULLET then + _, space, = get # SPACE + "*#{' ' * (space - 1)}" + when :LABEL then + _, space, = get # SPACE + "[#{data}]#{' ' * (space - data.length - 2)}" + when :LALPHA, :NUMBER, :UALPHA then + _, space, = get # SPACE + "#{data}.#{' ' * (space - 2)}" + when :NOTE then + _, space = get # SPACE + "#{data}::#{' ' * (space - data.length - 2)}" + when :TEXT then + data + when :NEWLINE then + unget + break + when nil then + break + else + raise ParseError, "unhandled token #{@current_token.inspect}" + end + end + + text + end + + ## + # Calculates the column and line of the current token based on +offset+. + + def token_pos offset + [offset - @line_pos, @line] + end + + ## + # Turns text +input+ into a stream of tokens + + def tokenize input + s = StringScanner.new input + + @line = 0 + @line_pos = 0 + + until s.eos? do + pos = s.pos + + @tokens << case + when s.scan(/\r?\n/) then + token = [:NEWLINE, s.matched, *token_pos(pos)] + @line_pos = s.pos + @line += 1 + token + when s.scan(/ +/) then + [:INDENT, s.matched_size, *token_pos(pos)] + when s.scan(/(=+)\s+/) then + level = s[1].length + level = 6 if level > 6 + @tokens << [:HEADER, level, *token_pos(pos)] + + pos = s.pos + s.scan(/.*/) + [:TEXT, s.matched, *token_pos(pos)] + when s.scan(/^(-{3,}) *$/) then + [:RULE, s[1].length - 2, *token_pos(pos)] + when s.scan(/([*-])\s+/) then + @tokens << [:BULLET, :BULLET, *token_pos(pos)] + [:SPACE, s.matched_size, *token_pos(pos)] + when s.scan(/([a-z]|\d+)\.[ \t]+\S/i) then + list_label = s[1] + width = s.matched_size - 1 + + s.pos -= 1 # unget \S + + list_type = case list_label + when /[a-z]/ then :LALPHA + when /[A-Z]/ then :UALPHA + when /\d/ then :NUMBER + else + raise ParseError, "BUG token #{list_label}" + end + + @tokens << [list_type, list_label, *token_pos(pos)] + [:SPACE, width, *token_pos(pos)] + when s.scan(/\[(.*?)\]( +|$)/) then + @tokens << [:LABEL, s[1], *token_pos(pos)] + [:SPACE, s.matched_size, *token_pos(pos)] + when s.scan(/(.*?)::( +|$)/) then + @tokens << [:NOTE, s[1], *token_pos(pos)] + [:SPACE, s.matched_size, *token_pos(pos)] + else s.scan(/.*/) + [:TEXT, s.matched, *token_pos(pos)] + end + end + + self + end + + ## + # Returns the current token or +token+ to the token stream + + def unget token = @current_token + p :unget => token if @debug + raise Error, 'too many #ungets' if token == @tokens.first + @tokens.unshift token if token + end + +end + +require 'rdoc/markup/blank_line' +require 'rdoc/markup/document' +require 'rdoc/markup/heading' +require 'rdoc/markup/list' +require 'rdoc/markup/list_item' +require 'rdoc/markup/paragraph' +require 'rdoc/markup/rule' +require 'rdoc/markup/verbatim' + diff --git a/lib/rdoc/markup/preprocess.rb b/lib/rdoc/markup/preprocess.rb index 00dd4be4ad..a175d179cf 100644 --- a/lib/rdoc/markup/preprocess.rb +++ b/lib/rdoc/markup/preprocess.rb @@ -7,6 +7,10 @@ require 'rdoc/markup' class RDoc::Markup::PreProcess + ## + # Creates a new pre-processor for +input_file_name+ that will look for + # included files in +include_path+ + def initialize(input_file_name, include_path) @input_file_name = input_file_name @include_path = include_path @@ -44,15 +48,16 @@ class RDoc::Markup::PreProcess def include_file(name, indent) if full_name = find_include_file(name) then - content = File.open(full_name) {|f| f.read} + content = File.read full_name + # strip leading '#'s, but only if all lines start with them - if content =~ /^[^#]/ + if content =~ /^[^#]/ then content.gsub(/^/, indent) else content.gsub(/^#?/, indent) end else - $stderr.puts "Couldn't find file to include: '#{name}'" + $stderr.puts "Couldn't find file to include '#{name}' from #{@input_file_name}" '' end end diff --git a/lib/rdoc/markup/rule.rb b/lib/rdoc/markup/rule.rb new file mode 100644 index 0000000000..4fcd040d2b --- /dev/null +++ b/lib/rdoc/markup/rule.rb @@ -0,0 +1,17 @@ +## +# A horizontal rule with a weight + +class RDoc::Markup::Rule < Struct.new :weight + + def accept visitor + visitor.accept_rule self + end + + def pretty_print q # :nodoc: + q.group 2, '[rule:', ']' do + q.pp weight + end + end + +end + diff --git a/lib/rdoc/markup/to_ansi.rb b/lib/rdoc/markup/to_ansi.rb new file mode 100644 index 0000000000..9a5be8babb --- /dev/null +++ b/lib/rdoc/markup/to_ansi.rb @@ -0,0 +1,72 @@ +require 'rdoc/markup/inline' + +## +# Outputs RDoc markup with vibrant ANSI color! + +class RDoc::Markup::ToAnsi < RDoc::Markup::ToRdoc + + def initialize + super + + @headings.clear + @headings[1] = ["\e[1;32m", "\e[m"] + @headings[2] = ["\e[4;32m", "\e[m"] + @headings[3] = ["\e[32m", "\e[m"] + end + + ## + # Maps attributes to ANSI sequences + + def init_tags + add_tag :BOLD, "\e[1m", "\e[m" + add_tag :TT, "\e[7m", "\e[m" + add_tag :EM, "\e[4m", "\e[m" + end + + def accept_list_item_end list_item + width = case @list_type.last + when :BULLET then + 2 + when :NOTE, :LABEL then + @res << "\n" + 2 + else + bullet = @list_index.last.to_s + @list_index[-1] = @list_index.last.succ + bullet.length + 2 + end + + @indent -= width + end + + def accept_list_item_start list_item + bullet = case @list_type.last + when :BULLET then + '*' + when :NOTE, :LABEL then + attributes(list_item.label) + ":\n" + else + @list_index.last.to_s + '.' + end + + case @list_type.last + when :NOTE, :LABEL then + @indent += 2 + @prefix = bullet + (' ' * @indent) + else + @prefix = (' ' * @indent) + bullet.ljust(bullet.length + 1) + + width = bullet.gsub(/\e\[[\d;]*m/, '').length + 1 + + @indent += width + end + end + + def start_accepting + super + + @res = ["\e[0m"] + end + +end + diff --git a/lib/rdoc/markup/to_bs.rb b/lib/rdoc/markup/to_bs.rb new file mode 100644 index 0000000000..e7af129824 --- /dev/null +++ b/lib/rdoc/markup/to_bs.rb @@ -0,0 +1,74 @@ +require 'rdoc/markup/inline' + +## +# Outputs RDoc markup with hot backspace action! You will probably need a +# pager to use this output format. +# +# This formatter won't work on 1.8.6 because it lacks String#chars. + +class RDoc::Markup::ToBs < RDoc::Markup::ToRdoc + + def initialize + super + + @in_b = false + @in_em = false + end + + ## + # Sets a flag that is picked up by #annotate to do the right thing in + # #convert_string + + def init_tags + add_tag :BOLD, '+b', '-b' + add_tag :EM, '+_', '-_' + end + + def accept_heading heading + use_prefix or @res << ' ' * @indent + @res << @headings[heading.level][0] + @in_b = true + @res << attributes(heading.text) + @in_b = false + @res << @headings[heading.level][1] + @res << "\n" + end + + ## + # Turns on or off special handling for +convert_string+ + + def annotate tag + case tag + when '+b' then @in_b = true + when '-b' then @in_b = false + when '+_' then @in_em = true + when '-_' then @in_em = false + end + + '' + end + + ## + # Calls convert_string on the result of convert_special + + def convert_special special + convert_string super + end + + ## + # Adds bold or underline mixed with backspaces + + def convert_string string + return string unless string.respond_to? :chars # your ruby is lame + return string unless @in_b or @in_em + chars = if @in_b then + string.chars.map do |char| "#{char}\b#{char}" end + elsif @in_em then + string.chars.map do |char| "_\b#{char}" end + end + + chars.join + end + +end + diff --git a/lib/rdoc/markup/to_flow.rb b/lib/rdoc/markup/to_flow.rb deleted file mode 100644 index 3d87b3e9c3..0000000000 --- a/lib/rdoc/markup/to_flow.rb +++ /dev/null @@ -1,185 +0,0 @@ -require 'rdoc/markup/formatter' -require 'rdoc/markup/fragments' -require 'rdoc/markup/inline' -require 'cgi' - -class RDoc::Markup - - module Flow - P = Struct.new(:body) - VERB = Struct.new(:body) - RULE = Struct.new(:width) - class LIST - attr_reader :type, :contents - def initialize(type) - @type = type - @contents = [] - end - def <<(stuff) - @contents << stuff - end - end - LI = Struct.new(:label, :body) - H = Struct.new(:level, :text) - end - - class ToFlow < RDoc::Markup::Formatter - LIST_TYPE_TO_HTML = { - :BULLET => [ "<ul>", "</ul>" ], - :NUMBER => [ "<ol>", "</ol>" ], - :UPPERALPHA => [ "<ol>", "</ol>" ], - :LOWERALPHA => [ "<ol>", "</ol>" ], - :LABELED => [ "<dl>", "</dl>" ], - :NOTE => [ "<table>", "</table>" ], - } - - InlineTag = Struct.new(:bit, :on, :off) - - def initialize - super - - init_tags - end - - ## - # Set up the standard mapping of attributes to HTML tags - - def init_tags - @attr_tags = [ - InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:BOLD), "<b>", "</b>"), - InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:TT), "<tt>", "</tt>"), - InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:EM), "<em>", "</em>"), - ] - end - - ## - # Add a new set of HTML tags for an attribute. We allow separate start and - # end tags for flexibility - - def add_tag(name, start, stop) - @attr_tags << InlineTag.new(RDoc::Markup::Attribute.bitmap_for(name), start, stop) - end - - ## - # Given an HTML tag, decorate it with class information and the like if - # required. This is a no-op in the base class, but is overridden in HTML - # output classes that implement style sheets - - def annotate(tag) - tag - end - - ## - # Here's the client side of the visitor pattern - - def start_accepting - @res = [] - @list_stack = [] - end - - def end_accepting - @res - end - - def accept_paragraph(am, fragment) - @res << Flow::P.new((convert_flow(am.flow(fragment.txt)))) - end - - def accept_verbatim(am, fragment) - @res << Flow::VERB.new((convert_flow(am.flow(fragment.txt)))) - end - - def accept_rule(am, fragment) - size = fragment.param - size = 10 if size > 10 - @res << Flow::RULE.new(size) - end - - def accept_list_start(am, fragment) - @list_stack.push(@res) - list = Flow::LIST.new(fragment.type) - @res << list - @res = list - end - - def accept_list_end(am, fragment) - @res = @list_stack.pop - end - - def accept_list_item(am, fragment) - @res << Flow::LI.new(fragment.param, convert_flow(am.flow(fragment.txt))) - end - - def accept_blank_line(am, fragment) - # @res << annotate("<p />") << "\n" - end - - def accept_heading(am, fragment) - @res << Flow::H.new(fragment.head_level, convert_flow(am.flow(fragment.txt))) - end - - private - - def on_tags(res, item) - attr_mask = item.turn_on - return if attr_mask.zero? - - @attr_tags.each do |tag| - if attr_mask & tag.bit != 0 - res << annotate(tag.on) - end - end - end - - def off_tags(res, item) - attr_mask = item.turn_off - return if attr_mask.zero? - - @attr_tags.reverse_each do |tag| - if attr_mask & tag.bit != 0 - res << annotate(tag.off) - end - end - end - - def convert_flow(flow) - res = "" - flow.each do |item| - case item - when String - res << convert_string(item) - when AttrChanger - off_tags(res, item) - on_tags(res, item) - when Special - res << convert_special(item) - else - raise "Unknown flow element: #{item.inspect}" - end - end - res - end - - def convert_string(item) - CGI.escapeHTML(item) - end - - def convert_special(special) - handled = false - Attribute.each_name_of(special.type) do |name| - method_name = "handle_special_#{name}" - if self.respond_to? method_name - special.text = send(method_name, special) - handled = true - end - end - - raise "Unhandled special: #{special}" unless handled - - special.text - end - - end - -end - diff --git a/lib/rdoc/markup/to_html.rb b/lib/rdoc/markup/to_html.rb index 0165042d84..f060fd1c72 100644 --- a/lib/rdoc/markup/to_html.rb +++ b/lib/rdoc/markup/to_html.rb @@ -1,38 +1,28 @@ require 'rdoc/markup/formatter' -require 'rdoc/markup/fragments' require 'rdoc/markup/inline' require 'cgi' +## +# Outputs RDoc markup as HTML + class RDoc::Markup::ToHtml < RDoc::Markup::Formatter + ## + # Maps RDoc::Markup::Parser::LIST_TOKENS types to HTML tags + LIST_TYPE_TO_HTML = { - :BULLET => %w[<ul> </ul>], - :NUMBER => %w[<ol> </ol>], - :UPPERALPHA => %w[<ol> </ol>], - :LOWERALPHA => %w[<ol> </ol>], - :LABELED => %w[<dl> </dl>], - :NOTE => %w[<table> </table>], + :BULLET => ['<ul>', '</ul>'], + :LABEL => ['<dl>', '</dl>'], + :LALPHA => ['<ol style="display: lower-alpha">', '</ol>'], + :NOTE => ['<table>', '</table>'], + :NUMBER => ['<ol>', '</ol>'], + :UALPHA => ['<ol style="display: upper-alpha">', '</ol>'], } - InlineTag = Struct.new(:bit, :on, :off) - - def initialize - super - - # @in_tt - tt nested levels count - # @tt_bit - cache - @in_tt = 0 - @tt_bit = RDoc::Markup::Attribute.bitmap_for :TT - - # external hyperlinks - @markup.add_special(/((link:|https?:|mailto:|ftp:|www\.)\S+\w)/, :HYPERLINK) - - # and links of the form <text>[<url>] - @markup.add_special(/(((\{.*?\})|\b\S+?)\[\S+?\.\S+?\])/, :TIDYLINK) - - init_tags - end + attr_reader :res # :nodoc: + attr_reader :in_list_entry # :nodoc: + attr_reader :list # :nodoc: ## # Converts a target url to one that is relative to a given path @@ -44,6 +34,9 @@ class RDoc::Markup::ToHtml < RDoc::Markup::Formatter from = from.split "/" to = to.split "/" + from.delete '.' + to.delete '.' + while from.size > 0 and to.size > 0 and from[0] == to[0] do from.shift to.shift @@ -55,6 +48,31 @@ class RDoc::Markup::ToHtml < RDoc::Markup::Formatter File.join(*from) end + def initialize + super + + @th = nil + @in_list_entry = nil + @list = nil + + # external hyperlinks + @markup.add_special(/((link:|https?:|mailto:|ftp:|www\.)\S+\w)/, :HYPERLINK) + + # and links of the form <text>[<url>] + @markup.add_special(/(((\{.*?\})|\b\S+?)\[\S+?\.\S+?\])/, :TIDYLINK) + + init_tags + end + + ## + # Maps attributes to HTML tags + + def init_tags + add_tag :BOLD, "<b>", "</b>" + add_tag :TT, "<tt>", "</tt>" + add_tag :EM, "<em>", "</em>" + end + ## # Generate a hyperlink for url, labeled with text. Handle the # special cases for img: and link: described under handle_special_HYPERLINK @@ -113,195 +131,121 @@ class RDoc::Markup::ToHtml < RDoc::Markup::Formatter end ## - # are we currently inside <tt> tags? - - def in_tt? - @in_tt > 0 - end - - ## - # is +tag+ a <tt> tag? - - def tt?(tag) - tag.bit == @tt_bit - end - - ## - # Set up the standard mapping of attributes to HTML tags - - def init_tags - @attr_tags = [ - InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:BOLD), "<b>", "</b>"), - InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:TT), "<tt>", "</tt>"), - InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:EM), "<em>", "</em>"), - ] - end - - ## - # Add a new set of HTML tags for an attribute. We allow separate start and - # end tags for flexibility. + # This is a higher speed (if messier) version of wrap - def add_tag(name, start, stop) - @attr_tags << InlineTag.new(RDoc::Markup::Attribute.bitmap_for(name), start, stop) - end + def wrap(txt, line_len = 76) + res = [] + sp = 0 + ep = txt.length - ## - # Given an HTML tag, decorate it with class information and the like if - # required. This is a no-op in the base class, but is overridden in HTML - # output classes that implement style sheets. + while sp < ep + # scan back for a space + p = sp + line_len - 1 + if p >= ep + p = ep + else + while p > sp and txt[p] != ?\s + p -= 1 + end + if p <= sp + p = sp + line_len + while p < ep and txt[p] != ?\s + p += 1 + end + end + end + res << txt[sp...p] << "\n" + sp = p + sp += 1 while sp < ep and txt[sp] == ?\s + end - def annotate(tag) - tag + res.join end ## - # Here's the client side of the visitor pattern + # :section: Visitor def start_accepting - @res = "" + @res = [] @in_list_entry = [] + @list = [] end def end_accepting - @res + @res.join end - def accept_paragraph(am, fragment) + def accept_paragraph(paragraph) @res << annotate("<p>") + "\n" - @res << wrap(convert_flow(am.flow(fragment.txt))) + @res << wrap(convert_flow(@am.flow(paragraph.text))) @res << annotate("</p>") + "\n" end - def accept_verbatim(am, fragment) - @res << annotate("<pre>") + "\n" - @res << CGI.escapeHTML(fragment.txt) + def accept_verbatim(verbatim) + @res << annotate("<pre>") << "\n" + @res << CGI.escapeHTML(verbatim.text) @res << annotate("</pre>") << "\n" end - def accept_rule(am, fragment) - size = fragment.param + def accept_rule(rule) + size = rule.weight size = 10 if size > 10 - @res << "<hr size=\"#{size}\"></hr>" + @res << "<hr style=\"height: #{size}px\"></hr>" end - def accept_list_start(am, fragment) - @res << html_list_name(fragment.type, true) << "\n" + def accept_list_start(list) + @list << list.type + @res << html_list_name(list.type, true) << "\n" @in_list_entry.push false end - def accept_list_end(am, fragment) + def accept_list_end(list) + @list.pop if tag = @in_list_entry.pop @res << annotate(tag) << "\n" end - @res << html_list_name(fragment.type, false) << "\n" + @res << html_list_name(list.type, false) << "\n" end - def accept_list_item(am, fragment) + def accept_list_item_start(list_item) if tag = @in_list_entry.last @res << annotate(tag) << "\n" end - @res << list_item_start(am, fragment) - - @res << wrap(convert_flow(am.flow(fragment.txt))) << "\n" + @res << list_item_start(list_item, @list.last) + end - @in_list_entry[-1] = list_end_for(fragment.type) + def accept_list_item_end(list_item) + @in_list_entry[-1] = list_end_for(@list.last) end - def accept_blank_line(am, fragment) + def accept_blank_line(blank_line) # @res << annotate("<p />") << "\n" end - def accept_heading(am, fragment) - @res << convert_heading(fragment.head_level, am.flow(fragment.txt)) - end - - ## - # This is a higher speed (if messier) version of wrap - - def wrap(txt, line_len = 76) - res = "" - sp = 0 - ep = txt.length - while sp < ep - # scan back for a space - p = sp + line_len - 1 - if p >= ep - p = ep - else - while p > sp and txt[p] != ?\s - p -= 1 - end - if p <= sp - p = sp + line_len - while p < ep and txt[p] != ?\s - p += 1 - end - end - end - res << txt[sp...p] << "\n" - sp = p - sp += 1 while sp < ep and txt[sp] == ?\s - end - res + def accept_heading(heading) + @res << convert_heading(heading.level, @am.flow(heading.text)) end private - def on_tags(res, item) - attr_mask = item.turn_on - return if attr_mask.zero? - - @attr_tags.each do |tag| - if attr_mask & tag.bit != 0 - res << annotate(tag.on) - @in_tt += 1 if tt?(tag) - end - end - end - - def off_tags(res, item) - attr_mask = item.turn_off - return if attr_mask.zero? - - @attr_tags.reverse_each do |tag| - if attr_mask & tag.bit != 0 - @in_tt -= 1 if tt?(tag) - res << annotate(tag.off) - end - end - end - - def convert_flow(flow) - res = "" - - flow.each do |item| - case item - when String - res << convert_string(item) - when RDoc::Markup::AttrChanger - off_tags(res, item) - on_tags(res, item) - when RDoc::Markup::Special - res << convert_special(item) - else - raise "Unknown flow element: #{item.inspect}" - end - end - - res - end + ## + # Converts string +item+ def convert_string(item) in_tt? ? convert_string_simple(item) : convert_string_fancy(item) end + ## + # Escapes HTML in +item+ + def convert_string_simple(item) CGI.escapeHTML item end ## - # some of these patterns are taken from SmartyPants... + # Converts ampersand, dashes, elipsis, quotes, copyright and registered + # trademark symbols to HTML escaped Unicode. def convert_string_fancy(item) # convert ampersand before doing anything else @@ -333,69 +277,62 @@ class RDoc::Markup::ToHtml < RDoc::Markup::Formatter gsub(/\(r\)/, '®') end - def convert_special(special) - handled = false - RDoc::Markup::Attribute.each_name_of(special.type) do |name| - method_name = "handle_special_#{name}" - if self.respond_to? method_name - special.text = send(method_name, special) - handled = true - end - end - raise "Unhandled special: #{special}" unless handled - special.text - end + ## + # Converts headings to hN elements def convert_heading(level, flow) - res = - annotate("<h#{level}>") + - convert_flow(flow) + - annotate("</h#{level}>\n") + [annotate("<h#{level}>"), + convert_flow(flow), + annotate("</h#{level}>\n")].join end - def html_list_name(list_type, is_open_tag) - tags = LIST_TYPE_TO_HTML[list_type] || raise("Invalid list type: #{list_type.inspect}") - annotate(tags[ is_open_tag ? 0 : 1]) - end + ## + # Determins the HTML list element for +list_type+ and +open_tag+ - def list_item_start(am, fragment) - case fragment.type - when :BULLET, :NUMBER then - annotate("<li>") + def html_list_name(list_type, open_tag) + tags = LIST_TYPE_TO_HTML[list_type] + raise RDoc::Error, "Invalid list type: #{list_type.inspect}" unless tags + annotate tags[open_tag ? 0 : 1] + end - when :UPPERALPHA then - annotate("<li type=\"A\">") + ## + # Starts a list item - when :LOWERALPHA then - annotate("<li type=\"a\">") + def list_item_start(list_item, list_type) + case list_type + when :BULLET, :LALPHA, :NUMBER, :UALPHA then + annotate("<li>") - when :LABELED then + when :LABEL then annotate("<dt>") + - convert_flow(am.flow(fragment.param)) + + convert_flow(@am.flow(list_item.label)) + annotate("</dt>") + annotate("<dd>") when :NOTE then annotate("<tr>") + annotate("<td valign=\"top\">") + - convert_flow(am.flow(fragment.param)) + + convert_flow(@am.flow(list_item.label)) + annotate("</td>") + annotate("<td>") else - raise "Invalid list type" + raise RDoc::Error, "Invalid list type: #{list_type.inspect}" end end - def list_end_for(fragment_type) - case fragment_type - when :BULLET, :NUMBER, :UPPERALPHA, :LOWERALPHA then + ## + # Ends a list item + + def list_end_for(list_type) + case list_type + when :BULLET, :LALPHA, :NUMBER, :UALPHA then "</li>" - when :LABELED then + when :LABEL then "</dd>" when :NOTE then "</td></tr>" else - raise "Invalid list type" + raise RDoc::Error, "Invalid list type: #{list_type.inspect}" end end diff --git a/lib/rdoc/markup/to_html_crossref.rb b/lib/rdoc/markup/to_html_crossref.rb index 930218bb7e..1f62ee04f9 100644 --- a/lib/rdoc/markup/to_html_crossref.rb +++ b/lib/rdoc/markup/to_html_crossref.rb @@ -1,44 +1,35 @@ require 'rdoc/markup/to_html' ## -# Subclass of the RDoc::Markup::ToHtml class that supports looking up words in -# the AllReferences list. Those that are found (like AllReferences in this -# comment) will be hyperlinked +# Subclass of the RDoc::Markup::ToHtml class that supports looking up words +# from a context. Those that are found will be hyperlinked. class RDoc::Markup::ToHtmlCrossref < RDoc::Markup::ToHtml - attr_accessor :context - - # Regular expressions to match class and method references. - # - # 1.) There can be a '\' in front of text to suppress - # any cross-references (note, however, that the single '\' - # is written as '\\\\' in order to escape it twice, once - # in the Ruby String literal and once in the regexp). - # 2.) There can be a '::' in front of class names to reference - # from the top-level namespace. - # 3.) The method can be followed by parenthesis, - # which may or may not have things inside (this - # apparently is allowed for Fortran 95, but I also think that this - # is a good idea for Ruby, as it is very reasonable to want to - # reference a call with arguments). - # - # NOTE: In order to support Fortran 95 properly, the [A-Z] below - # should be changed to [A-Za-z]. This slows down rdoc significantly, - # however, and the Fortran 95 support is broken in any case due to - # the return in handle_special_CROSSREF if the token consists - # entirely of lowercase letters. + ## + # Regular expression to match class references # - # The markup/cross-referencing engine needs a rewrite for - # Fortran 95 to be supported properly. + # 1) There can be a '\' in front of text to suppress any cross-references + # 2) There can be a '::' in front of class names to reference from the + # top-level namespace. + # 3) The method can be followed by parenthesis + CLASS_REGEXP_STR = '\\\\?((?:\:{2})?[A-Z]\w*(?:\:\:\w+)*)' - METHOD_REGEXP_STR = '(\w+[!?=]?)(?:\([\.\w+\*\/\+\-\=\<\>]*\))?' + ## + # Regular expression to match method references. + # + # See CLASS_REGEXP_STR + + METHOD_REGEXP_STR = '(\w+[!?=]?)(?:\([\w.+*/=<>-]*\))?' + + ## # Regular expressions matching text that should potentially have - # cross-reference links generated are passed to add_special. - # Note that these expressions are meant to pick up text for which - # cross-references have been suppressed, since the suppression - # characters are removed by the code that is triggered. + # cross-reference links generated are passed to add_special. Note that + # these expressions are meant to pick up text for which cross-references + # have been suppressed, since the suppression characters are removed by the + # code that is triggered. + CROSSREF_REGEXP = /( # A::B::C.meth #{CLASS_REGEXP_STR}[\.\#]#{METHOD_REGEXP_STR} @@ -72,8 +63,14 @@ class RDoc::Markup::ToHtmlCrossref < RDoc::Markup::ToHtml )/x ## - # We need to record the html path of our caller so we can generate - # correct relative paths for any hyperlinks that we find + # RDoc::CodeObject for generating references + + attr_accessor :context + + ## + # Creates a new crossref resolver that generates links relative to +context+ + # which lives at +from_path+ in the generated files. '#' characters on + # references are removed unless +show_hash+ is true. def initialize(from_path, context, show_hash) raise ArgumentError, 'from_path cannot be nil' if from_path.nil? @@ -89,19 +86,18 @@ class RDoc::Markup::ToHtmlCrossref < RDoc::Markup::ToHtml end ## - # We're invoked when any text matches the CROSSREF pattern - # (defined in MarkUp). If we fine the corresponding reference, - # generate a hyperlink. If the name we're looking for contains - # no punctuation, we look for it up the module/class chain. For - # example, HyperlinkHtml is found, even without the Generator:: - # prefix, because we look for it in module Generator first. + # We're invoked when any text matches the CROSSREF pattern (defined in + # MarkUp). If we find the corresponding reference, generate a hyperlink. + # If the name we're looking for contains no punctuation, we look for it up + # the module/class chain. For example, HyperlinkHtml is found, even without + # the Generator:: prefix, because we look for it in module Generator first. def handle_special_CROSSREF(special) name = special.text # This ensures that words entirely consisting of lowercase letters will - # not have cross-references generated (to suppress lots of - # erroneous cross-references to "new" in text, for instance) + # not have cross-references generated (to suppress lots of erroneous + # cross-references to "new" in text, for instance) return name if name =~ /\A[a-z]*\z/ return @seen[name] if @seen.include? name @@ -113,7 +109,6 @@ class RDoc::Markup::ToHtmlCrossref < RDoc::Markup::ToHtml lookup = name end - # Find class, module, or method in class or module. # # Do not, however, use an if/elsif/else chain to do so. Instead, test @@ -132,7 +127,9 @@ class RDoc::Markup::ToHtmlCrossref < RDoc::Markup::ToHtml ref = @context.find_symbol lookup unless ref - out = if lookup =~ /^\\/ then + out = if lookup == '\\' then + lookup + elsif lookup =~ /^\\/ then $' elsif ref and ref.document_self then "<a href=\"#{ref.as_href(@from_path)}\">#{name}</a>" @@ -146,3 +143,4 @@ class RDoc::Markup::ToHtmlCrossref < RDoc::Markup::ToHtml end end + diff --git a/lib/rdoc/markup/to_latex.rb b/lib/rdoc/markup/to_latex.rb deleted file mode 100644 index bbf958f2ed..0000000000 --- a/lib/rdoc/markup/to_latex.rb +++ /dev/null @@ -1,328 +0,0 @@ -require 'rdoc/markup/formatter' -require 'rdoc/markup/fragments' -require 'rdoc/markup/inline' - -require 'cgi' - -## -# Convert SimpleMarkup to basic LaTeX report format. - -class RDoc::Markup::ToLaTeX < RDoc::Markup::Formatter - - BS = "\020" # \ - OB = "\021" # { - CB = "\022" # } - DL = "\023" # Dollar - - BACKSLASH = "#{BS}symbol#{OB}92#{CB}" - HAT = "#{BS}symbol#{OB}94#{CB}" - BACKQUOTE = "#{BS}symbol#{OB}0#{CB}" - TILDE = "#{DL}#{BS}sim#{DL}" - LESSTHAN = "#{DL}<#{DL}" - GREATERTHAN = "#{DL}>#{DL}" - - def self.l(str) - str.tr('\\', BS).tr('{', OB).tr('}', CB).tr('$', DL) - end - - def l(arg) - RDoc::Markup::ToLaTeX.l(arg) - end - - LIST_TYPE_TO_LATEX = { - :BULLET => [ l("\\begin{itemize}"), l("\\end{itemize}") ], - :NUMBER => [ l("\\begin{enumerate}"), l("\\end{enumerate}"), "\\arabic" ], - :UPPERALPHA => [ l("\\begin{enumerate}"), l("\\end{enumerate}"), "\\Alph" ], - :LOWERALPHA => [ l("\\begin{enumerate}"), l("\\end{enumerate}"), "\\alph" ], - :LABELED => [ l("\\begin{description}"), l("\\end{description}") ], - :NOTE => [ - l("\\begin{tabularx}{\\linewidth}{@{} l X @{}}"), - l("\\end{tabularx}") ], - } - - InlineTag = Struct.new(:bit, :on, :off) - - def initialize - init_tags - @list_depth = 0 - @prev_list_types = [] - end - - ## - # Set up the standard mapping of attributes to LaTeX - - def init_tags - @attr_tags = [ - InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:BOLD), l("\\textbf{"), l("}")), - InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:TT), l("\\texttt{"), l("}")), - InlineTag.new(RDoc::Markup::Attribute.bitmap_for(:EM), l("\\emph{"), l("}")), - ] - end - - ## - # Escape a LaTeX string - - def escape(str) - $stderr.print "FE: ", str if $DEBUG_RDOC - s = str. - sub(/\s+$/, ''). - gsub(/([_\${}&%#])/, "#{BS}\\1"). - gsub(/\\/, BACKSLASH). - gsub(/\^/, HAT). - gsub(/~/, TILDE). - gsub(/</, LESSTHAN). - gsub(/>/, GREATERTHAN). - gsub(/,,/, ",{},"). - gsub(/\`/, BACKQUOTE) - $stderr.print "-> ", s, "\n" if $DEBUG_RDOC - s - end - - ## - # Add a new set of LaTeX tags for an attribute. We allow - # separate start and end tags for flexibility - - def add_tag(name, start, stop) - @attr_tags << InlineTag.new(RDoc::Markup::Attribute.bitmap_for(name), start, stop) - end - - ## - # Here's the client side of the visitor pattern - - def start_accepting - @res = "" - @in_list_entry = [] - end - - def end_accepting - @res.tr(BS, '\\').tr(OB, '{').tr(CB, '}').tr(DL, '$') - end - - def accept_paragraph(am, fragment) - @res << wrap(convert_flow(am.flow(fragment.txt))) - @res << "\n" - end - - def accept_verbatim(am, fragment) - @res << "\n\\begin{code}\n" - @res << fragment.txt.sub(/[\n\s]+\Z/, '') - @res << "\n\\end{code}\n\n" - end - - def accept_rule(am, fragment) - size = fragment.param - size = 10 if size > 10 - @res << "\n\n\\rule{\\linewidth}{#{size}pt}\n\n" - end - - def accept_list_start(am, fragment) - @res << list_name(fragment.type, true) << "\n" - @in_list_entry.push false - end - - def accept_list_end(am, fragment) - if tag = @in_list_entry.pop - @res << tag << "\n" - end - @res << list_name(fragment.type, false) << "\n" - end - - def accept_list_item(am, fragment) - if tag = @in_list_entry.last - @res << tag << "\n" - end - @res << list_item_start(am, fragment) - @res << wrap(convert_flow(am.flow(fragment.txt))) << "\n" - @in_list_entry[-1] = list_end_for(fragment.type) - end - - def accept_blank_line(am, fragment) - # @res << "\n" - end - - def accept_heading(am, fragment) - @res << convert_heading(fragment.head_level, am.flow(fragment.txt)) - end - - ## - # This is a higher speed (if messier) version of wrap - - def wrap(txt, line_len = 76) - res = "" - sp = 0 - ep = txt.length - while sp < ep - # scan back for a space - p = sp + line_len - 1 - if p >= ep - p = ep - else - while p > sp and txt[p] != ?\s - p -= 1 - end - if p <= sp - p = sp + line_len - while p < ep and txt[p] != ?\s - p += 1 - end - end - end - res << txt[sp...p] << "\n" - sp = p - sp += 1 while sp < ep and txt[sp] == ?\s - end - res - end - - private - - def on_tags(res, item) - attr_mask = item.turn_on - return if attr_mask.zero? - - @attr_tags.each do |tag| - if attr_mask & tag.bit != 0 - res << tag.on - end - end - end - - def off_tags(res, item) - attr_mask = item.turn_off - return if attr_mask.zero? - - @attr_tags.reverse_each do |tag| - if attr_mask & tag.bit != 0 - res << tag.off - end - end - end - - def convert_flow(flow) - res = "" - flow.each do |item| - case item - when String - $stderr.puts "Converting '#{item}'" if $DEBUG_RDOC - res << convert_string(item) - when AttrChanger - off_tags(res, item) - on_tags(res, item) - when Special - res << convert_special(item) - else - raise "Unknown flow element: #{item.inspect}" - end - end - res - end - - ## - # some of these patterns are taken from SmartyPants... - - def convert_string(item) - escape(item). - - # convert ... to elipsis (and make sure .... becomes .<elipsis>) - gsub(/\.\.\.\./, '.\ldots{}').gsub(/\.\.\./, '\ldots{}'). - - # convert single closing quote - gsub(%r{([^ \t\r\n\[\{\(])\'}, '\1\''). - gsub(%r{\'(?=\W|s\b)}, "'" ). - - # convert single opening quote - gsub(/'/, '`'). - - # convert double closing quote - gsub(%r{([^ \t\r\n\[\{\(])\"(?=\W)}, "\\1''"). - - # convert double opening quote - gsub(/"/, "``"). - - # convert copyright - gsub(/\(c\)/, '\copyright{}') - - end - - def convert_special(special) - handled = false - Attribute.each_name_of(special.type) do |name| - method_name = "handle_special_#{name}" - if self.respond_to? method_name - special.text = send(method_name, special) - handled = true - end - end - raise "Unhandled special: #{special}" unless handled - special.text - end - - def convert_heading(level, flow) - res = - case level - when 1 then "\\chapter{" - when 2 then "\\section{" - when 3 then "\\subsection{" - when 4 then "\\subsubsection{" - else "\\paragraph{" - end + - convert_flow(flow) + - "}\n" - end - - def list_name(list_type, is_open_tag) - tags = LIST_TYPE_TO_LATEX[list_type] || raise("Invalid list type: #{list_type.inspect}") - if tags[2] # enumerate - if is_open_tag - @list_depth += 1 - if @prev_list_types[@list_depth] != tags[2] - case @list_depth - when 1 - roman = "i" - when 2 - roman = "ii" - when 3 - roman = "iii" - when 4 - roman = "iv" - else - raise("Too deep list: level #{@list_depth}") - end - @prev_list_types[@list_depth] = tags[2] - return l("\\renewcommand{\\labelenum#{roman}}{#{tags[2]}{enum#{roman}}}") + "\n" + tags[0] - end - else - @list_depth -= 1 - end - end - tags[ is_open_tag ? 0 : 1] - end - - def list_item_start(am, fragment) - case fragment.type - when :BULLET, :NUMBER, :UPPERALPHA, :LOWERALPHA then - "\\item " - - when :LABELED then - "\\item[" + convert_flow(am.flow(fragment.param)) + "] " - - when :NOTE then - convert_flow(am.flow(fragment.param)) + " & " - else - raise "Invalid list type" - end - end - - def list_end_for(fragment_type) - case fragment_type - when :BULLET, :NUMBER, :UPPERALPHA, :LOWERALPHA, :LABELED then - "" - when :NOTE - "\\\\\n" - else - raise "Invalid list type" - end - end - -end - diff --git a/lib/rdoc/markup/to_rdoc.rb b/lib/rdoc/markup/to_rdoc.rb new file mode 100644 index 0000000000..97c7d0c984 --- /dev/null +++ b/lib/rdoc/markup/to_rdoc.rb @@ -0,0 +1,243 @@ +require 'rdoc/markup/inline' + +## +# Outputs RDoc markup as RDoc markup! (mostly) + +class RDoc::Markup::ToRdoc < RDoc::Markup::Formatter + + attr_accessor :indent + attr_reader :list_index + attr_reader :list_type + attr_reader :list_width + attr_reader :prefix + attr_reader :res + + def initialize + super + + @markup.add_special(/\\[^\s]/, :SUPPRESSED_CROSSREF) + + @width = 78 + @prefix = '' + + init_tags + + @headings = {} + @headings.default = [] + + @headings[1] = ['= ', ''] + @headings[2] = ['== ', ''] + @headings[3] = ['=== ', ''] + @headings[4] = ['==== ', ''] + @headings[5] = ['===== ', ''] + @headings[6] = ['====== ', ''] + end + + ## + # Maps attributes to ANSI sequences + + def init_tags + add_tag :BOLD, "<b>", "</b>" + add_tag :TT, "<tt>", "</tt>" + add_tag :EM, "<em>", "</em>" + end + + def accept_blank_line blank_line + @res << "\n" + end + + def accept_heading heading + use_prefix or @res << ' ' * @indent + @res << @headings[heading.level][0] + @res << attributes(heading.text) + @res << @headings[heading.level][1] + @res << "\n" + end + + def accept_list_end list + @list_index.pop + @list_type.pop + @list_width.pop + end + + def accept_list_item_end list_item + width = case @list_type.last + when :BULLET then + 2 + when :NOTE, :LABEL then + @res << "\n" + 2 + else + bullet = @list_index.last.to_s + @list_index[-1] = @list_index.last.succ + bullet.length + 2 + end + + @indent -= width + end + + def accept_list_item_start list_item + bullet = case @list_type.last + when :BULLET then + '*' + when :NOTE, :LABEL then + attributes(list_item.label) + ":\n" + else + @list_index.last.to_s + '.' + end + + case @list_type.last + when :NOTE, :LABEL then + @indent += 2 + @prefix = bullet + (' ' * @indent) + else + @prefix = (' ' * @indent) + bullet.ljust(bullet.length + 1) + + width = bullet.length + 1 + + @indent += width + end + end + + def accept_list_start list + case list.type + when :BULLET then + @list_index << nil + @list_width << 1 + when :LABEL, :NOTE then + @list_index << nil + @list_width << 2 + when :LALPHA then + @list_index << 'a' + @list_width << list.items.length.to_s.length + when :NUMBER then + @list_index << 1 + @list_width << list.items.length.to_s.length + when :UALPHA then + @list_index << 'A' + @list_width << list.items.length.to_s.length + else + raise RDoc::Error, "invalid list type #{list.type}" + end + + @list_type << list.type + end + + def accept_paragraph paragraph + wrap attributes(paragraph.text) + end + + def accept_rule rule + use_prefix or @res << ' ' * @indent + @res << '-' * (@width - @indent) + @res << "\n" + end + + ## + # Outputs +verbatim+ flush left and indented 2 columns + + def accept_verbatim verbatim + indent = ' ' * (@indent + 2) + + lines = [] + current_line = [] + + # split into lines + verbatim.parts.each do |part| + current_line << part + + if part == "\n" then + lines << current_line + current_line = [] + end + end + + lines << current_line unless current_line.empty? + + # calculate margin + indented = lines.select { |line| line != ["\n"] } + margin = indented.map { |line| line.first.length }.min + + # flush left + indented.each { |line| line[0][0...margin] = '' } + + # output + use_prefix or @res << indent # verbatim is unlikely to have prefix + @res << lines.shift.join + + lines.each do |line| + @res << indent unless line == ["\n"] + @res << line.join + end + + @res << "\n" + end + + def attributes text + flow = @am.flow text.dup + convert_flow flow + end + + def end_accepting + @res.join + end + + def handle_special_SUPPRESSED_CROSSREF special + special.text.sub(/\\/, '') + end + + def start_accepting + @res = [""] + @indent = 0 + @prefix = nil + + @list_index = [] + @list_type = [] + @list_width = [] + end + + def use_prefix + prefix = @prefix + @prefix = nil + @res << prefix if prefix + + prefix + end + + def wrap text + return unless text && !text.empty? + + text_len = @width - @indent + + text_len = 20 if text_len < 20 + + re = /^(.{0,#{text_len}})[ \n]/ + next_prefix = ' ' * @indent + + prefix = @prefix || next_prefix + @prefix = nil + + @res << prefix + + while text.length > text_len + if text =~ re then + @res << $1 + text.slice!(0, $&.length) + else + @res << text.slice!(0, text_len) + end + + @res << "\n" << next_prefix + end + + if text.empty? then + @res.pop + @res.pop + else + @res << text + @res << "\n" + end + end + +end + diff --git a/lib/rdoc/markup/to_test.rb b/lib/rdoc/markup/to_test.rb index ce6aff6e9a..0afdb96a18 100644 --- a/lib/rdoc/markup/to_test.rb +++ b/lib/rdoc/markup/to_test.rb @@ -6,44 +6,58 @@ require 'rdoc/markup/formatter' class RDoc::Markup::ToTest < RDoc::Markup::Formatter + ## + # :section: Visitor + def start_accepting @res = [] + @list = [] end def end_accepting @res end - def accept_paragraph(am, fragment) - @res << fragment.to_s + def accept_paragraph(paragraph) + @res << paragraph.text + end + + def accept_verbatim(verbatim) + @res << verbatim.text end - def accept_verbatim(am, fragment) - @res << fragment.to_s + def accept_list_start(list) + @list << case list.type + when :BULLET then + '*' + when :NUMBER then + '1' + else + list.type + end end - def accept_list_start(am, fragment) - @res << fragment.to_s + def accept_list_end(list) + @list.pop end - def accept_list_end(am, fragment) - @res << fragment.to_s + def accept_list_item_start(list_item) + @res << "#{' ' * (@list.size - 1)}#{@list.last}: " end - def accept_list_item(am, fragment) - @res << fragment.to_s + def accept_list_item_end(list_item) end - def accept_blank_line(am, fragment) - @res << fragment.to_s + def accept_blank_line(blank_line) + @res << "\n" end - def accept_heading(am, fragment) - @res << fragment.to_s + def accept_heading(heading) + @res << "#{'=' * heading.level} #{heading.text}" end - def accept_rule(am, fragment) - @res << fragment.to_s + def accept_rule(rule) + @res << '-' * rule.weight end end diff --git a/lib/rdoc/markup/to_texinfo.rb b/lib/rdoc/markup/to_texinfo.rb deleted file mode 100644 index 65a1608c4d..0000000000 --- a/lib/rdoc/markup/to_texinfo.rb +++ /dev/null @@ -1,69 +0,0 @@ -require 'rdoc/markup/formatter' -require 'rdoc/markup/fragments' -require 'rdoc/markup/inline' - -require 'rdoc/markup' -require 'rdoc/markup/formatter' - -## -# Convert SimpleMarkup to basic TexInfo format -# -# TODO: WTF is AttributeManager for? -# -class RDoc::Markup::ToTexInfo < RDoc::Markup::Formatter - - def start_accepting - @text = [] - end - - def end_accepting - @text.join("\n") - end - - def accept_paragraph(attributes, text) - @text << format(text) - end - - def accept_verbatim(attributes, text) - @text << "@verb{|#{format(text)}|}" - end - - def accept_heading(attributes, text) - heading = ['@majorheading', '@chapheading'][text.head_level - 1] || '@heading' - @text << "#{heading} #{format(text)}" - end - - def accept_list_start(attributes, text) - @text << '@itemize @bullet' - end - - def accept_list_end(attributes, text) - @text << '@end itemize' - end - - def accept_list_item(attributes, text) - @text << "@item\n#{format(text)}" - end - - def accept_blank_line(attributes, text) - @text << "\n" - end - - def accept_rule(attributes, text) - @text << '-----' - end - - def format(text) - text.txt. - gsub(/@/, "@@"). - gsub(/\{/, "@{"). - gsub(/\}/, "@}"). - # gsub(/,/, "@,"). # technically only required in cross-refs - gsub(/\+([\w]+)\+/, "@code{\\1}"). - gsub(/\<tt\>([^<]+)\<\/tt\>/, "@code{\\1}"). - gsub(/\*([\w]+)\*/, "@strong{\\1}"). - gsub(/\<b\>([^<]+)\<\/b\>/, "@strong{\\1}"). - gsub(/_([\w]+)_/, "@emph{\\1}"). - gsub(/\<em\>([^<]+)\<\/em\>/, "@emph{\\1}") - end -end diff --git a/lib/rdoc/markup/verbatim.rb b/lib/rdoc/markup/verbatim.rb new file mode 100644 index 0000000000..faf539a723 --- /dev/null +++ b/lib/rdoc/markup/verbatim.rb @@ -0,0 +1,42 @@ +## +# A section of verbatim text + +class RDoc::Markup::Verbatim < RDoc::Markup::Paragraph + + def accept visitor + visitor.accept_verbatim self + end + + ## + # Collapses 3+ newlines into two newlines + + def normalize + parts = [] + + newlines = 0 + + @parts.each do |part| + case part + when /\n/ then + newlines += 1 + parts << part if newlines <= 2 + else + newlines = 0 + parts << part + end + end + + parts.slice!(-1) if parts[-2..-1] == ["\n", "\n"] + + @parts = parts + end + + ## + # The text of the section + + def text + @parts.join + end + +end + |