docrb 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +21 -0
- data/.rspec +3 -0
- data/.rubocop.yml +70 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +79 -0
- data/Rakefile +12 -0
- data/bin/console +16 -0
- data/bin/json +16 -0
- data/bin/md +8 -0
- data/bin/setup +8 -0
- data/docrb.gemspec +33 -0
- data/exe/docrb +205 -0
- data/lib/docrb/comment_parser/code_example_block.rb +36 -0
- data/lib/docrb/comment_parser/code_example_parser.rb +29 -0
- data/lib/docrb/comment_parser/field_block.rb +18 -0
- data/lib/docrb/comment_parser/field_list_parser.rb +90 -0
- data/lib/docrb/comment_parser/text_block.rb +43 -0
- data/lib/docrb/comment_parser.rb +272 -0
- data/lib/docrb/doc_compiler/base_container/computations.rb +178 -0
- data/lib/docrb/doc_compiler/base_container.rb +123 -0
- data/lib/docrb/doc_compiler/doc_attribute.rb +58 -0
- data/lib/docrb/doc_compiler/doc_blocks.rb +111 -0
- data/lib/docrb/doc_compiler/doc_class.rb +43 -0
- data/lib/docrb/doc_compiler/doc_method.rb +66 -0
- data/lib/docrb/doc_compiler/doc_module.rb +9 -0
- data/lib/docrb/doc_compiler/file_ref.rb +41 -0
- data/lib/docrb/doc_compiler/object_container.rb +68 -0
- data/lib/docrb/doc_compiler.rb +55 -0
- data/lib/docrb/markdown.rb +62 -0
- data/lib/docrb/module_extensions.rb +13 -0
- data/lib/docrb/resolvable.rb +178 -0
- data/lib/docrb/ruby_parser.rb +630 -0
- data/lib/docrb/spec.rb +31 -0
- data/lib/docrb/version.rb +5 -0
- data/lib/docrb.rb +71 -0
- metadata +139 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Docrb
|
4
|
+
class CommentParser
|
5
|
+
# TextBlock represents an array of characters to be built into a single
|
6
|
+
# text block.
|
7
|
+
class TextBlock
|
8
|
+
def initialize(text = nil)
|
9
|
+
@buffer = [text].compact
|
10
|
+
end
|
11
|
+
|
12
|
+
def <<(obj)
|
13
|
+
@buffer << obj
|
14
|
+
@text = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def empty?
|
18
|
+
@buffer.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
def match?(regexp)
|
22
|
+
regexp.match? text
|
23
|
+
end
|
24
|
+
|
25
|
+
def last
|
26
|
+
@buffer.last
|
27
|
+
end
|
28
|
+
|
29
|
+
def text
|
30
|
+
@text ||= @buffer.join
|
31
|
+
end
|
32
|
+
|
33
|
+
def subs!(index)
|
34
|
+
@text = nil
|
35
|
+
@buffer = @buffer[index...]
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_h
|
39
|
+
{ type: :text_block, contents: text.gsub(/\n /, " ") }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,272 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Docrb
|
4
|
+
# CommentParser implements a small parser for matching comment's contents to
|
5
|
+
# relevant references and annotations.
|
6
|
+
class CommentParser
|
7
|
+
COMMENT_METHOD_REF_REGEXP = /(?:([A-Z][a-zA-Z0-9_]*::)*([A-Z][a-zA-Z0-9_]*))?(::|\.|#)([A-Za-z_][a-zA-Z0-9_@]*[!?]?)(?:\([a-zA-Z0-9=_,\s*]+\))?/
|
8
|
+
COMMENT_SYMBOL_REGEXP = /:(!|[@$][a-z_][a-z0-9_]*|[a-z_][a-z0-9_]*|[a-z_][a-z0-9_]*[?!]?)/i
|
9
|
+
CAMELCASE_IDENTIFIER_REGEXP = /[A-Z][a-z]+(?:[A-Z][a-z]+)+/
|
10
|
+
INTERNAL_ANNOTATION_REGEXP = /^internal:/i
|
11
|
+
PUBLIC_ANNOTATION_REGEXP = /^public:/i
|
12
|
+
PRIVATE_ANNOTATION_REGEXP = /^private:/i
|
13
|
+
DEPRECATED_ANNOTATION_REGEXP = /^deprecated:/i
|
14
|
+
VISIBILITY_ANNOTATIONS = {
|
15
|
+
internal: INTERNAL_ANNOTATION_REGEXP,
|
16
|
+
public: PUBLIC_ANNOTATION_REGEXP,
|
17
|
+
private: PRIVATE_ANNOTATION_REGEXP,
|
18
|
+
depreacated: DEPRECATED_ANNOTATION_REGEXP
|
19
|
+
}.freeze
|
20
|
+
CARRIAGE = "\r"
|
21
|
+
LINE_BREAK = "\n"
|
22
|
+
SPACE = " "
|
23
|
+
DASH = "-"
|
24
|
+
|
25
|
+
autoload :TextBlock, "docrb/comment_parser/text_block"
|
26
|
+
autoload :FieldListParser, "docrb/comment_parser/field_list_parser"
|
27
|
+
autoload :CodeExampleParser, "docrb/comment_parser/code_example_parser"
|
28
|
+
autoload :FieldBlock, "docrb/comment_parser/field_block"
|
29
|
+
autoload :CodeExampleBlock, "docrb/comment_parser/code_example_block"
|
30
|
+
|
31
|
+
# Parses a given comment for a given object type.
|
32
|
+
#
|
33
|
+
# type: - Type of the object's to which the comment data belongs to
|
34
|
+
# comment: - A string containing the object's documentation.
|
35
|
+
#
|
36
|
+
# Returns a Hash containing the parsed content for the comment.
|
37
|
+
def self.parse(type:, comment:)
|
38
|
+
new(type).parse(comment)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Internal: Initializes a new parser with a provided type
|
42
|
+
#
|
43
|
+
# type - Type of the object being documented
|
44
|
+
def initialize(type)
|
45
|
+
@type = type
|
46
|
+
@components = []
|
47
|
+
@current = TextBlock.new
|
48
|
+
@last_char = nil
|
49
|
+
@current_char = nil
|
50
|
+
@meta = {}
|
51
|
+
end
|
52
|
+
|
53
|
+
# Intenral: Loads the provided data into the parser and executes all steps
|
54
|
+
# required to extract relevant information and annotations.
|
55
|
+
#
|
56
|
+
# data - String containing the comment being parsed
|
57
|
+
#
|
58
|
+
# Returns a Hash containing the parsed content for the comment.
|
59
|
+
def parse(data)
|
60
|
+
load(data)
|
61
|
+
coalesce_field_list
|
62
|
+
detect_code_example
|
63
|
+
|
64
|
+
infer_visibility_from_doc if %i[def defs].include? @type
|
65
|
+
|
66
|
+
data = to_h
|
67
|
+
data[:contents] = detect_references(data[:contents])
|
68
|
+
data
|
69
|
+
end
|
70
|
+
|
71
|
+
# Internal: Attempts to infer an object visiblity based on the comment's
|
72
|
+
# prefix. Supported visibility options are `public:`, `private:`, and
|
73
|
+
# `internal:`. Annotations are case-insensitive.
|
74
|
+
# This method also removes the detected annotation from the comment block,
|
75
|
+
# to reduce clutter in the emitted documentation.
|
76
|
+
def infer_visibility_from_doc
|
77
|
+
return if @components.empty?
|
78
|
+
|
79
|
+
item = @components.first
|
80
|
+
return unless item.is_a? TextBlock
|
81
|
+
return unless (annotation = VISIBILITY_ANNOTATIONS.find { |_k, v| v.match?(item.text) })
|
82
|
+
|
83
|
+
item.subs! item.text.index(":") + 1
|
84
|
+
@meta[:doc_visibility_annotation] = annotation.first
|
85
|
+
end
|
86
|
+
|
87
|
+
# Internal: Commits the current text block being processed by #load
|
88
|
+
def commit_current!
|
89
|
+
@components << @current if @current && !@current.empty?
|
90
|
+
@current = TextBlock.new
|
91
|
+
end
|
92
|
+
|
93
|
+
# Internal: Loads a given stirng into the parser, by splitting it into
|
94
|
+
# text blocks.
|
95
|
+
def load(text)
|
96
|
+
text.each_char do |c|
|
97
|
+
@last_char = @current_char
|
98
|
+
@current_char = c
|
99
|
+
next if c == CARRIAGE
|
100
|
+
|
101
|
+
if @last_char == LINE_BREAK && (c == LINE_BREAK)
|
102
|
+
commit_current!
|
103
|
+
next
|
104
|
+
end
|
105
|
+
@current << c
|
106
|
+
end
|
107
|
+
commit_current!
|
108
|
+
end
|
109
|
+
|
110
|
+
# Internal: Attempts to find and split field lists from the loaded comment.
|
111
|
+
def coalesce_field_list
|
112
|
+
return if @field_list_coalesced
|
113
|
+
|
114
|
+
@field_list_coalesced = true
|
115
|
+
|
116
|
+
new_contents = []
|
117
|
+
has_field_list = false
|
118
|
+
|
119
|
+
@components.each do |block|
|
120
|
+
unless has_field_list
|
121
|
+
parser = FieldListParser.new(block.text)
|
122
|
+
if parser.detect
|
123
|
+
has_field_list = true
|
124
|
+
new_contents << FieldBlock.new(parser.result)
|
125
|
+
next
|
126
|
+
end
|
127
|
+
end
|
128
|
+
new_contents << block
|
129
|
+
end
|
130
|
+
|
131
|
+
@components = new_contents
|
132
|
+
end
|
133
|
+
|
134
|
+
# Internal: Attempts to find and extract code examples contained within the
|
135
|
+
# comment
|
136
|
+
def detect_code_example
|
137
|
+
return if @code_examples_detected
|
138
|
+
|
139
|
+
@code_examples_detected = true
|
140
|
+
|
141
|
+
@components = CodeExampleParser.process(@components)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Internal: Attempts to detect references on a provided list of blocks
|
145
|
+
#
|
146
|
+
# on - List of blocks to process
|
147
|
+
#
|
148
|
+
# Returns an updated list of blocks with references transformed into
|
149
|
+
# specialised structures.
|
150
|
+
def detect_references(on)
|
151
|
+
new_contents = []
|
152
|
+
|
153
|
+
on.each do |block|
|
154
|
+
case block[:type]
|
155
|
+
when :text_block
|
156
|
+
block[:contents] = detect_text_references(block[:contents])
|
157
|
+
when :field_block
|
158
|
+
block[:contents] = detect_field_references(block[:contents])
|
159
|
+
end
|
160
|
+
new_contents << block
|
161
|
+
end
|
162
|
+
|
163
|
+
new_contents
|
164
|
+
end
|
165
|
+
|
166
|
+
# Internal: Attempts to detect references on a text object.
|
167
|
+
#
|
168
|
+
# text - Text data to have references transformed into specialised objects
|
169
|
+
#
|
170
|
+
# Returns an array containing the updated text data
|
171
|
+
def detect_text_references(text)
|
172
|
+
changed = true
|
173
|
+
text, changed = update_next_reference(text) while changed
|
174
|
+
text
|
175
|
+
end
|
176
|
+
|
177
|
+
# Internal: Attempts to detect field references on a given field object.
|
178
|
+
#
|
179
|
+
# Returns a new hash containing the field's data post-processed with
|
180
|
+
# reference annotations
|
181
|
+
def detect_field_references(field)
|
182
|
+
field
|
183
|
+
.transform_values do |v|
|
184
|
+
{ type: :text_block, contents: detect_text_references(v) }
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def process_comment_method_reference(contents, match)
|
189
|
+
class_path, target, invocation, name = match.to_a.slice(1...)
|
190
|
+
class_path&.gsub!(/::$/, "")
|
191
|
+
reference_type = invocation == "#" ? :method : :ambiguous
|
192
|
+
begin_at = match.begin(0)
|
193
|
+
end_at = match.end(0)
|
194
|
+
left_slice = contents[0...begin_at]
|
195
|
+
right_slice = contents[end_at...]
|
196
|
+
item = {
|
197
|
+
type: :ref,
|
198
|
+
ref_type: reference_type,
|
199
|
+
name:,
|
200
|
+
target:,
|
201
|
+
class_path:,
|
202
|
+
contents: match[0]
|
203
|
+
}
|
204
|
+
[
|
205
|
+
{ type: :span, contents: left_slice },
|
206
|
+
item,
|
207
|
+
{ type: :span, contents: right_slice }
|
208
|
+
].reject { |i| i[:contents].empty? }
|
209
|
+
end
|
210
|
+
|
211
|
+
def process_simple_symbol(type, contents, match)
|
212
|
+
begin_at = match.begin(0)
|
213
|
+
end_at = match.end(0)
|
214
|
+
left_slice = contents[0...begin_at]
|
215
|
+
right_slice = contents[end_at...]
|
216
|
+
[
|
217
|
+
{ type: :span, contents: left_slice },
|
218
|
+
{ type:, contents: match[0] },
|
219
|
+
{ type: :span, contents: right_slice }
|
220
|
+
].reject { |i| i[:contents].empty? }
|
221
|
+
end
|
222
|
+
|
223
|
+
def update_string_reference(contents)
|
224
|
+
updated = false
|
225
|
+
if (match = COMMENT_METHOD_REF_REGEXP.match(contents))
|
226
|
+
contents = process_comment_method_reference(contents, match)
|
227
|
+
updated = true
|
228
|
+
elsif (match = COMMENT_SYMBOL_REGEXP.match(contents))
|
229
|
+
contents = process_simple_symbol(:sym_ref, contents, match)
|
230
|
+
updated = true
|
231
|
+
elsif (match = CAMELCASE_IDENTIFIER_REGEXP.match(contents))
|
232
|
+
contents = process_simple_symbol(:camelcase_identifier, contents, match)
|
233
|
+
updated = true
|
234
|
+
end
|
235
|
+
[contents, updated]
|
236
|
+
end
|
237
|
+
|
238
|
+
# Internal: Recursivelly updates a given value until all references are
|
239
|
+
# processed.
|
240
|
+
#
|
241
|
+
# contents - Contents to be processed
|
242
|
+
#
|
243
|
+
# Returns a new array containing the processed contents
|
244
|
+
def update_next_reference(contents)
|
245
|
+
return update_string_reference(contents) if contents.is_a? String
|
246
|
+
|
247
|
+
new_contents = []
|
248
|
+
changed = false
|
249
|
+
contents.each do |block|
|
250
|
+
if block[:type] == :span
|
251
|
+
updated, ok = update_next_reference(block[:contents])
|
252
|
+
if ok
|
253
|
+
changed = true
|
254
|
+
next new_contents.append(*updated)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
new_contents << block
|
258
|
+
end
|
259
|
+
|
260
|
+
[new_contents, changed]
|
261
|
+
end
|
262
|
+
|
263
|
+
# Public: Returns a hash by merging contents and metadata obtained through
|
264
|
+
# the parsing process.
|
265
|
+
def to_h
|
266
|
+
@meta.merge({
|
267
|
+
type: @type,
|
268
|
+
contents: @components.map(&:to_h)
|
269
|
+
})
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Docrb
|
4
|
+
class DocCompiler
|
5
|
+
class BaseContainer
|
6
|
+
# Module Computations implements utility methods to handle hierarchy and
|
7
|
+
# nesting.
|
8
|
+
module Computations
|
9
|
+
# Recursively computes all dependants of the receiver.
|
10
|
+
# This method expands all references into concrete representations.
|
11
|
+
def compute_dependants
|
12
|
+
return if @compute_dependants
|
13
|
+
|
14
|
+
@compute_dependants = true
|
15
|
+
|
16
|
+
classes.each(&:compute_dependants)
|
17
|
+
modules.each(&:compute_dependants)
|
18
|
+
|
19
|
+
extends.each do |ref|
|
20
|
+
resolve_ref(ref)&.compute_dependants
|
21
|
+
end
|
22
|
+
includes.each do |ref|
|
23
|
+
resolve_ref(ref)&.compute_dependants
|
24
|
+
end
|
25
|
+
|
26
|
+
return unless (parent_name = @inherits)
|
27
|
+
|
28
|
+
@parent_class = resolve(parent_name)
|
29
|
+
return unless @parent_class
|
30
|
+
|
31
|
+
@parent_class.compute_dependants
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns a Hash containing all methods for the container, along with
|
35
|
+
# inherited and included ones.
|
36
|
+
def merged_instance_methods
|
37
|
+
return @merged_instance_methods if @merged_instance_methods
|
38
|
+
|
39
|
+
compute_dependants
|
40
|
+
|
41
|
+
methods = {}
|
42
|
+
@parent_class&.merged_instance_methods&.each do |k, v|
|
43
|
+
methods[k] = { source: :inheritance, definition: v }
|
44
|
+
end
|
45
|
+
|
46
|
+
includes.each do |ref|
|
47
|
+
next unless (container = resolve_container(ref))
|
48
|
+
|
49
|
+
container.merged_instance_methods.each do |k, v|
|
50
|
+
methods[k] = { source: :inclusion, definition: v, overriding: methods[k] }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
defs.each do |m|
|
55
|
+
methods[m.name] = { source: :source, definition: m, overriding: methods[m.name] }
|
56
|
+
end
|
57
|
+
|
58
|
+
methods
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns a hash containing all class methods for the container, along
|
62
|
+
# with inherited and extended ones.
|
63
|
+
def merged_class_methods
|
64
|
+
return @merged_class_methods if @merged_class_methods
|
65
|
+
|
66
|
+
compute_dependants
|
67
|
+
|
68
|
+
methods = {}
|
69
|
+
@parent_class&.merged_class_methods&.each do |k, v|
|
70
|
+
methods[k] = { source: :inheritance, definition: v }
|
71
|
+
end
|
72
|
+
|
73
|
+
includes.each do |ref|
|
74
|
+
next unless (container = resolve_container(ref))
|
75
|
+
|
76
|
+
container.merged_class_methods.each do |k, v|
|
77
|
+
methods[k] = { source: :extension, definition: v, overriding: methods[k] }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
sdefs.each do |m|
|
82
|
+
methods[m.name] = { source: :source, definition: m, overriding: methods[m.name] }
|
83
|
+
end
|
84
|
+
|
85
|
+
methods
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns a hash containing all attributes for the container, along with
|
89
|
+
# inherited ones.
|
90
|
+
def merged_attributes
|
91
|
+
return @merged_attributes if @merged_attributes
|
92
|
+
|
93
|
+
compute_dependants
|
94
|
+
|
95
|
+
attrs = {}
|
96
|
+
@parent_class&.merged_attributes&.each do |k, v|
|
97
|
+
attrs[k] = { source: :inheritance, definition: v }
|
98
|
+
end
|
99
|
+
|
100
|
+
attributes.each do |m|
|
101
|
+
attrs[m.name] = { source: :source, definition: m, overriding: attrs[m.name] }
|
102
|
+
end
|
103
|
+
|
104
|
+
attrs
|
105
|
+
end
|
106
|
+
|
107
|
+
# Deprecated: Use #merged_instance_methods.
|
108
|
+
def all_defs
|
109
|
+
return @all_defs if @all_defs
|
110
|
+
|
111
|
+
compute_dependants
|
112
|
+
|
113
|
+
methods = {}
|
114
|
+
@parent_class&.all_defs&.each do |k, v|
|
115
|
+
methods[k] = v
|
116
|
+
end
|
117
|
+
|
118
|
+
includes.each do |ref|
|
119
|
+
resolve_ref(ref)&.all_defs&.each do |met|
|
120
|
+
if methods.key? met.name
|
121
|
+
methods[key].override! met
|
122
|
+
next
|
123
|
+
end
|
124
|
+
|
125
|
+
methods[met.name] = met
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
defs.each do |met|
|
130
|
+
if methods.key? met.name
|
131
|
+
methods[met.name].override! met
|
132
|
+
next
|
133
|
+
end
|
134
|
+
|
135
|
+
methods[met.name] = met
|
136
|
+
end
|
137
|
+
|
138
|
+
@all_defs = methods
|
139
|
+
end
|
140
|
+
|
141
|
+
# Deprecated: Use #merged_class_methods.
|
142
|
+
def all_sdefs
|
143
|
+
return @all_sdefs if @all_sdefs
|
144
|
+
|
145
|
+
compute_dependants
|
146
|
+
|
147
|
+
methods = {}
|
148
|
+
|
149
|
+
@parent_class&.all_sdefs&.each do |k, v|
|
150
|
+
methods[k] = v
|
151
|
+
end
|
152
|
+
|
153
|
+
extends.each do |ref|
|
154
|
+
resolve_ref(ref)&.all_defs&.each do |met|
|
155
|
+
if methods.key? met.name
|
156
|
+
methods[met.name].override! met
|
157
|
+
next
|
158
|
+
end
|
159
|
+
|
160
|
+
methods[met.name] = met
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
@sdefs.each do |met|
|
165
|
+
if methods.key? met.name
|
166
|
+
methods[met.name].override! met
|
167
|
+
next
|
168
|
+
end
|
169
|
+
|
170
|
+
methods[met.name] = met
|
171
|
+
end
|
172
|
+
|
173
|
+
@all_sdefs = methods
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Docrb
|
4
|
+
class DocCompiler
|
5
|
+
# BaseContainer represents a container for methods, classes, modules and
|
6
|
+
# attributes.
|
7
|
+
class BaseContainer < Resolvable
|
8
|
+
require_relative "./base_container/computations"
|
9
|
+
include Computations
|
10
|
+
|
11
|
+
attr_accessor :extends, :includes, :classes, :modules, :defs, :sdefs,
|
12
|
+
:type, :name, :defined_by, :parent, :doc
|
13
|
+
|
14
|
+
# Initialises a new BaseContainer instance with a provided parent,
|
15
|
+
# filename and represented object.
|
16
|
+
def initialize(parent, filename, obj)
|
17
|
+
super()
|
18
|
+
@parent = parent
|
19
|
+
@type = obj[:type]
|
20
|
+
@name = obj[:name]
|
21
|
+
@extends = []
|
22
|
+
@includes = []
|
23
|
+
@classes = ObjectContainer.new(self, DocClass)
|
24
|
+
@modules = ObjectContainer.new(self, DocModule)
|
25
|
+
@defs = ObjectContainer.new(self, DocMethod)
|
26
|
+
@sdefs = ObjectContainer.new(self, DocMethod)
|
27
|
+
@defined_by = ObjectContainer.new(nil, FileRef)
|
28
|
+
append(filename, obj)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Appends a new object defined by the provided file to this container
|
32
|
+
def <<(filename, obj)
|
33
|
+
append(filename, obj)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Appends a new class defined by the provided file to this container
|
37
|
+
def append_class(filename, cls)
|
38
|
+
@classes.push filename, cls
|
39
|
+
end
|
40
|
+
|
41
|
+
def inspect
|
42
|
+
ext = "#{extends.length} extend#{extends.length == 1 ? "" : "s"}"
|
43
|
+
inc = "#{includes.length} include#{includes.length == 1 ? "" : "s"}"
|
44
|
+
cls = "#{classes.length} class#{classes.length == 1 ? "" : "es"}"
|
45
|
+
mod = "#{modules.length} module#{modules.length == 1 ? "" : "s"}"
|
46
|
+
de = "#{defs.length} def#{defs.length == 1 ? "" : "s"}"
|
47
|
+
sde = "#{sdefs.length} sdef#{sdefs.length == 1 ? "" : "s"}"
|
48
|
+
|
49
|
+
"#<#{self.class.name}:#{format("0x%08x",
|
50
|
+
object_id * 2)} #{@type} #{@name} #{ext}, #{inc}, #{cls}, #{mod}, #{de}, #{sde}>"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Attempts to append a given object defined in a provided filename.
|
54
|
+
# Raises ArgumentError in case the object can't be added to the container
|
55
|
+
# due to type contraints.
|
56
|
+
#
|
57
|
+
# filename - Filename defining the object being appended
|
58
|
+
# obj - The object definition to be appended to this container.
|
59
|
+
def append(filename, obj)
|
60
|
+
raise ArgumentError, "cannot append obj of type #{obj[:type]} into #{@name} (#{@type})" if obj[:type] != @type
|
61
|
+
|
62
|
+
raise ArgumentError, "cannot append obj named #{obj[:name]} into #{@name} (#{@type})" if obj[:name] != @name
|
63
|
+
|
64
|
+
unless (doc = obj[:doc]).nil?
|
65
|
+
@defined_by.push(filename, obj)
|
66
|
+
@doc = doc
|
67
|
+
end
|
68
|
+
|
69
|
+
obj.fetch(:classes, []).each { |c| @classes.push(filename, c) }
|
70
|
+
obj.fetch(:modules, []).each { |m| @modules.push(filename, m) }
|
71
|
+
obj.fetch(:extend, []).each { |e| @extends << e }
|
72
|
+
obj.fetch(:include, []).each { |i| @includes << i }
|
73
|
+
obj.fetch(:methods, []).each do |met|
|
74
|
+
if met[:type] == :def
|
75
|
+
@defs.push(filename, met)
|
76
|
+
else
|
77
|
+
@sdefs.push(filename, met)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
@inherits = obj.fetch(:inherits, nil) if obj[:type] == :class
|
82
|
+
|
83
|
+
appended(filename, obj)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Courtesy method. This method is called whenever a new object is
|
87
|
+
# appended to the container. Inheriting classes can override it to perform
|
88
|
+
# further operations on the appended object.
|
89
|
+
def appended(filename, obj); end
|
90
|
+
|
91
|
+
# Intenral: Recursively unpacks a given element by checking its
|
92
|
+
# :definition and :overriding keys.
|
93
|
+
#
|
94
|
+
# Returns the updated element.
|
95
|
+
def unpack(elem)
|
96
|
+
return unless elem
|
97
|
+
|
98
|
+
elem.tap do |e|
|
99
|
+
inner = e[:definition]
|
100
|
+
e[:definition] = inner.is_a?(Hash) ? unpack(inner) : inner.to_h
|
101
|
+
e[:overriding] = unpack(e[:overriding])
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns a Hash representation of the current container along with its
|
106
|
+
# children.
|
107
|
+
def to_h
|
108
|
+
{
|
109
|
+
type:,
|
110
|
+
name:,
|
111
|
+
doc: DocBlocks.prepare(doc, parent: self),
|
112
|
+
defined_by: defined_by.map(&:to_h),
|
113
|
+
defs: merged_instance_methods.transform_values { |v| unpack(v) },
|
114
|
+
sdefs: merged_class_methods.transform_values { |v| unpack(v) },
|
115
|
+
classes: classes.map(&:to_h),
|
116
|
+
modules: modules.map(&:to_h),
|
117
|
+
includes: includes.map(&:to_h),
|
118
|
+
extends: extends.map(&:to_h)
|
119
|
+
}
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Docrb
|
4
|
+
class DocCompiler
|
5
|
+
# DocAttribute represents a single attribute defined on a class.
|
6
|
+
class DocAttribute < Resolvable
|
7
|
+
attr_reader :parent, :defined_by, :name, :docs, :type, :writer_visibility,
|
8
|
+
:reader_visibility
|
9
|
+
|
10
|
+
def initialize(parent, filename, obj)
|
11
|
+
super()
|
12
|
+
@parent = parent
|
13
|
+
@defined_by = [filename]
|
14
|
+
@name = obj[:name]
|
15
|
+
@docs = obj[:docs]
|
16
|
+
@type = nil
|
17
|
+
@writer_visibility = obj[:writer_visibility]
|
18
|
+
@reader_visibility = obj[:reader_visibility]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Marks the current attribute as an accessor.
|
22
|
+
#
|
23
|
+
# Returns the attribute's instance for chaining.
|
24
|
+
def accessor!
|
25
|
+
@type = :accessor
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
# Marks the current attribute as an reader.
|
30
|
+
#
|
31
|
+
# Returns the attribute's instance for chaining.
|
32
|
+
def reader!
|
33
|
+
@type = :reader
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
# Marks the current attribute as an writer.
|
38
|
+
#
|
39
|
+
# Returns the attribute's instance for chaining.
|
40
|
+
def writer!
|
41
|
+
@type = :writer
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns a Hash representing this attribute
|
46
|
+
def to_h
|
47
|
+
{
|
48
|
+
defined_by:,
|
49
|
+
name:,
|
50
|
+
docs:,
|
51
|
+
type:,
|
52
|
+
writer_visibility:,
|
53
|
+
reader_visibility:
|
54
|
+
}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|