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