@@ -162,6 +162,12 @@ def normalize
162162 self
163163 end
164164
165+ # Change normalized, when creating already normalized comment.
166+
167+ def normalized = ( value )
168+ @normalized = value
169+ end
170+
165171 ##
166172 # Was this text normalized?
167173
@@ -223,14 +229,169 @@ def tomdoc?
223229 @format == 'tomdoc'
224230 end
225231
226- ##
227- # Create a new parsed comment from a document
232+ MULTILINE_DIRECTIVES = %w[ call-seq ] . freeze # :nodoc:
228233
229- def self . from_document ( document ) # :nodoc:
230- comment = RDoc :: Comment . new ( '' )
231- comment . document = document
232- comment . location = RDoc :: TopLevel . new ( document . file ) if document . file
233- comment
234- end
234+ # There are more, but already handled by RDoc::Parser::C
235+ COLON_LESS_DIRECTIVES = %w[ call-seq Document-method ] . freeze # :nodoc:
236+
237+ private_constant :MULTILINE_DIRECTIVES , :COLON_LESS_DIRECTIVES
238+
239+ class << self
235240
241+ ##
242+ # Create a new parsed comment from a document
243+
244+ def from_document ( document ) # :nodoc:
245+ comment = RDoc ::Comment . new ( '' )
246+ comment . document = document
247+ comment . location = RDoc ::TopLevel . new ( document . file ) if document . file
248+ comment
249+ end
250+
251+ # Parse comment, collect directives as an attribute and return [normalized_comment_text, directives_hash]
252+ # This method expands include and removes everything not needed in the document text, such as
253+ # private section, directive line, comment characters `# /* * */` and indent spaces.
254+ #
255+ # RDoc comment consists of include, directive, multiline directive, private section and comment text.
256+ #
257+ # Include
258+ # # :include: filename
259+ #
260+ # Directive
261+ # # :directive-without-value:
262+ # # :directive-with-value: value
263+ #
264+ # Multiline directive (only :call-seq:)
265+ # # :multiline-directive:
266+ # # value1
267+ # # value2
268+ #
269+ # Private section
270+ # #--
271+ # # private comment
272+ # #++
273+
274+ def parse ( text , filename , line_no , type )
275+ case type
276+ when :ruby
277+ text = text . gsub ( /^#+/ , '' ) if text . start_with? ( '#' )
278+ private_start_regexp = /^-{2,}$/
279+ private_end_regexp = /^\+ {2}$/
280+ indent_regexp = /^\s */
281+ when :c
282+ private_start_regexp = /^(\s *\* )?-{2,}$/
283+ private_end_regexp = /^(\s *\* )?\+ {2}$/
284+ indent_regexp = /^\s *(\/ \* +|\* )?\s */
285+ text = text . gsub ( /\s *\* +\/ \s *\z / , '' )
286+ # TODO: should not be here. Looks like another type of directive
287+ # text = text.gsub %r%Document-method:\s+[\w:.#=!?|^&<>~+\-/*\%@`\[\]]+%, ''
288+ when :simple
289+ # Unlike other types, this implementation only looks for two dashes at
290+ # the beginning of the line. Three or more dashes are considered to be
291+ # a rule and ignored.
292+ private_start_regexp = /^-{2}$/
293+ private_end_regexp = /^\+ {2}$/
294+ indent_regexp = /^\s */
295+ end
296+
297+ directives = { }
298+ lines = text . split ( "\n " )
299+ in_private = false
300+ comment_lines = [ ]
301+ until lines . empty?
302+ line = lines . shift
303+ read_lines = 1
304+ if in_private
305+ in_private = false if line . match? ( private_end_regexp )
306+ line_no += read_lines
307+ next
308+ elsif line . match? ( private_start_regexp )
309+ in_private = true
310+ line_no += read_lines
311+ next
312+ end
313+
314+ prefix = line [ indent_regexp ]
315+ prefix_indent = ' ' * prefix . size
316+ line = line . byteslice ( prefix . bytesize ..)
317+ /\A (?<colon>\\ ?:|:?)(?<directive>[\w -]+):(?<param>.*)/ =~ line
318+
319+ if colon == '\\:'
320+ # unescape if escaped
321+ comment_lines << prefix_indent + line . sub ( '\\:' , ':' )
322+ elsif !directive || param . start_with? ( ':' ) || ( colon . empty? && !COLON_LESS_DIRECTIVES . include? ( directive ) )
323+ # Something like `:toto::` is not a directive
324+ # Only few directives allows to start without a colon
325+ comment_lines << prefix_indent + line
326+ elsif directive == 'include'
327+ filename_to_include = param . strip
328+ yield ( filename_to_include , prefix_indent ) . lines . each { |l | comment_lines << l . chomp }
329+ elsif MULTILINE_DIRECTIVES . include? ( directive )
330+ param = param . strip
331+ value_lines = take_multiline_directive_value_lines ( directive , filename , line_no , lines , prefix_indent . size , indent_regexp , !param . empty? )
332+ read_lines += value_lines . size
333+ lines . shift ( value_lines . size )
334+ unless param . empty?
335+ # Accept `:call-seq: first-line\n second-line` for now
336+ value_lines . unshift ( param )
337+ end
338+ value = value_lines . join ( "\n " )
339+ directives [ directive ] = [ value . empty? ? nil : value , line_no ]
340+ else
341+ value = param . strip
342+ directives [ directive ] = [ value . empty? ? nil : value , line_no ]
343+ end
344+ line_no += read_lines
345+ end
346+ # normalize comment
347+ min_spaces = nil
348+ comment_lines . each do |l |
349+ next if l . match? ( /\A \s *\z / )
350+ n = l [ /\A */ ] . size
351+ min_spaces = n if !min_spaces || n < min_spaces
352+ end
353+ comment_lines . map! { |l | l [ min_spaces ..] || '' } if min_spaces
354+ comment_lines . shift while comment_lines . first &.match? ( /\A \s *\z / )
355+ [ String . new ( encoding : text . encoding ) << comment_lines . join ( "\n " ) , directives ]
356+ end
357+
358+ # Take value lines of multiline directive
359+
360+ private def take_multiline_directive_value_lines ( directive , filename , line_no , lines , base_indent_size , indent_regexp , has_param )
361+ return [ ] if lines . empty?
362+
363+ first_indent_size = lines . first [ indent_regexp ] . size
364+
365+ # Blank line or unindented line is not part of multiline-directive value
366+ return [ ] if first_indent_size <= base_indent_size
367+
368+ if has_param
369+ # :multiline-directive: line1
370+ # line2
371+ # line3
372+ #
373+ value_lines = lines . take_while do |l |
374+ l . rstrip [ indent_regexp ] . size > base_indent_size
375+ end
376+ min_indent = value_lines . map { |l | l [ indent_regexp ] . size } . min
377+ value_lines . map { |l | l [ min_indent ..] }
378+ else
379+ # Take indented lines accepting blank lines between them
380+ value_lines = lines . take_while do |l |
381+ l = l . rstrip
382+ indent = l [ indent_regexp ]
383+ if indent == l || indent . size >= first_indent_size
384+ true
385+ end
386+ end
387+ value_lines . map! { |l | ( l [ first_indent_size ..] || '' ) . chomp }
388+
389+ if value_lines . size != lines . size && !value_lines . last . empty?
390+ warn "#{ filename } :#{ line_no } Multiline directive :#{ directive } : should end with a blank line."
391+ end
392+ value_lines . pop while value_lines . last &.empty?
393+ value_lines
394+ end
395+ end
396+ end
236397end
0 commit comments