@@ -165,6 +165,12 @@ def normalize
165
165
self
166
166
end
167
167
168
+ # Change normalized, when creating already normalized comment.
169
+
170
+ def normalized = ( value )
171
+ @normalized = value
172
+ end
173
+
168
174
##
169
175
# Was this text normalized?
170
176
@@ -226,14 +232,169 @@ def tomdoc?
226
232
@format == 'tomdoc'
227
233
end
228
234
229
- ##
230
- # Create a new parsed comment from a document
235
+ MULTILINE_DIRECTIVES = %w[ call-seq ] . freeze # :nodoc:
231
236
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
238
243
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
239
400
end
0 commit comments