docrb-parser 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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