Skip to content

Commit 5ba51f4

Browse files
committed
Change comment directive parsing
1 parent 8417536 commit 5ba51f4

File tree

12 files changed

+626
-209
lines changed

12 files changed

+626
-209
lines changed

lib/rdoc/comment.rb

Lines changed: 169 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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
236397
end

lib/rdoc/markup/pre_process.rb

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,18 +97,15 @@ def initialize(input_file_name, include_path)
9797
# RDoc::CodeObject#metadata for details.
9898

9999
def handle(text, code_object = nil, &block)
100-
first_line = 1
101100
if RDoc::Comment === text then
102101
comment = text
103102
text = text.text
104-
first_line = comment.line || 1
105103
end
106104

107105
# regexp helper (square brackets for optional)
108106
# $1 $2 $3 $4 $5
109107
# [prefix][\]:directive:[spaces][param]newline
110-
text = text.lines.map.with_index(first_line) do |line, num|
111-
next line unless line =~ /\A([ \t]*(?:#|\/?\*)?[ \t]*)(\\?):([\w-]+):([ \t]*)(.+)?(\r?\n|$)/
108+
text = text.gsub(/^([ \t]*(?:#|\/?\*)?[ \t]*)(\\?):([\w-]+):([ \t]*)(.+)?(\r?\n|$)/) do
112109
# skip something like ':toto::'
113110
next $& if $4.empty? and $5 and $5[0, 1] == ':'
114111

@@ -122,21 +119,49 @@ def handle(text, code_object = nil, &block)
122119
comment.format = $5.downcase
123120
next "#{$1.strip}\n"
124121
end
125-
126-
handle_directive $1, $3, $5, code_object, text.encoding, num, &block
127-
end.join
122+
handle_directive $1, $3, $5, code_object, text.encoding, &block
123+
end
128124

129125
if comment then
130126
comment.text = text
131127
else
132128
comment = text
133129
end
134130

131+
run_post_processes(comment, code_object)
132+
133+
text
134+
end
135+
136+
# Apply directives to a code object
137+
138+
def run_pre_processes(comment_text, code_object, start_line_no, type)
139+
comment_text, directives = parse_comment(comment_text, start_line_no, type)
140+
directives.each do |directive, (param, line_no)|
141+
handle_directive('', directive, param, code_object)
142+
end
143+
if code_object.is_a?(RDoc::AnyMethod) && (call_seq, = directives['call-seq']) && call_seq
144+
code_object.call_seq = call_seq.lines.map(&:chomp).reject(&:empty?).join("\n") if call_seq
145+
end
146+
format, = directives['markup']
147+
[comment_text, format]
148+
end
149+
150+
151+
# Perform post preocesses to a code object
152+
153+
def run_post_processes(comment, code_object)
135154
self.class.post_processors.each do |handler|
136155
handler.call comment, code_object
137156
end
157+
end
138158

139-
text
159+
# Parse comment and return [normalized_comment_text, directives_hash]
160+
161+
def parse_comment(text, line_no, type)
162+
RDoc::Comment.parse(text, @input_file_name, line_no, type) do |filename, prefix_indent|
163+
include_file(filename, prefix_indent, text.encoding)
164+
end
140165
end
141166

142167
##
@@ -151,7 +176,7 @@ def handle(text, code_object = nil, &block)
151176
# When 1.8.7 support is ditched prefix can be defaulted to ''
152177

153178
def handle_directive(prefix, directive, param, code_object = nil,
154-
encoding = nil, line = nil)
179+
encoding = nil)
155180
blankline = "#{prefix.strip}\n"
156181
directive = directive.downcase
157182

@@ -244,7 +269,7 @@ def handle_directive(prefix, directive, param, code_object = nil,
244269

245270
blankline
246271
else
247-
result = yield directive, param, line if block_given?
272+
result = yield directive, param if block_given?
248273

249274
case result
250275
when nil then

lib/rdoc/parser/c.rb

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -607,8 +607,6 @@ def find_body(class_name, meth_name, meth_obj, file_content, quiet = false)
607607
body = args[1]
608608
offset, = args[2]
609609

610-
comment.remove_private if comment
611-
612610
# try to find the whole body
613611
body = $& if /#{Regexp.escape body}[^(]*?\{.*?^\}/m =~ file_content
614612

@@ -621,7 +619,6 @@ def find_body(class_name, meth_name, meth_obj, file_content, quiet = false)
621619
override_comment = find_override_comment class_name, meth_obj
622620
comment = override_comment if override_comment
623621

624-
comment.normalize
625622
find_modifiers comment, meth_obj if comment
626623

627624
#meth_obj.params = params
@@ -639,7 +636,6 @@ def find_body(class_name, meth_name, meth_obj, file_content, quiet = false)
639636

640637
find_body class_name, args[3], meth_obj, file_content, true
641638

642-
comment.normalize
643639
find_modifiers comment, meth_obj
644640

645641
meth_obj.start_collecting_tokens
@@ -663,7 +659,6 @@ def find_body(class_name, meth_name, meth_obj, file_content, quiet = false)
663659
comment = find_override_comment class_name, meth_obj
664660

665661
if comment then
666-
comment.normalize
667662
find_modifiers comment, meth_obj
668663
meth_obj.comment = comment
669664

@@ -742,7 +737,6 @@ def find_class_comment(class_name, class_mod)
742737
end
743738

744739
comment = new_comment comment, @top_level, :c
745-
comment.normalize
746740

747741
look_for_directives_in class_mod, comment
748742

@@ -807,9 +801,6 @@ def find_const_comment(type, const_name, class_name = nil)
807801
# Handles modifiers in +comment+ and updates +meth_obj+ as appropriate.
808802

809803
def find_modifiers(comment, meth_obj)
810-
comment.normalize
811-
meth_obj.call_seq = comment.extract_call_seq
812-
813804
look_for_directives_in meth_obj, comment
814805
end
815806

@@ -823,10 +814,10 @@ def find_override_comment(class_name, meth_obj)
823814
comment = if @content =~ %r%Document-method:
824815
\s+#{class_name}#{prefix}#{name}
825816
\s*?\n((?>.*?\*/))%xm then
826-
"/*#{$1}"
817+
"/*\n#{$1}"
827818
elsif @content =~ %r%Document-method:
828819
\s#{name}\s*?\n((?>.*?\*/))%xm then
829-
"/*#{$1}"
820+
"/*\n#{$1}"
830821
end
831822

832823
return unless comment
@@ -1102,35 +1093,10 @@ def load_variable_map(map_name)
11021093
# Both :main: and :title: directives are deprecated and will be removed in RDoc 7.
11031094

11041095
def look_for_directives_in(context, comment)
1105-
@preprocess.handle comment, context do |directive, param|
1106-
case directive
1107-
when 'main' then
1108-
@options.main_page = param
1109-
1110-
warn <<~MSG
1111-
The :main: directive is deprecated and will be removed in RDoc 7.
1112-
1113-
You can use these options to specify the initial page displayed instead:
1114-
- `--main=#{param}` via the command line
1115-
- `rdoc.main = "#{param}"` if you use `RDoc::Task`
1116-
- `main_page: #{param}` in your `.rdoc_options` file
1117-
MSG
1118-
''
1119-
when 'title' then
1120-
@options.default_title = param if @options.respond_to? :default_title=
1121-
1122-
warn <<~MSG
1123-
The :title: directive is deprecated and will be removed in RDoc 7.
1124-
1125-
You can use these options to specify the title displayed instead:
1126-
- `--title=#{param}` via the command line
1127-
- `rdoc.title = "#{param}"` if you use `RDoc::Task`
1128-
- `title: #{param}` in your `.rdoc_options` file
1129-
MSG
1130-
''
1131-
end
1132-
end
1133-
1096+
comment.text, format = @preprocess.run_pre_processes(comment.text, context, comment.line || 1, :c)
1097+
comment.format = format if format
1098+
@preprocess.run_post_processes(comment, context)
1099+
comment.normalized = true
11341100
comment
11351101
end
11361102

0 commit comments

Comments
 (0)