1##
2# A comment holds the text comment for a RDoc::CodeObject and provides a
3# unified way of cleaning it up and parsing it into an RDoc::Markup::Document.
4#
5# Each comment may have a different markup format set by #format=.  By default
6# 'rdoc' is used.  The :markup: directive tells RDoc which format to use.
7#
8# See RDoc::Markup@Other+directives for instructions on adding an alternate
9# format.
10
11class RDoc::Comment
12
13  include RDoc::Text
14
15  ##
16  # The format of this comment.  Defaults to RDoc::Markup
17
18  attr_reader :format
19
20  ##
21  # The RDoc::TopLevel this comment was found in
22
23  attr_accessor :location
24
25  ##
26  # For duck-typing when merging classes at load time
27
28  alias file location # :nodoc:
29
30  ##
31  # The text for this comment
32
33  attr_reader :text
34
35  ##
36  # Overrides the content returned by #parse.  Use when there is no #text
37  # source for this comment
38
39  attr_writer   :document
40
41  ##
42  # Creates a new comment with +text+ that is found in the RDoc::TopLevel
43  # +location+.
44
45  def initialize text = nil, location = nil
46    @location = location
47    @text     = text
48
49    @document   = nil
50    @format     = 'rdoc'
51    @normalized = false
52  end
53
54  ##
55  #--
56  # TODO deep copy @document
57
58  def initialize_copy copy # :nodoc:
59    @text = copy.text.dup
60  end
61
62  def == other # :nodoc:
63    self.class === other and
64      other.text == @text and other.location == @location
65  end
66
67  ##
68  # Look for a 'call-seq' in the comment to override the normal parameter
69  # handling.  The :call-seq: is indented from the baseline.  All lines of the
70  # same indentation level and prefix are consumed.
71  #
72  # For example, all of the following will be used as the :call-seq:
73  #
74  #   # :call-seq:
75  #   #   ARGF.readlines(sep=$/)     -> array
76  #   #   ARGF.readlines(limit)      -> array
77  #   #   ARGF.readlines(sep, limit) -> array
78  #   #
79  #   #   ARGF.to_a(sep=$/)     -> array
80  #   #   ARGF.to_a(limit)      -> array
81  #   #   ARGF.to_a(sep, limit) -> array
82
83  def extract_call_seq method
84    # we must handle situations like the above followed by an unindented first
85    # comment.  The difficulty is to make sure not to match lines starting
86    # with ARGF at the same indent, but that are after the first description
87    # paragraph.
88    if @text =~ /^\s*:?call-seq:(.*?(?:\S).*?)^\s*$/m then
89      all_start, all_stop = $~.offset(0)
90      seq_start, seq_stop = $~.offset(1)
91
92      # we get the following lines that start with the leading word at the
93      # same indent, even if they have blank lines before
94      if $1 =~ /(^\s*\n)+^(\s*\w+)/m then
95        leading = $2 # ' *    ARGF' in the example above
96        re = %r%
97          \A(
98             (^\s*\n)+
99             (^#{Regexp.escape leading}.*?\n)+
100            )+
101          ^\s*$
102        %xm
103
104        if @text[seq_stop..-1] =~ re then
105          all_stop = seq_stop + $~.offset(0).last
106          seq_stop = seq_stop + $~.offset(1).last
107        end
108      end
109
110      seq = @text[seq_start..seq_stop]
111      seq.gsub!(/^\s*(\S|\n)/m, '\1')
112      @text.slice! all_start...all_stop
113
114      method.call_seq = seq.chomp
115
116    elsif @text.sub!(/^\s*:?call-seq:(.*?)(^\s*$|\z)/m, '') then
117      seq = $1
118      seq.gsub!(/^\s*/, '')
119      method.call_seq = seq
120    end
121    #elsif @text.sub!(/\A\/\*\s*call-seq:(.*?)\*\/\Z/, '') then
122    #  method.call_seq = $1.strip
123    #end
124
125    method
126  end
127
128  ##
129  # A comment is empty if its text String is empty.
130
131  def empty?
132    @text.empty?
133  end
134
135  ##
136  # HACK dubious
137
138  def force_encoding encoding
139    @text.force_encoding encoding
140  end
141
142  ##
143  # Sets the format of this comment and resets any parsed document
144
145  def format= format
146    @format = format
147    @document = nil
148  end
149
150  def inspect # :nodoc:
151    location = @location ? @location.relative_name : '(unknown)'
152
153    "#<%s:%x %s %p>" % [self.class, object_id, location, @text]
154  end
155
156  ##
157  # Normalizes the text.  See RDoc::Text#normalize_comment for details
158
159  def normalize
160    return self unless @text
161    return self if @normalized # TODO eliminate duplicate normalization
162
163    @text = normalize_comment @text
164
165    @normalized = true
166
167    self
168  end
169
170  ##
171  # Was this text normalized?
172
173  def normalized? # :nodoc:
174    @normalized
175  end
176
177  ##
178  # Parses the comment into an RDoc::Markup::Document.  The parsed document is
179  # cached until the text is changed.
180
181  def parse
182    return @document if @document
183
184    @document = super @text, @format
185    @document.file = @location
186    @document
187  end
188
189  ##
190  # Removes private sections from this comment.  Private sections are flush to
191  # the comment marker and start with <tt>--</tt> and end with <tt>++</tt>.
192  # For C-style comments, a private marker may not start at the opening of the
193  # comment.
194  #
195  #   /*
196  #    *--
197  #    * private
198  #    *++
199  #    * public
200  #    */
201
202  def remove_private
203    # Workaround for gsub encoding for Ruby 1.9.2 and earlier
204    empty = ''
205    empty.force_encoding @text.encoding if Object.const_defined? :Encoding
206
207    @text = @text.gsub(%r%^\s*([#*]?)--.*?^\s*(\1)\+\+\n?%m, empty)
208    @text = @text.sub(%r%^\s*[#*]?--.*%m, '')
209  end
210
211  ##
212  # Replaces this comment's text with +text+ and resets the parsed document.
213  #
214  # An error is raised if the comment contains a document but no text.
215
216  def text= text
217    raise RDoc::Error, 'replacing document-only comment is not allowed' if
218      @text.nil? and @document
219
220    @document = nil
221    @text = text
222  end
223
224  ##
225  # Returns true if this comment is in TomDoc format.
226
227  def tomdoc?
228    @format == 'tomdoc'
229  end
230
231end
232
233