docrb-parser 0.1.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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +75 -0
  4. data/Rakefile +12 -0
  5. data/docrb-parser.gemspec +38 -0
  6. data/lib/docrb/core_extensions.rb +60 -0
  7. data/lib/docrb/parser/attribute.rb +25 -0
  8. data/lib/docrb/parser/call.rb +27 -0
  9. data/lib/docrb/parser/class.rb +94 -0
  10. data/lib/docrb/parser/comment.rb +40 -0
  11. data/lib/docrb/parser/comment_parser.rb +290 -0
  12. data/lib/docrb/parser/computations.rb +471 -0
  13. data/lib/docrb/parser/constant.rb +19 -0
  14. data/lib/docrb/parser/container.rb +305 -0
  15. data/lib/docrb/parser/deferred_singleton_class.rb +17 -0
  16. data/lib/docrb/parser/location.rb +43 -0
  17. data/lib/docrb/parser/method.rb +62 -0
  18. data/lib/docrb/parser/method_parameters.rb +85 -0
  19. data/lib/docrb/parser/module.rb +50 -0
  20. data/lib/docrb/parser/node_array.rb +24 -0
  21. data/lib/docrb/parser/reference.rb +25 -0
  22. data/lib/docrb/parser/reloader.rb +19 -0
  23. data/lib/docrb/parser/resolved_reference.rb +26 -0
  24. data/lib/docrb/parser/version.rb +7 -0
  25. data/lib/docrb/parser/virtual_container.rb +21 -0
  26. data/lib/docrb/parser/virtual_location.rb +9 -0
  27. data/lib/docrb/parser/virtual_method.rb +19 -0
  28. data/lib/docrb/parser.rb +139 -0
  29. data/lib/docrb-parser.rb +3 -0
  30. data/sig/docrb/core_extensions.rbs +24 -0
  31. data/sig/docrb/parser/attribute.rbs +18 -0
  32. data/sig/docrb/parser/call.rbs +17 -0
  33. data/sig/docrb/parser/class.rbs +34 -0
  34. data/sig/docrb/parser/comment.rbs +14 -0
  35. data/sig/docrb/parser/comment_parser.rbs +79 -0
  36. data/sig/docrb/parser/constant.rbs +15 -0
  37. data/sig/docrb/parser/container.rbs +91 -0
  38. data/sig/docrb/parser/deferred_singleton_class.rbs +12 -0
  39. data/sig/docrb/parser/location.rbs +24 -0
  40. data/sig/docrb/parser/method.rbs +34 -0
  41. data/sig/docrb/parser/method_parameters.rbs +34 -0
  42. data/sig/docrb/parser/module.rbs +14 -0
  43. data/sig/docrb/parser/node_array.rbs +12 -0
  44. data/sig/docrb/parser/reference.rbs +19 -0
  45. data/sig/docrb/parser/reloader.rbs +7 -0
  46. data/sig/docrb/parser/resolved_reference.rbs +22 -0
  47. data/sig/docrb/parser/virtual_method.rbs +17 -0
  48. data/sig/docrb/parser.rbs +5 -0
  49. metadata +109 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bfa4a19237733b6e0ace1d9af8584eeeb1dc671ec3a08c000d2b5f99adfcfa90
4
+ data.tar.gz: 02e8182ff82e529c89c99655411a7980a56c89a511263923616468c1bbbc0f65
5
+ SHA512:
6
+ metadata.gz: '048771cd5044806674962f019c76fa4fedbd5f2778ff461b3b7fd4eef77076bf93b4562283b5667318d0f68d99d7c816dd77b6a803c26fe8859f6f8358075ea7'
7
+ data.tar.gz: ba979ca0cb853ec2a3ef2542957f887f0b5b9e92355e380e1006bfd6883e06506b65ed49c3ed867116f7f11bfc9e32470b595a140e2917d669f5b407667e579c
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,75 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.2
3
+ SuggestExtensions: false
4
+ NewCops: enable
5
+ Exclude:
6
+ - spec/fixtures/*.rb
7
+
8
+ Style/StringLiterals:
9
+ Enabled: true
10
+ EnforcedStyle: double_quotes
11
+
12
+ Style/StringLiteralsInInterpolation:
13
+ Enabled: true
14
+ EnforcedStyle: double_quotes
15
+
16
+ Layout/LineLength:
17
+ Max: 120
18
+ Exclude:
19
+ - spec/**/**
20
+
21
+ Naming/VariableNumber:
22
+ Exclude:
23
+ - spec/**/**
24
+
25
+ Layout/FirstHashElementIndentation:
26
+ EnforcedStyle: consistent
27
+
28
+ Layout/EndAlignment:
29
+ EnforcedStyleAlignWith: start_of_line
30
+
31
+ Layout/MultilineMethodCallIndentation:
32
+ EnforcedStyle: indented
33
+
34
+ Style/Documentation:
35
+ Enabled: false
36
+
37
+ Layout/CaseIndentation:
38
+ EnforcedStyle: end
39
+
40
+ Layout/FirstArgumentIndentation:
41
+ EnforcedStyle: consistent_relative_to_receiver
42
+
43
+ Layout/ArgumentAlignment:
44
+ EnforcedStyle: with_fixed_indentation
45
+
46
+ Style/EmptyCaseCondition:
47
+ Enabled: false
48
+
49
+ Metrics/BlockLength:
50
+ Enabled: false
51
+
52
+ Metrics/ClassLength:
53
+ Enabled: false
54
+
55
+ Metrics/ModuleLength:
56
+ Enabled: false
57
+
58
+ Metrics/MethodLength:
59
+ Enabled: false
60
+
61
+ Metrics/AbcSize:
62
+ Enabled: false
63
+
64
+ Metrics/CyclomaticComplexity:
65
+ Enabled: false
66
+
67
+ Metrics/PerceivedComplexity:
68
+ Enabled: false
69
+
70
+ Naming/PredicateName:
71
+ Enabled: false
72
+
73
+ Naming/FileName:
74
+ Exclude:
75
+ - lib/docrb-parser.rb
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/docrb/parser/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "docrb-parser"
7
+ spec.version = Docrb::Parser::VERSION
8
+ spec.authors = ["Victor Gama"]
9
+ spec.email = ["hey@vito.io"]
10
+
11
+ spec.summary = "Docrb's Ruby Parser"
12
+ spec.description = <<~DESC
13
+ docrb-parser is responsible for parsing Ruby sources into a structured#{" "}
14
+ format for usage by docrb and docrb-html.
15
+ DESC
16
+ spec.homepage = "https://github.com/heyvito/docrb"
17
+ spec.license = "MIT"
18
+ spec.required_ruby_version = ">= 3.2"
19
+
20
+ spec.metadata["homepage_uri"] = spec.homepage
21
+ spec.metadata["source_code_uri"] = "#{spec.homepage}/tree/trunk/lib/docrb-parser"
22
+ spec.metadata["changelog_uri"] = spec.homepage
23
+ spec.metadata["rubygems_mfa_required"] = "true"
24
+
25
+ # Specify which files should be added to the gem when it is released.
26
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
27
+ spec.files = Dir.chdir(__dir__) do
28
+ `git ls-files -z`.split("\x0").reject do |f|
29
+ (File.expand_path(f) == __FILE__) ||
30
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
31
+ end
32
+ end
33
+ spec.bindir = "exe"
34
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
35
+ spec.require_paths = ["lib"]
36
+
37
+ spec.add_dependency "prism", "~> 0.13"
38
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Object
4
+ def own_methods = methods.sort - Object.methods
5
+ def object_id_hex = "0x#{object_id.to_s(16).rjust(16, "0")}"
6
+
7
+ def self.docrb_inspect(&)
8
+ return if @__inspect__installed__
9
+
10
+ @__inspect__installed__ = true
11
+ define_method(:to_s) { "<#{self.class.name}:#{object_id_hex} #{instance_exec(&)}>" }
12
+ define_method(:inspect) { to_s }
13
+ end
14
+
15
+ def try(method, *, **, &)
16
+ return nil unless respond_to? method
17
+
18
+ send(method, *, **, &)
19
+ end
20
+
21
+ def attr_list(*names)
22
+ names.map { "#{_1}: #{send(_1).inspect}" }.join(", ")
23
+ end
24
+
25
+ def self.docrb_inspect_attrs(*)
26
+ @inspectable_attrs = superclass.instance_variable_get(:@inspectable_attrs).dup || [] if @inspectable_attrs.nil?
27
+ @inspectable_attrs.append(*)
28
+ docrb_inspect { attr_list(*self.class.instance_variable_get(:@inspectable_attrs)) }
29
+ end
30
+
31
+ def self.visible_attr_reader(*)
32
+ attr_reader(*)
33
+
34
+ docrb_inspect_attrs(*)
35
+ end
36
+
37
+ def self.visible_attr_accessor(*)
38
+ attr_accessor(*)
39
+
40
+ docrb_inspect_attrs(*)
41
+ end
42
+ end
43
+
44
+ class Array
45
+ def first!
46
+ first or raise("#first! called on empty array")
47
+ end
48
+
49
+ alias old_first first
50
+
51
+ def first(*, **, &)
52
+ return old_first(*, **) unless block_given?
53
+
54
+ lazy.map(&).filter(&:itself).first
55
+ end
56
+ end
57
+
58
+ module Kernel
59
+ def then! = nil? ? nil : yield(self)
60
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class Parser
5
+ class Attribute
6
+ visible_attr_reader :name, :location
7
+ visible_attr_accessor :writer_visibility, :reader_visibility, :type
8
+ attr_accessor :parent, :doc
9
+
10
+ def initialize(parser, parent, node, name, type)
11
+ @object_id = parser.make_id(self)
12
+ @name = name
13
+ @parent = parent
14
+ @location = parser.location(node.location)
15
+ @type = type
16
+ (parent.current_visibility_modifier || :public).tap do |vis|
17
+ @writer_visibility = vis
18
+ @reader_visibility = vis
19
+ end
20
+ end
21
+
22
+ def id = @object_id
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class Parser
5
+ class Call
6
+ visible_attr_reader :name, :arguments, :parent, :location
7
+
8
+ def initialize(parser, parent, node)
9
+ @object_id = parser.make_id(self)
10
+ @name = node.name.to_sym
11
+ @arguments = []
12
+ @parent = parent
13
+ @location = parser.location(node.location)
14
+ node.arguments&.arguments&.each do |arg|
15
+ @arguments << case arg.type
16
+ when :constant_path_node, :constant_read_node
17
+ parser.unfurl_constant_path(arg)
18
+ else
19
+ arg
20
+ end
21
+ end
22
+ end
23
+
24
+ def id = @object_id
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class Parser
5
+ class Class < Container
6
+ visible_attr_accessor :inherits, :singleton
7
+ attr_accessor :node
8
+
9
+ def kind = :class
10
+
11
+ def initialize(parser, parent, node)
12
+ @default_constructor_visibility = :public
13
+
14
+ # WARNING: super WILL CALL methods that may require ivars to already be
15
+ # defined. Define those ivars before this point.
16
+ super
17
+
18
+ @inherits = if node&.type == :class_node && !node.superclass.nil?
19
+ reference(parser.unfurl_constant_path(node.superclass))
20
+ end
21
+
22
+ update_constructor_visibility!
23
+ adjust_split_attributes! :class
24
+ adjust_split_attributes! :instance
25
+ end
26
+
27
+ def unowned_classes
28
+ super.tap do |arr|
29
+ arr.merge_unowned(*@inherits.dereference!.all_classes) if @inherits&.fulfilled?
30
+ end
31
+ end
32
+
33
+ def unowned_modules
34
+ super.tap do |arr|
35
+ arr.merge_unowned(*@inherits.dereference!.unowned_modules) if @inherits&.fulfilled?
36
+ end
37
+ end
38
+
39
+ def unowned_instance_methods
40
+ super.tap do |arr|
41
+ arr.merge_unowned(*@inherits.dereference!.all_instance_methods) if @inherits&.fulfilled?
42
+ end
43
+ end
44
+
45
+ def unowned_class_methods
46
+ super.tap do |arr|
47
+ arr.merge_unowned(*@inherits.dereference!.unowned_class_methods) if @inherits&.fulfilled?
48
+ end
49
+ end
50
+
51
+ def unowned_class_attributes
52
+ super.tap do |arr|
53
+ arr.merge_unowned(*@inherits.dereference!.unowned_class_attributes) if @inherits&.fulfilled?
54
+ end
55
+ end
56
+
57
+ def unowned_instance_attributes
58
+ super.tap do |arr|
59
+ arr.merge_unowned(*@inherits.dereference!.unowned_instance_attributes) if @inherits&.fulfilled?
60
+ end
61
+ end
62
+
63
+ def is_inherited?(obj, parent)
64
+ return false if parent.nil? || !parent.fulfilled?
65
+ return true if parent.resolved.id == obj.parent.id
66
+
67
+ is_inherited?(obj, parent.dereference!.inherits)
68
+ end
69
+
70
+ def source_of(obj)
71
+ return :inherited if is_inherited?(obj, inherits)
72
+
73
+ super
74
+ end
75
+
76
+ def singleton! = tap { @singleton = true }
77
+
78
+ def singleton? = @singleton || false
79
+
80
+ def handle_parsed_node(parser, node) = parser.unhandled_node! node
81
+
82
+ def merge_singleton_class(other)
83
+ raise ArgumentError, "Cannot merge non-singleton class #{other.name} into #{name}" unless other.singleton?
84
+
85
+ class_methods.append(*other.instance_methods)
86
+ class_attributes.append(*other.instance_attributes)
87
+ return if other.location.try(:virtual?)
88
+
89
+ @defined_by << other.location
90
+ @location = other.location if @location.nil? || @location.try(:virtual)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class Parser
5
+ class Comment
6
+ attr_reader :comments
7
+
8
+ def initialize(parser, location)
9
+ @location = location
10
+ @file_path = location.file_path
11
+ @parser = parser
12
+ @comments = nil
13
+ locate
14
+ end
15
+
16
+ def locate
17
+ return if @location.virtual?
18
+
19
+ lines = @parser.lines_for(@file_path, @location.ast)
20
+
21
+ # NOTE: Regarding -2, -1 since `lines` is zero-indexed, and another -1
22
+ # to get the line before the current location
23
+ offset = @location.line_start - 2
24
+ comments = []
25
+
26
+ until offset.zero?
27
+ line = lines[offset]
28
+ break if line.nil?
29
+
30
+ break unless line.strip.start_with? "#"
31
+
32
+ comments << line
33
+ offset -= 1
34
+ end
35
+
36
+ @comments = comments.reverse.map(&:strip)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class Parser
5
+ # CommentParser implements a small parser for matching comment's contents to
6
+ # relevant references and annotations.
7
+ class CommentParser
8
+ NEWLINE = "\n"
9
+ POUND = "#"
10
+ SPACE = " "
11
+ DASH = "-"
12
+ COLON = ":"
13
+
14
+ attr_accessor :objects, :current_object, :cursor, :visibility
15
+
16
+ def self.parse(data)
17
+ new(data)
18
+ .tap(&:parse)
19
+ .then do |parser|
20
+ { meta: { visibility: parser.visibility }.compact, value: parser.objects }
21
+ end
22
+ end
23
+
24
+ def initialize(data)
25
+ @objects = []
26
+ @current_object = []
27
+ @data = data
28
+ .split(NEWLINE)
29
+ .map(&:rstrip)
30
+ .map { _1.gsub(/^\s*#\s?/, "") }
31
+ .join(NEWLINE)
32
+ .each_grapheme_cluster
33
+ .to_a
34
+ @data_len = @data.length
35
+ @visibility = nil
36
+
37
+ @cursor = 0
38
+ end
39
+
40
+ def at_end? = (cursor >= @data_len)
41
+
42
+ def will_end? = (cursor + 1 >= @data_len)
43
+
44
+ def at_start? = cursor.zero?
45
+
46
+ def peek = at_end? ? nil : @data[cursor]
47
+
48
+ def peek_next = will_end? ? nil : @data[cursor + 1]
49
+
50
+ def peek_prev = at_start? ? nil : @data[cursor - 1]
51
+
52
+ def advance = at_end? ? nil : peek.tap { self.cursor += 1 }
53
+
54
+ def match?(*args) = args.any? { _1 == peek }
55
+
56
+ def consume_spaces = (advance while match?(SPACE) && !at_end?)
57
+
58
+ def extract_while = (current_object << advance while yield && !at_end?)
59
+
60
+ def extract_until
61
+ until at_end?
62
+ break if yield
63
+
64
+ current_object << advance
65
+ end
66
+ end
67
+
68
+ def parse
69
+ parse_one until at_end?
70
+ flush_current_object
71
+ detect_field_list
72
+ process_code_examples
73
+ process_text_blocks
74
+ process_visibility
75
+ objects.map! { normalize_tree(_1) }
76
+ true
77
+ end
78
+
79
+ def flush_current_object
80
+ data = current_object.join.rstrip
81
+ return if data.empty?
82
+
83
+ objects << data
84
+ current_object.clear
85
+ end
86
+
87
+ def parse_one
88
+ extract_until { match? NEWLINE }
89
+ advance # Consume newline
90
+ if match? NEWLINE
91
+ advance # consume newline
92
+ flush_current_object
93
+ return
94
+ end
95
+ current_object << peek_prev if peek_prev == NEWLINE
96
+ end
97
+
98
+ FIELD_LIST_HEADING = /^([a-z][a-z_0-9]*:?)\s+-\s+(.*)/
99
+
100
+ def detect_field_list
101
+ objects.each.with_index do |obj, idx|
102
+ definitions = obj.split("\n").reject { _1.start_with? SPACE }
103
+
104
+ if (definitions.length == 1 && definitions.first =~ FIELD_LIST_HEADING) ||
105
+ (definitions.first =~ FIELD_LIST_HEADING && definitions[1] =~ FIELD_LIST_HEADING)
106
+ return process_field_list(obj, idx)
107
+ end
108
+ end
109
+ end
110
+
111
+ def process_field_list(obj, at)
112
+ lines = obj.lines
113
+ result = {}
114
+ last_key = nil
115
+ lines.each do |line|
116
+ if (match = FIELD_LIST_HEADING.match(line))
117
+ last_key = match[1]
118
+ contents = match[2]
119
+ result[last_key] = contents
120
+ elsif last_key
121
+ result[last_key] = "#{result[last_key]} #{line.lstrip}"
122
+ end
123
+ end
124
+ objects[at] = { type: :fields, value: result }
125
+ end
126
+
127
+ def process_text_blocks
128
+ objects.each.with_index do |obj, idx|
129
+ next objects[idx] = process_text_block(obj) if obj.is_a? String
130
+
131
+ case obj[:type]
132
+ when :fields
133
+ obj[:value].transform_values! { process_text_block(_1) }
134
+ when :code_example then next
135
+ else
136
+ raise NotImplementedError, "Can't process text block for type #{obj[:type]}"
137
+ end
138
+ end
139
+ end
140
+
141
+ def span(text) = { type: :span, value: text }
142
+
143
+ def process_text_block(text)
144
+ objs = [span(text)]
145
+ changed = true
146
+ while changed
147
+ changed = false
148
+ objs.each.with_index do |obj, idx|
149
+ next unless obj[:type] == :span
150
+
151
+ value = obj[:value]
152
+ changes = extract_method_reference(value) ||
153
+ extract_symbol(value) ||
154
+ extract_camelcase_identifier(value)
155
+ next unless changes
156
+
157
+ changes => { start_idx:, end_idx:, object: }
158
+ objs.delete_at(idx)
159
+ left = value[0...start_idx]
160
+ right = value[end_idx...]
161
+
162
+ new_items = [
163
+ (span(left) unless left.empty?),
164
+ object,
165
+ (span(right) unless right.empty?)
166
+ ]
167
+ objs.insert(idx, *new_items.compact)
168
+ changed = true
169
+ break
170
+ end
171
+ end
172
+ objs.length == 1 ? objs.first : objs
173
+ end
174
+
175
+ # rubocop:disable Layout/LineLength
176
+ 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*]+\))?/
177
+ # rubocop:enable Layout/LineLength
178
+
179
+ def extract_method_reference(text)
180
+ match = COMMENT_METHOD_REF_REGEXP.match(text) or return nil
181
+ value, class_path, target, invocation, name = match.to_a
182
+ class_path&.gsub!(/::$/, "")
183
+
184
+ {
185
+ start_idx: match.begin(0),
186
+ end_idx: match.end(0),
187
+ object: {
188
+ type: invocation == POUND ? :method_ref : :class_path_ref,
189
+ class_path:,
190
+ target:,
191
+ name:,
192
+ value:
193
+ }
194
+ }
195
+ end
196
+
197
+ COMMENT_SYMBOL_REGEXP = /:(!|[@$][a-z_][a-z0-9_]*|[a-z_][a-z0-9_]*|[a-z_][a-z0-9_]*[?!]?)/i
198
+
199
+ def extract_symbol(text)
200
+ match = COMMENT_SYMBOL_REGEXP.match(text) or return nil
201
+
202
+ {
203
+ start_idx: match.begin(0),
204
+ end_idx: match.end(0),
205
+ object: {
206
+ type: :symbol,
207
+ value: match[0]
208
+ }
209
+ }
210
+ end
211
+
212
+ CAMELCASE_IDENTIFIER_REGEXP = /[A-Z][a-z]+(?:[A-Z][a-z]+)+/
213
+
214
+ def extract_camelcase_identifier(text)
215
+ match = CAMELCASE_IDENTIFIER_REGEXP.match(text) or return nil
216
+
217
+ {
218
+ start_idx: match.begin(0),
219
+ end_idx: match.end(0),
220
+ object: {
221
+ type: :identifier,
222
+ value: match[0]
223
+ }
224
+ }
225
+ end
226
+
227
+ VISIBILITY_INDICATOR_REGEXP = /^\s*(public|private|internal|deprecated|protected):\s+/i
228
+
229
+ def process_visibility(obj = nil)
230
+ obj ||= objects.first
231
+ case obj
232
+ when Array then process_visibility(obj.first)
233
+ when Hash
234
+ return if obj[:type] == :fields
235
+ return process_visibility(obj[:value]) unless obj[:type] == :span
236
+
237
+ value = obj[:value]
238
+ match = VISIBILITY_INDICATOR_REGEXP.match(value) or return nil
239
+ obj[:value] = value[match.end(0)...]
240
+ @visibility = match[1]
241
+ nil
242
+ end
243
+ end
244
+
245
+ def process_code_examples
246
+ changed = true
247
+ while changed
248
+ start_at = nil
249
+ changed = false
250
+ objects.each.with_index do |obj, idx|
251
+ is_code = (obj.is_a?(String) && obj.start_with?(" "))
252
+ next start_at = idx if is_code && start_at.nil?
253
+
254
+ if !is_code && start_at
255
+ join_code_example_lines(start_at, idx)
256
+ start_at = nil
257
+ changed = true
258
+ break
259
+ end
260
+ end
261
+ join_code_example_lines(start_at, objects.length) unless start_at.nil?
262
+ end
263
+ end
264
+
265
+ def join_code_example_lines(start_at, end_at)
266
+ lines = objects[start_at...end_at]
267
+ .map { _1.split("\n") }
268
+ .map { |el| el.map { "#{_1[2...]}\n" } }
269
+ .flatten
270
+ objects.slice!(start_at...end_at)
271
+ objects.insert(start_at, {
272
+ type: :code_example,
273
+ source: lines.join("\n")
274
+ })
275
+ end
276
+
277
+ def normalize_tree(obj)
278
+ if obj.is_a?(Array)
279
+ { type: :block, value: obj }
280
+ elsif obj.is_a?(Hash) && obj[:type] == :span
281
+ { type: :block, value: [obj] }
282
+ elsif obj.is_a?(Hash) && obj[:type] == :fields
283
+ obj.tap { |f| f[:value].transform_values! { normalize_tree(_1) } }
284
+ else
285
+ obj
286
+ end
287
+ end
288
+ end
289
+ end
290
+ end