docrb-html 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.
Files changed (104) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +21 -0
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +52 -0
  5. data/Gemfile +7 -0
  6. data/Gemfile.lock +37 -0
  7. data/assets/breadcrumb.scss +25 -0
  8. data/assets/checkbox.scss +34 -0
  9. data/assets/class_header.scss +75 -0
  10. data/assets/class_mod_name.scss +8 -0
  11. data/assets/component_list.scss +4 -0
  12. data/assets/container.scss +9 -0
  13. data/assets/doc_box.scss +79 -0
  14. data/assets/documentation_block.scss +5 -0
  15. data/assets/favicon.ico +0 -0
  16. data/assets/fonts.scss +105 -0
  17. data/assets/footer.scss +15 -0
  18. data/assets/images/balance.svg +9 -0
  19. data/assets/images/breadcrumb_separator.svg +1 -0
  20. data/assets/images/checkbox-off.svg +1 -0
  21. data/assets/images/checkbox-on.svg +1 -0
  22. data/assets/images/chevron.svg +1 -0
  23. data/assets/images/docrb-label.svg +20 -0
  24. data/assets/images/github.svg +11 -0
  25. data/assets/images/home.svg +1 -0
  26. data/assets/images/inherited.svg +1 -0
  27. data/assets/images/override.svg +1 -0
  28. data/assets/images/questionmark.svg +1 -0
  29. data/assets/images/rubygems.svg +9 -0
  30. data/assets/images/user.svg +12 -0
  31. data/assets/js/filtering.js +66 -0
  32. data/assets/links.scss +16 -0
  33. data/assets/markdown.scss +41 -0
  34. data/assets/method_argument.scss +75 -0
  35. data/assets/method_display.scss +27 -0
  36. data/assets/method_list.scss +51 -0
  37. data/assets/project_header.scss +55 -0
  38. data/assets/reference.scss +36 -0
  39. data/assets/shared.scss +23 -0
  40. data/assets/style.scss +43 -0
  41. data/assets/symbol.scss +5 -0
  42. data/assets/tab_bar.scss +22 -0
  43. data/assets/text_block.scss +23 -0
  44. data/assets/type_definition.scss +3 -0
  45. data/assets/typedef.scss +6 -0
  46. data/bin/console +15 -0
  47. data/bin/setup +8 -0
  48. data/exe/docrb-html +12 -0
  49. data/lib/renderer/component/breadcrumb.rb +14 -0
  50. data/lib/renderer/component/checkbox.rb +9 -0
  51. data/lib/renderer/component/class_header.rb +14 -0
  52. data/lib/renderer/component/class_mod_name.rb +9 -0
  53. data/lib/renderer/component/component_list.rb +15 -0
  54. data/lib/renderer/component/doc_box.rb +38 -0
  55. data/lib/renderer/component/documentation_block.rb +9 -0
  56. data/lib/renderer/component/footer.rb +9 -0
  57. data/lib/renderer/component/markdown.rb +9 -0
  58. data/lib/renderer/component/method_argument.rb +106 -0
  59. data/lib/renderer/component/method_display.rb +9 -0
  60. data/lib/renderer/component/method_list.rb +9 -0
  61. data/lib/renderer/component/project_header.rb +9 -0
  62. data/lib/renderer/component/reference.rb +24 -0
  63. data/lib/renderer/component/symbol.rb +9 -0
  64. data/lib/renderer/component/tab_bar.rb +9 -0
  65. data/lib/renderer/component/text_block.rb +15 -0
  66. data/lib/renderer/component/type_definition.rb +9 -0
  67. data/lib/renderer/component/typedef.rb +9 -0
  68. data/lib/renderer/component.rb +50 -0
  69. data/lib/renderer/core_extensions.rb +11 -0
  70. data/lib/renderer/defs/specialized_object.rb +172 -0
  71. data/lib/renderer/defs/specialized_projection.rb +31 -0
  72. data/lib/renderer/defs.rb +180 -0
  73. data/lib/renderer/helpers.rb +82 -0
  74. data/lib/renderer/metadata.rb +44 -0
  75. data/lib/renderer/page.rb +17 -0
  76. data/lib/renderer/template.rb +38 -0
  77. data/lib/renderer/version.rb +5 -0
  78. data/lib/renderer.rb +129 -0
  79. data/renderer.gemspec +31 -0
  80. data/script/makecomponent +23 -0
  81. data/script/reload.js +14 -0
  82. data/script/serve +2 -0
  83. data/script/watch +17 -0
  84. data/templates/base.erb +25 -0
  85. data/templates/breadcrumb.erb +28 -0
  86. data/templates/checkbox.erb +8 -0
  87. data/templates/class_header.erb +53 -0
  88. data/templates/class_mod_name.erb +3 -0
  89. data/templates/component_list.erb +21 -0
  90. data/templates/doc_box.erb +82 -0
  91. data/templates/documentation_block.erb +18 -0
  92. data/templates/footer.erb +9 -0
  93. data/templates/markdown.erb +3 -0
  94. data/templates/method_argument.erb +29 -0
  95. data/templates/method_display.erb +28 -0
  96. data/templates/method_list.erb +25 -0
  97. data/templates/project_header.erb +38 -0
  98. data/templates/reference.erb +14 -0
  99. data/templates/symbol.erb +3 -0
  100. data/templates/tab_bar.erb +7 -0
  101. data/templates/text_block.erb +16 -0
  102. data/templates/type_definition.erb +4 -0
  103. data/templates/typedef.erb +3 -0
  104. metadata +178 -0
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Renderer
4
+ class Component
5
+ class MethodArgument < Component
6
+ prop :type, :name, :value, :value_type, :computed
7
+
8
+ def prepare
9
+ @computed = {
10
+ rest: rest_arg,
11
+ name:,
12
+ continuation: continuation_for_type,
13
+ value: value_for_argument
14
+ }
15
+ end
16
+
17
+ def continuation_for_type
18
+ if type == "kwarg" || type == "kwoptarg"
19
+ :colon
20
+ elsif type&.index("opt")&.zero?
21
+ :equal
22
+ end
23
+ end
24
+
25
+ REST_ARG_BY_TYPE = {
26
+ "kwrestarg" => :double,
27
+ "restarg" => :single,
28
+ "blockarg" => :block
29
+ }.freeze
30
+
31
+ def rest_arg
32
+ REST_ARG_BY_TYPE[type]
33
+ end
34
+
35
+ def value_for_argument
36
+ return if value_type.nil?
37
+
38
+ case value_type
39
+ when "sym"
40
+ { kind: :symbol, value: }
41
+ when "bool"
42
+ { kind: :symbol, value: value.inspect }
43
+ when "nil"
44
+ { kind: :symbol, value: "nil" }
45
+ when "int"
46
+ { kind: :number, value: }
47
+ when "str"
48
+ { kind: :string, value: }
49
+ when "send"
50
+ method_call_argument(value)
51
+ when "const"
52
+ const_value(value)
53
+ else
54
+ { kind: :plain, value: }
55
+ end
56
+ end
57
+
58
+ def method_call_argument(value)
59
+ class_path = value[:target].map do |i|
60
+ next { kind: :symbol, value: "self" } if i == "self"
61
+
62
+ # TODO: Is this plain?
63
+ [{ kind: :class_or_module, value: i }, { kind: :plain, value: "::" }]
64
+ end.flatten
65
+
66
+ class_path.pop
67
+
68
+ {
69
+ kind: :method_call_argument,
70
+ value: [
71
+ class_path,
72
+ { kind: :plain, value: "." },
73
+ { kind: :plain, value: value[:name] }
74
+ ].flatten
75
+ }
76
+ end
77
+
78
+ def const_value(value)
79
+ class_path = value[:target].map do |i|
80
+ # TODO: Is this plain?
81
+ [{ kind: :class_or_module, value: i }, { kind: :continuation, double: true }]
82
+ end.flatten
83
+
84
+ class_path.pop
85
+
86
+ if class_path.empty?
87
+ return {
88
+ kind: :const,
89
+ value: [
90
+ { kind: :class_or_module, value: value[:name] }
91
+ ].flatten
92
+ }
93
+ end
94
+
95
+ {
96
+ kind: :const,
97
+ value: [
98
+ class_path,
99
+ { kind: :continuation, double: true },
100
+ { kind: :class_or_module, value: value[:name] }
101
+ ].flatten
102
+ }
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Renderer
4
+ class Component
5
+ class MethodDisplay < Component
6
+ prop :visibility, :type, :name, :href, :args, :doc, :decoration, :short_type
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Renderer
4
+ class Component
5
+ class MethodList < Component
6
+ prop :list
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Renderer
4
+ class Component
5
+ class ProjectHeader < Component
6
+ prop :name, :description, :owner, :license, :links
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Renderer
4
+ class Component
5
+ class Reference < Component
6
+ prop :ref, :unresolved, :path
7
+
8
+ def prepare
9
+ @unresolved = !ref[:ref_path]
10
+ return if @unresolved
11
+
12
+ components = ref[:ref_path].dup
13
+ @path = if ref[:ref_type] == "method"
14
+ method_name = components.pop
15
+ parent_name = components.pop
16
+ components + ["#{parent_name}.html#{method_name}"]
17
+ else
18
+ last = components.pop
19
+ (components + ["#{last}.html"]).compact
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Renderer
4
+ class Component
5
+ class Symbol < Component
6
+ prop :name
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Renderer
4
+ class Component
5
+ class TabBar < Component
6
+ prop :items, :selected_index
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Renderer
4
+ class Component
5
+ class TextBlock < Component
6
+ prop :list
7
+
8
+ def prepare
9
+ return unless !@list.nil? && !@list.is_a?(Array)
10
+
11
+ @list = [{ type: "html", contents: @list }]
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Renderer
4
+ class Component
5
+ class TypeDefinition < Component
6
+ prop :type, :name, :href
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Renderer
4
+ class Component
5
+ class Typedef < Component
6
+ prop :name
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Renderer
4
+ class Component
5
+ attr_accessor :id
6
+
7
+ def self.prop(*names)
8
+ attr_accessor(*names)
9
+
10
+ @props ||= []
11
+ @props.append(*names)
12
+ end
13
+
14
+ class << self
15
+ attr_writer :template
16
+ end
17
+
18
+ def self.template
19
+ return @template_instance if @template_instance
20
+
21
+ t_name = @template || name.split("::").last.snakify
22
+ @template_instance = Template.new("templates/#{t_name}.erb")
23
+ end
24
+
25
+ def props = @props ||= self.class.instance_variable_get(:@props) || []
26
+ def template = self.class.template
27
+
28
+ def initialize(**kwargs)
29
+ @id = kwargs.delete(:id)
30
+ kwargs.each do |k, v|
31
+ raise ArgumentError, "Unknown property #{k} for #{self.class.name}" unless props.include? k
32
+
33
+ send("#{k}=", v)
34
+ end
35
+ end
36
+
37
+ def prepare; end
38
+
39
+ def render(&)
40
+ prepare
41
+ opts = props.map { [_1, send(_1)] }.to_h
42
+ opts[:id] = @id
43
+ template.render(HELPERS, **opts, &)
44
+ end
45
+ end
46
+ end
47
+
48
+ Dir[Pathname.new(__dir__).join("component/*.rb")].each do |file|
49
+ require_relative "component/#{Pathname.new(file).basename}"
50
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class String
4
+ def snakify
5
+ gsub(/::/, "/")
6
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
7
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
8
+ .tr("-", "_")
9
+ .downcase
10
+ end
11
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Renderer
4
+ class Defs
5
+ class SpecializedObject
6
+ def initialize(obj, parent, provider)
7
+ @obj = obj
8
+ @provider = provider
9
+ prepare(parent)
10
+ end
11
+
12
+ def self.specialize(obj, parent, provider)
13
+ return nil if obj.nil?
14
+
15
+ new(obj, parent, provider)
16
+ end
17
+
18
+ def [](key) = @obj[key]
19
+ def fetch(*, **, &) = @obj.fetch(*, **, &)
20
+
21
+ def resolve(name)
22
+ named = named_as(name)
23
+ modules.find(&named) \
24
+ || classes.find(&named) \
25
+ || attributes&.find(&named) \
26
+ || defs.find(&named) \
27
+ || sdefs.find(&named)
28
+ end
29
+
30
+ def resolve_inheritance(name)
31
+ named = named_as(name)
32
+ modules.find(&named) \
33
+ || classes.find(&named) \
34
+ || parent&.resolve_inheritance(name)
35
+ end
36
+
37
+ def resolve_parent(name)
38
+ parent = self[:parent]
39
+ return { type: "Unknown Ref", name: } unless parent
40
+
41
+ parent.resolve(name) || parent.resolve_parent(name)
42
+ end
43
+
44
+ def resolve_path(path)
45
+ p = path.dup
46
+ obj = self
47
+ until p.empty?
48
+ obj = obj.resolve(p[0])
49
+ return unless obj
50
+
51
+ p.shift
52
+ end
53
+ obj
54
+ end
55
+
56
+ def resolve_qualified(obj)
57
+ result = nil
58
+ query = nil
59
+
60
+ if obj[:class_path]&.length&.> 0
61
+ query = obj[:class_path] + [obj[:name]]
62
+ result = root.resolve_path(query)
63
+ else
64
+ query = obj[:name]
65
+ result = resolve(query)
66
+ result ||= resolve_inheritance(query)
67
+ end
68
+
69
+ puts "Qualified resolution of #{query.inspect} by #{name} failed." unless result
70
+
71
+ result
72
+ end
73
+
74
+ def method_missing(method_name, *args, **kwargs, &)
75
+ var = "@#{method_name}".to_sym
76
+ if instance_variables.include?(var)
77
+ instance_variable_get(var)
78
+ elsif @obj.key? method_name
79
+ @obj.fetch(method_name)
80
+ else
81
+ super
82
+ end
83
+ end
84
+
85
+ def respond_to_missing?(method_name, include_private = false)
86
+ instance_variables.include?("@#{method_name}".to_sym) || @obj.key?(method_name) || super
87
+ end
88
+
89
+ def to_s
90
+ "<SpecializedObject for #{@provider.path_of(@obj).join("::")}>"
91
+ end
92
+
93
+ def inspect = to_s
94
+
95
+ def prepare_inheritance
96
+ @inheritance_prepared = true
97
+ @inherits = coerce_inheritance_data(@obj[:inherits])
98
+ @extends = @obj[:extends]&.map { resolve_qualified _1 }
99
+ @includes = @obj[:includes]&.map { resolve_qualified _1 }
100
+ @classes.each(&:prepare_inheritance)
101
+ @modules.each(&:prepare_inheritance)
102
+ end
103
+
104
+ private
105
+
106
+ def make_path
107
+ r = [name]
108
+ p = parent
109
+ while p
110
+ r << p.name
111
+ p = p.parent
112
+ end
113
+
114
+ r.reverse
115
+ end
116
+
117
+ def parent_of(parent)
118
+ return unless parent
119
+
120
+ p = parent
121
+ while p
122
+ return p unless p.parent
123
+
124
+ p = p.parent
125
+ end
126
+
127
+ p
128
+ end
129
+
130
+ def specialize_defs(defs, _parent)
131
+ defs.values.map do |i|
132
+ decoration = if i[:source] == "inheritance"
133
+ "inherited"
134
+ elsif i[:overriding]
135
+ "override"
136
+ else
137
+ ""
138
+ end
139
+
140
+ origin = i[:source]
141
+
142
+ @provider.find_source(i)
143
+ .merge({ decoration:, origin: })
144
+ end
145
+ end
146
+
147
+ def coerce_inheritance_data(obj)
148
+ return obj if obj.nil?
149
+
150
+ resolve_inheritance(obj) || obj
151
+ end
152
+
153
+ def prepare(parent)
154
+ @inheritance_prepared = true
155
+ @parent = parent
156
+ @defs = specialize_defs(@obj[:defs], self)
157
+ @sdefs = specialize_defs(@obj[:sdefs], self)
158
+ @attributes = (@obj[:attributes] || {}).values.map do |v|
159
+ @provider.prepare_attr(v)
160
+ end
161
+
162
+ @root = parent_of(parent)
163
+ @path = make_path
164
+
165
+ @classes = @obj[:classes].map { SpecializedObject.specialize(_1, self, @provider) }
166
+ @modules = @obj[:modules].map { SpecializedObject.specialize(_1, self, @provider) }
167
+ end
168
+
169
+ def named_as(n) = ->(o) { o.name == n }
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Renderer
4
+ class Defs
5
+ class SpecializedProjection < Array
6
+ def initialize(provider)
7
+ super()
8
+ @provider = provider
9
+ replace((provider.modules + provider.classes)
10
+ .map { provider.specialize_object _1 }
11
+ .each(&:prepare_inheritance))
12
+ end
13
+
14
+ def find_path(path)
15
+ path = @provider.path_of(path) unless path.is_a? Array
16
+ p = path.dup
17
+ obj = find { _1.name == p.first }
18
+ p.shift
19
+ return unless obj
20
+
21
+ until p.empty?
22
+ obj = obj.resolve(p.first)
23
+ p.shift
24
+ return unless obj
25
+ end
26
+
27
+ obj
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "defs/specialized_object"
4
+ require_relative "defs/specialized_projection"
5
+
6
+ class Renderer
7
+ class Defs
8
+ class << self
9
+ attr_reader :singleton
10
+ end
11
+
12
+ class << self
13
+ attr_writer :singleton
14
+ end
15
+
16
+ def initialize(base, metadata)
17
+ @data = JSON.parse(File.read("#{base}/data.json"), symbolize_names: true)
18
+ @meta = metadata
19
+ Defs.singleton = self
20
+ end
21
+
22
+ attr_reader :meta
23
+
24
+ def classes = @classes ||= make_paths(@data[:classes])
25
+ def modules = @modules ||= make_paths(@data[:modules])
26
+
27
+ def make_paths(obj)
28
+ return [] unless obj
29
+
30
+ obj.each { set_parent(_1, nil) }
31
+ end
32
+
33
+ def set_parent(obj, parent)
34
+ obj[:parent] = parent
35
+ obj[:classes].each { set_parent(_1, obj) }
36
+ obj[:modules].each { set_parent(_1, obj) }
37
+ end
38
+
39
+ def document_outline
40
+ (classes + modules).map { outline(_1) }
41
+ end
42
+
43
+ def outline(object, level = 0)
44
+ defs = object[:defs].values.map { map_method(_1) }.sort_by { _1[:name] }
45
+ sdefs = object[:sdefs].values.map { map_method(_1) }.sort_by { _1[:name] }
46
+ attributes = object[:attributes]&.values&.map { prepare_attr(_1) }&.sort_by { _1[:name] }
47
+
48
+ {
49
+ level:,
50
+ name: object[:name],
51
+ type: object[:type],
52
+ classes: object[:classes].map { outline(_1, level + 1) },
53
+ modules: object[:modules].map { outline(_1, level + 1) },
54
+ defs: defs + sdefs,
55
+ attributes:
56
+ }
57
+ end
58
+
59
+ def map_method(met)
60
+ decoration = if met[:source] == "inheritance"
61
+ "inherited"
62
+ elsif met[:overriding]
63
+ "override"
64
+ end
65
+
66
+ obj = find_source(met)
67
+ type = [].tap do |arr|
68
+ arr << "Class" if obj[:type] == "defs"
69
+ arr << "Method"
70
+ end.join(" ")
71
+
72
+ {
73
+ name: obj[:name],
74
+ visibility: obj[:visibility],
75
+ args: obj[:args],
76
+ type:,
77
+ short_type: obj[:type],
78
+ doc: obj[:doc],
79
+ decoration:
80
+ }
81
+ end
82
+
83
+ def find_source(met)
84
+ obj = met
85
+ obj = obj[:definition] while obj[:source] != "source"
86
+ obj[:definition]
87
+ end
88
+
89
+ def prepare_attr(met)
90
+ decoration = if met[:source] == "inheritance"
91
+ "inherited"
92
+ elsif met[:overriding]
93
+ "override"
94
+ end
95
+ origin = met[:source]
96
+ att = find_source(met)
97
+ visibility = if att[:reader_visibility] == "public" && att[:writer_visibility] == "public"
98
+ "read/write"
99
+ elsif att[:reader_visibility] == "public" && att[:writer_visibility] != "public"
100
+ "read-only"
101
+ else
102
+ "write-only"
103
+ end
104
+
105
+ {
106
+ name: att[:name],
107
+ type: "Attribute",
108
+ visibility:,
109
+ decoration:,
110
+ origin:,
111
+ doc: att[:doc]
112
+ }
113
+ end
114
+
115
+ def path_of(item)
116
+ p = []
117
+ parent = item
118
+ until parent.nil?
119
+ p << parent[:name]
120
+ parent = parent[:parent]
121
+ end
122
+ p.reverse
123
+ end
124
+
125
+ def definitions_of(item)
126
+ item[:defined_by]&.map do |d|
127
+ path_components = d[:filename].split("/")
128
+ {
129
+ name: path_components.last,
130
+ href: git_url(d)
131
+ }
132
+ end
133
+ end
134
+
135
+ def git_url(definition)
136
+ "#{@meta.git_url}/blob/#{@meta.git_tip}#{definition[:filename].gsub(@meta.git_root,
137
+ "")}#L#{definition[:start_at]}"
138
+ end
139
+
140
+ def clean_file_path(definition) = definition[:filename].gsub(meta.git_root, "")
141
+
142
+ def specialized_projection
143
+ @specialized_projection ||= SpecializedProjection.new(self)
144
+ end
145
+
146
+ def specialize_data
147
+ @specialize_data ||= (data[:modules] + data[:classes])
148
+ .map { specialize_object _1 }
149
+ .each(&:prepare_inheritance)
150
+ end
151
+
152
+ def specialize_object(obj, parent = nil)
153
+ SpecializedObject.specialize(obj, parent, self)
154
+ end
155
+
156
+ def by_name(n) = ->(o) { o[:name] == n }
157
+
158
+ def doc_for(path)
159
+ path = path_of(path) if path.is_a? Hash
160
+ path = path.dup
161
+ named = by_name(path.shift)
162
+ root = classes.find(&named) || modules.find(&named)
163
+
164
+ return unless root
165
+
166
+ find_recursive(path, root)
167
+ end
168
+
169
+ def find_recursive(path, root)
170
+ return root if path.empty?
171
+
172
+ named = by_name(path.shift)
173
+ next_item = root.classes.find(&named) || root.modules.find(&named)
174
+
175
+ return nil unless next_item
176
+
177
+ find_recursive(path, next_item)
178
+ end
179
+ end
180
+ end