docrb 0.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.
@@ -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