docscribe 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +692 -180
- data/exe/docscribe +2 -74
- data/lib/docscribe/cli/config_builder.rb +62 -0
- data/lib/docscribe/cli/init.rb +58 -0
- data/lib/docscribe/cli/options.rb +204 -0
- data/lib/docscribe/cli/run.rb +415 -0
- data/lib/docscribe/cli.rb +31 -0
- data/lib/docscribe/config/defaults.rb +71 -0
- data/lib/docscribe/config/emit.rb +126 -0
- data/lib/docscribe/config/filtering.rb +160 -0
- data/lib/docscribe/config/loader.rb +59 -0
- data/lib/docscribe/config/rbs.rb +51 -0
- data/lib/docscribe/config/sorbet.rb +87 -0
- data/lib/docscribe/config/sorting.rb +23 -0
- data/lib/docscribe/config/template.rb +176 -0
- data/lib/docscribe/config/utils.rb +102 -0
- data/lib/docscribe/config.rb +20 -230
- data/lib/docscribe/infer/ast_walk.rb +28 -0
- data/lib/docscribe/infer/constants.rb +11 -0
- data/lib/docscribe/infer/literals.rb +55 -0
- data/lib/docscribe/infer/names.rb +43 -0
- data/lib/docscribe/infer/params.rb +62 -0
- data/lib/docscribe/infer/raises.rb +68 -0
- data/lib/docscribe/infer/returns.rb +171 -0
- data/lib/docscribe/infer.rb +110 -259
- data/lib/docscribe/inline_rewriter/collector.rb +845 -0
- data/lib/docscribe/inline_rewriter/doc_block.rb +383 -0
- data/lib/docscribe/inline_rewriter/doc_builder.rb +605 -0
- data/lib/docscribe/inline_rewriter/source_helpers.rb +228 -0
- data/lib/docscribe/inline_rewriter/tag_sorter.rb +244 -0
- data/lib/docscribe/inline_rewriter.rb +604 -425
- data/lib/docscribe/parsing.rb +120 -0
- data/lib/docscribe/types/provider_chain.rb +37 -0
- data/lib/docscribe/types/rbs/provider.rb +213 -0
- data/lib/docscribe/types/rbs/type_formatter.rb +132 -0
- data/lib/docscribe/types/signature.rb +65 -0
- data/lib/docscribe/types/sorbet/base_provider.rb +217 -0
- data/lib/docscribe/types/sorbet/rbi_provider.rb +35 -0
- data/lib/docscribe/types/sorbet/source_provider.rb +25 -0
- data/lib/docscribe/version.rb +1 -1
- data/lib/docscribe.rb +1 -0
- metadata +85 -17
- data/.rspec +0 -3
- data/.rubocop.yml +0 -11
- data/.rubocop_todo.yml +0 -73
- data/CODE_OF_CONDUCT.md +0 -84
- data/Gemfile +0 -6
- data/Gemfile.lock +0 -73
- data/Rakefile +0 -12
- data/rakelib/docs.rake +0 -73
- data/stingray_docs_internal.gemspec +0 -41
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'parser/source/range'
|
|
4
|
+
|
|
5
|
+
module Docscribe
|
|
6
|
+
module InlineRewriter
|
|
7
|
+
# Source-level helpers: ranges, insertion positions, indentation, and comment-block detection.
|
|
8
|
+
#
|
|
9
|
+
# These helpers operate on raw source text and parser source locations rather than Ruby semantics.
|
|
10
|
+
module SourceHelpers
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Extract the method name from a `:def` or `:defs` node.
|
|
14
|
+
#
|
|
15
|
+
# @note module_function: when included, also defines #node_name (instance visibility: private)
|
|
16
|
+
# @param [Parser::AST::Node] node
|
|
17
|
+
# @return [Symbol, nil]
|
|
18
|
+
def node_name(node)
|
|
19
|
+
case node.type
|
|
20
|
+
when :def then node.children[0]
|
|
21
|
+
when :defs then node.children[1]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Return a zero-width range at the beginning of the line containing a node.
|
|
26
|
+
#
|
|
27
|
+
# Used as the insertion point for generated documentation.
|
|
28
|
+
#
|
|
29
|
+
# @note module_function: when included, also defines #line_start_range (instance visibility: private)
|
|
30
|
+
# @param [Parser::Source::Buffer] buffer
|
|
31
|
+
# @param [Parser::AST::Node] node
|
|
32
|
+
# @return [Parser::Source::Range]
|
|
33
|
+
def line_start_range(buffer, node)
|
|
34
|
+
start_pos = node.loc.expression.begin_pos
|
|
35
|
+
src = buffer.source
|
|
36
|
+
bol = start_pos <= 0 ? -1 : src.rindex("\n", start_pos - 1) || -1
|
|
37
|
+
Parser::Source::Range.new(buffer, bol + 1, bol + 1)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Return structured information about a contiguous doc-like comment block above a method.
|
|
41
|
+
#
|
|
42
|
+
# Result includes:
|
|
43
|
+
# - all lines in the contiguous block
|
|
44
|
+
# - preserved directive prefix lines
|
|
45
|
+
# - editable doc lines
|
|
46
|
+
# - source positions for replacement
|
|
47
|
+
#
|
|
48
|
+
# Returns nil if no doc-like block is present.
|
|
49
|
+
#
|
|
50
|
+
# @note module_function: when included, also defines #doc_comment_block_info (instance visibility: private)
|
|
51
|
+
# @param [Parser::Source::Buffer] buffer
|
|
52
|
+
# @param [Integer] def_bol_pos beginning-of-line position of the target def
|
|
53
|
+
# @return [Hash, nil]
|
|
54
|
+
def doc_comment_block_info(buffer, def_bol_pos)
|
|
55
|
+
src = buffer.source
|
|
56
|
+
lines = src.lines
|
|
57
|
+
def_line_idx = src[0...def_bol_pos].count("\n")
|
|
58
|
+
i = def_line_idx - 1
|
|
59
|
+
|
|
60
|
+
# Skip blank lines directly above def
|
|
61
|
+
i -= 1 while i >= 0 && lines[i].strip.empty?
|
|
62
|
+
|
|
63
|
+
# Nearest non-blank line must be a comment
|
|
64
|
+
return nil unless i >= 0 && lines[i] =~ /^\s*#/
|
|
65
|
+
|
|
66
|
+
# Walk upward to include the entire contiguous comment block
|
|
67
|
+
end_idx = i
|
|
68
|
+
start_idx = i
|
|
69
|
+
start_idx -= 1 while start_idx >= 0 && lines[start_idx] =~ /^\s*#/
|
|
70
|
+
start_idx += 1
|
|
71
|
+
|
|
72
|
+
# Preserve leading directive-style comments
|
|
73
|
+
removable_start_idx = start_idx
|
|
74
|
+
removable_start_idx += 1 while removable_start_idx <= end_idx && preserved_comment_line?(lines[removable_start_idx])
|
|
75
|
+
|
|
76
|
+
return nil if removable_start_idx > end_idx
|
|
77
|
+
|
|
78
|
+
remaining = lines[removable_start_idx..end_idx]
|
|
79
|
+
return nil unless remaining.any? { |line| doc_marker_line?(line) }
|
|
80
|
+
|
|
81
|
+
start_pos = start_idx.positive? ? lines[0...start_idx].join.length : 0
|
|
82
|
+
doc_start_pos = removable_start_idx.positive? ? lines[0...removable_start_idx].join.length : 0
|
|
83
|
+
end_pos = lines[0..end_idx].join.length
|
|
84
|
+
|
|
85
|
+
{
|
|
86
|
+
lines: lines[start_idx..end_idx],
|
|
87
|
+
preserved_lines: lines[start_idx...removable_start_idx],
|
|
88
|
+
doc_lines: lines[removable_start_idx..end_idx],
|
|
89
|
+
start_pos: start_pos,
|
|
90
|
+
doc_start_pos: doc_start_pos,
|
|
91
|
+
end_pos: end_pos
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Compute the removable range for an existing doc-like block above a method.
|
|
96
|
+
#
|
|
97
|
+
# Preserved directive lines (such as RuboCop directives or magic comments) are excluded
|
|
98
|
+
# from the returned range.
|
|
99
|
+
#
|
|
100
|
+
# @note module_function: when included, also defines #comment_block_removal_range (instance visibility: private)
|
|
101
|
+
# @param [Parser::Source::Buffer] buffer
|
|
102
|
+
# @param [Integer] def_bol_pos beginning-of-line position of the target def
|
|
103
|
+
# @return [Parser::Source::Range, nil]
|
|
104
|
+
def comment_block_removal_range(buffer, def_bol_pos)
|
|
105
|
+
src = buffer.source
|
|
106
|
+
lines = src.lines
|
|
107
|
+
def_line_idx = src[0...def_bol_pos].count("\n")
|
|
108
|
+
i = def_line_idx - 1
|
|
109
|
+
|
|
110
|
+
# Skip blank lines directly above def
|
|
111
|
+
i -= 1 while i >= 0 && lines[i].strip.empty?
|
|
112
|
+
|
|
113
|
+
# Nearest non-blank line must be a comment to remove anything
|
|
114
|
+
return nil unless i >= 0 && lines[i] =~ /^\s*#/
|
|
115
|
+
|
|
116
|
+
# Walk upward to include the entire contiguous comment block
|
|
117
|
+
start_idx = i
|
|
118
|
+
start_idx -= 1 while start_idx >= 0 && lines[start_idx] =~ /^\s*#/
|
|
119
|
+
start_idx += 1
|
|
120
|
+
|
|
121
|
+
# Preserve leading directive-style comments (currently: rubocop directives)
|
|
122
|
+
removable_start_idx = start_idx
|
|
123
|
+
removable_start_idx += 1 while removable_start_idx <= i && preserved_comment_line?(lines[removable_start_idx])
|
|
124
|
+
|
|
125
|
+
# If the whole block is preserved directives, there is nothing to remove
|
|
126
|
+
return nil if removable_start_idx > i
|
|
127
|
+
|
|
128
|
+
# SAFETY: only remove if the remaining block looks like documentation
|
|
129
|
+
remaining = lines[removable_start_idx..i]
|
|
130
|
+
return nil unless remaining.any? { |line| doc_marker_line?(line) }
|
|
131
|
+
|
|
132
|
+
start_pos = removable_start_idx.positive? ? lines[0...removable_start_idx].join.length : 0
|
|
133
|
+
Parser::Source::Range.new(buffer, start_pos, def_bol_pos)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Whether a comment line should be preserved during aggressive replacement.
|
|
137
|
+
#
|
|
138
|
+
# Preserved lines include:
|
|
139
|
+
# - RuboCop directives
|
|
140
|
+
# - Ruby magic comments
|
|
141
|
+
# - tool directives such as `:nocov:` / `:stopdoc:`
|
|
142
|
+
#
|
|
143
|
+
# @note module_function: when included, also defines #preserved_comment_line? (instance visibility: private)
|
|
144
|
+
# @param [String] line
|
|
145
|
+
# @return [Boolean]
|
|
146
|
+
def preserved_comment_line?(line)
|
|
147
|
+
# RuboCop directives
|
|
148
|
+
return true if line =~ /^\s*#\s*rubocop:(disable|enable|todo)\b/
|
|
149
|
+
|
|
150
|
+
# Ruby magic comments
|
|
151
|
+
return true if line =~ /^\s*#\s*(?:frozen_string_literal|warn_indent)\s*:\s*(?:true|false)\b/i
|
|
152
|
+
return true if line =~ /^\s*#.*\b(?:encoding|coding)\s*:\s*[\w.-]+\b/i
|
|
153
|
+
|
|
154
|
+
# Tool directives like:
|
|
155
|
+
# # :nocov:
|
|
156
|
+
# # :stopdoc:
|
|
157
|
+
# # :nodoc:
|
|
158
|
+
return true if line =~ /^\s*#\s*:\s*[\w-]+\s*:(?=\s|\z)/i
|
|
159
|
+
|
|
160
|
+
false
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Whether a comment line looks like documentation content.
|
|
164
|
+
#
|
|
165
|
+
# Recognized forms include:
|
|
166
|
+
# - Docscribe header lines
|
|
167
|
+
# - YARD tags/directives beginning with `@`
|
|
168
|
+
#
|
|
169
|
+
# @note module_function: when included, also defines #doc_marker_line? (instance visibility: private)
|
|
170
|
+
# @param [String] line
|
|
171
|
+
# @return [Boolean]
|
|
172
|
+
def doc_marker_line?(line)
|
|
173
|
+
# Docscribe header line:
|
|
174
|
+
# # +A#foo+ -> Integer
|
|
175
|
+
return true if line =~ /^\s*#\s*\+\S.*\+\s*->\s*\S/
|
|
176
|
+
|
|
177
|
+
# YARD tags and directives:
|
|
178
|
+
# # @param ...
|
|
179
|
+
# # @return ...
|
|
180
|
+
# # @raise ...
|
|
181
|
+
# # @private / @protected
|
|
182
|
+
# # @!attribute ...
|
|
183
|
+
# also matches indented attribute tag lines like:
|
|
184
|
+
# # @return [Type]
|
|
185
|
+
return true if line =~ /^\s*#\s*@/
|
|
186
|
+
|
|
187
|
+
false
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Whether any comment exists immediately above the insertion point.
|
|
191
|
+
#
|
|
192
|
+
# This helper is retained for compatibility/legacy behavior checks.
|
|
193
|
+
#
|
|
194
|
+
# @note module_function: when included, also defines #already_has_doc_immediately_above? (instance visibility: private)
|
|
195
|
+
# @param [Parser::Source::Buffer] buffer
|
|
196
|
+
# @param [Integer] insert_pos
|
|
197
|
+
# @return [Boolean]
|
|
198
|
+
def already_has_doc_immediately_above?(buffer, insert_pos)
|
|
199
|
+
src = buffer.source
|
|
200
|
+
lines = src.lines
|
|
201
|
+
current_line_index = src[0...insert_pos].count("\n")
|
|
202
|
+
i = current_line_index - 1
|
|
203
|
+
i -= 1 while i >= 0 && lines[i].strip.empty?
|
|
204
|
+
return false if i.negative?
|
|
205
|
+
|
|
206
|
+
!!(lines[i] =~ /^\s*#/)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Return the indentation prefix of a node's source line.
|
|
210
|
+
#
|
|
211
|
+
# Tabs and spaces are preserved exactly.
|
|
212
|
+
#
|
|
213
|
+
# @note module_function: when included, also defines #line_indent (instance visibility: private)
|
|
214
|
+
# @param [Parser::AST::Node] node
|
|
215
|
+
# @raise [StandardError]
|
|
216
|
+
# @return [String]
|
|
217
|
+
def line_indent(node)
|
|
218
|
+
line = node.loc.expression.source_line
|
|
219
|
+
return '' unless line
|
|
220
|
+
|
|
221
|
+
# Preserve tabs/spaces exactly.
|
|
222
|
+
line[/\A[ \t]*/] || ''
|
|
223
|
+
rescue StandardError
|
|
224
|
+
''
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docscribe
|
|
4
|
+
module InlineRewriter
|
|
5
|
+
# Older tag-sorting helper operating on comment-line segments.
|
|
6
|
+
#
|
|
7
|
+
# This module sorts contiguous runs of top-level tag entries and keeps related `@option`
|
|
8
|
+
# entries attached to their owning `@param`.
|
|
9
|
+
#
|
|
10
|
+
# If `DocBlock` fully supersedes this module in your codebase, consider removing it.
|
|
11
|
+
module TagSorter
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# One sortable top-level tag entry plus its continuation lines.
|
|
15
|
+
# @!attribute [rw] tag
|
|
16
|
+
# @return [Object]
|
|
17
|
+
# @param [Object] value
|
|
18
|
+
#
|
|
19
|
+
# @!attribute [rw] lines
|
|
20
|
+
# @return [Object]
|
|
21
|
+
# @param [Object] value
|
|
22
|
+
#
|
|
23
|
+
# @!attribute [rw] param_name
|
|
24
|
+
# @return [Object]
|
|
25
|
+
# @param [Object] value
|
|
26
|
+
#
|
|
27
|
+
# @!attribute [rw] option_owner
|
|
28
|
+
# @return [Object]
|
|
29
|
+
# @param [Object] value
|
|
30
|
+
#
|
|
31
|
+
# @!attribute [rw] index
|
|
32
|
+
# @return [Object]
|
|
33
|
+
# @param [Object] value
|
|
34
|
+
Entry = Struct.new(:tag, :lines, :param_name, :option_owner, :index, keyword_init: true)
|
|
35
|
+
|
|
36
|
+
# Sort contiguous top-level tag runs according to configured tag order.
|
|
37
|
+
#
|
|
38
|
+
# Non-tag content is preserved as-is and acts as a sort boundary.
|
|
39
|
+
#
|
|
40
|
+
# @note module_function: when included, also defines #sort (instance visibility: private)
|
|
41
|
+
# @param [Array<String>] lines comment block lines
|
|
42
|
+
# @param [Array<String>] tag_order configured tag order
|
|
43
|
+
# @return [Array<String>]
|
|
44
|
+
def sort(lines, tag_order:)
|
|
45
|
+
priority = build_priority(tag_order)
|
|
46
|
+
segments = parse_segments(lines)
|
|
47
|
+
segments.flat_map { |seg| sort_segment(seg, priority: priority) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Build a tag priority map from configured tag order.
|
|
51
|
+
#
|
|
52
|
+
# @note module_function: when included, also defines #build_priority (instance visibility: private)
|
|
53
|
+
# @param [Array<String>] tag_order
|
|
54
|
+
# @return [Hash{String=>Integer}]
|
|
55
|
+
def build_priority(tag_order)
|
|
56
|
+
Array(tag_order).map { |t| t.to_s.sub(/\A@/, '') }
|
|
57
|
+
.each_with_index
|
|
58
|
+
.to_h
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Parse lines into sortable tag-run segments and non-sortable segments.
|
|
62
|
+
#
|
|
63
|
+
# @note module_function: when included, also defines #parse_segments (instance visibility: private)
|
|
64
|
+
# @param [Array<String>] lines
|
|
65
|
+
# @return [Array<Hash>]
|
|
66
|
+
def parse_segments(lines)
|
|
67
|
+
segments = []
|
|
68
|
+
i = 0
|
|
69
|
+
|
|
70
|
+
while i < lines.length
|
|
71
|
+
line = lines[i]
|
|
72
|
+
|
|
73
|
+
if top_level_tag_line?(line)
|
|
74
|
+
entries = []
|
|
75
|
+
while i < lines.length && top_level_tag_line?(lines[i])
|
|
76
|
+
entry, i = consume_entry(lines, i)
|
|
77
|
+
entries << entry
|
|
78
|
+
end
|
|
79
|
+
segments << { type: :tag_run, entries: entries }
|
|
80
|
+
else
|
|
81
|
+
segments << { type: :other, lines: [line] }
|
|
82
|
+
i += 1
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
segments
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Sort one parsed segment if it is a tag run.
|
|
90
|
+
#
|
|
91
|
+
# @note module_function: when included, also defines #sort_segment (instance visibility: private)
|
|
92
|
+
# @param [Hash] segment
|
|
93
|
+
# @param [Hash{String=>Integer}] priority
|
|
94
|
+
# @return [Array<String>]
|
|
95
|
+
def sort_segment(segment, priority:)
|
|
96
|
+
return segment[:lines] unless segment[:type] == :tag_run
|
|
97
|
+
|
|
98
|
+
groups = group_entries(segment[:entries])
|
|
99
|
+
|
|
100
|
+
groups
|
|
101
|
+
.each_with_index
|
|
102
|
+
.sort_by { |(group, idx)| [group_priority(group, priority), idx] }
|
|
103
|
+
.flat_map(&:first)
|
|
104
|
+
.flat_map(&:lines)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Compute sort priority for a grouped tag entry.
|
|
108
|
+
#
|
|
109
|
+
# @note module_function: when included, also defines #group_priority (instance visibility: private)
|
|
110
|
+
# @param [Array<Entry>] group
|
|
111
|
+
# @param [Hash{String=>Integer}] priority
|
|
112
|
+
# @return [Integer]
|
|
113
|
+
def group_priority(group, priority)
|
|
114
|
+
first = group.first
|
|
115
|
+
priority.fetch(first.tag, priority.length)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Consume one top-level tag entry and its continuation lines.
|
|
119
|
+
#
|
|
120
|
+
# @note module_function: when included, also defines #consume_entry (instance visibility: private)
|
|
121
|
+
# @param [Array<String>] lines
|
|
122
|
+
# @param [Integer] start_idx
|
|
123
|
+
# @return [Array<(Entry, Integer)>]
|
|
124
|
+
def consume_entry(lines, start_idx)
|
|
125
|
+
first = lines[start_idx]
|
|
126
|
+
tag = extract_tag_name(first)
|
|
127
|
+
entry_lines = [first]
|
|
128
|
+
i = start_idx + 1
|
|
129
|
+
|
|
130
|
+
while i < lines.length
|
|
131
|
+
line = lines[i]
|
|
132
|
+
|
|
133
|
+
break if top_level_tag_line?(line)
|
|
134
|
+
break if blank_comment_line?(line)
|
|
135
|
+
break unless comment_line?(line)
|
|
136
|
+
|
|
137
|
+
entry_lines << line
|
|
138
|
+
i += 1
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
entry = Entry.new(
|
|
142
|
+
tag: tag,
|
|
143
|
+
lines: entry_lines,
|
|
144
|
+
param_name: extract_param_name(first),
|
|
145
|
+
option_owner: extract_option_owner(first),
|
|
146
|
+
index: start_idx
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
[entry, i]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Group entries so `@option` tags remain attached to their owning `@param`.
|
|
153
|
+
#
|
|
154
|
+
# @note module_function: when included, also defines #group_entries (instance visibility: private)
|
|
155
|
+
# @param [Array<Entry>] entries
|
|
156
|
+
# @return [Array<Array<Entry>>]
|
|
157
|
+
def group_entries(entries)
|
|
158
|
+
groups = []
|
|
159
|
+
i = 0
|
|
160
|
+
|
|
161
|
+
while i < entries.length
|
|
162
|
+
entry = entries[i]
|
|
163
|
+
|
|
164
|
+
if entry.tag == 'param'
|
|
165
|
+
group = [entry]
|
|
166
|
+
i += 1
|
|
167
|
+
|
|
168
|
+
while i < entries.length &&
|
|
169
|
+
entries[i].tag == 'option' &&
|
|
170
|
+
entries[i].option_owner &&
|
|
171
|
+
entries[i].option_owner == entry.param_name
|
|
172
|
+
group << entries[i]
|
|
173
|
+
i += 1
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
groups << group
|
|
177
|
+
else
|
|
178
|
+
groups << [entry]
|
|
179
|
+
i += 1
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
groups
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Whether a line begins a top-level tag entry.
|
|
187
|
+
#
|
|
188
|
+
# @note module_function: when included, also defines #top_level_tag_line? (instance visibility: private)
|
|
189
|
+
# @param [String] line
|
|
190
|
+
# @return [Boolean]
|
|
191
|
+
def top_level_tag_line?(line)
|
|
192
|
+
!!(line =~ /^\s*#\s*@\w+/)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Whether a line is any comment line.
|
|
196
|
+
#
|
|
197
|
+
# @note module_function: when included, also defines #comment_line? (instance visibility: private)
|
|
198
|
+
# @param [String] line
|
|
199
|
+
# @return [Boolean]
|
|
200
|
+
def comment_line?(line)
|
|
201
|
+
!!(line =~ /^\s*#/)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Whether a line is a blank comment separator.
|
|
205
|
+
#
|
|
206
|
+
# @note module_function: when included, also defines #blank_comment_line? (instance visibility: private)
|
|
207
|
+
# @param [String] line
|
|
208
|
+
# @return [Boolean]
|
|
209
|
+
def blank_comment_line?(line)
|
|
210
|
+
!!(line =~ /^\s*#\s*$/)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Extract tag name from a top-level tag line without the leading `@`.
|
|
214
|
+
#
|
|
215
|
+
# @note module_function: when included, also defines #extract_tag_name (instance visibility: private)
|
|
216
|
+
# @param [String] line
|
|
217
|
+
# @return [String, nil]
|
|
218
|
+
def extract_tag_name(line)
|
|
219
|
+
line[/^\s*#\s*@(\w+)/, 1]
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Extract parameter name from a `@param` line.
|
|
223
|
+
#
|
|
224
|
+
# @note module_function: when included, also defines #extract_param_name (instance visibility: private)
|
|
225
|
+
# @param [String] line
|
|
226
|
+
# @return [String, nil]
|
|
227
|
+
def extract_param_name(line)
|
|
228
|
+
return Regexp.last_match(1) if line =~ /^\s*#\s*@param\b\s+\[[^\]]+\]\s+(\S+)/
|
|
229
|
+
return Regexp.last_match(1) if line =~ /^\s*#\s*@param\b\s+(\S+)\s+\[[^\]]+\]/
|
|
230
|
+
|
|
231
|
+
nil
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Extract owning options-hash param name from an `@option` line.
|
|
235
|
+
#
|
|
236
|
+
# @note module_function: when included, also defines #extract_option_owner (instance visibility: private)
|
|
237
|
+
# @param [String] line
|
|
238
|
+
# @return [String, nil]
|
|
239
|
+
def extract_option_owner(line)
|
|
240
|
+
line[/^\s*#\s*@option\b\s+(\S+)/, 1]
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|