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,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class DocCompiler
5
+ # DocBlocks provides utilities for annotating and formatting documentation
6
+ # blocks.
7
+ class DocBlocks
8
+ # Internal: Transforms a provided reference on a given parent by a
9
+ # ttempting to resolve it into a specific object.
10
+ #
11
+ # ref - Reference to be resolved
12
+ # parent - The reference's parent
13
+ #
14
+ # Returns the reference itself by augmenting it with :ref_type and
15
+ # :ref_path information.
16
+ def self.process_reference(ref, parent)
17
+ return process_method_reference(ref, parent) if ref[:ref_type] == :method
18
+ return ref if ref[:ref_type] != :ambiguous
19
+
20
+ resolved = parent.resolve_ref(ref)
21
+ if resolved.nil?
22
+ ref[:ref_type] = :not_found
23
+ return ref
24
+ end
25
+ ref[:ref_type] = resolved.type
26
+ ref[:ref_path] = resolved.path
27
+ ref
28
+ end
29
+
30
+ def self.process_method_reference(ref, parent)
31
+ if (resolved = parent.resolve_ref(ref))
32
+ ref[:ref_path] = resolved.path
33
+ end
34
+ ref
35
+ end
36
+
37
+ # Internal: Processes a identifier on a provided parent. Returns an
38
+ # augmented reference with extra location information whenever it can
39
+ # be resolved.
40
+ #
41
+ # id - Identifier to be processed
42
+ # parent - Parent on which the identifier appeared.
43
+ def self.process_identifier(id, parent)
44
+ resolved = parent.resolve(id[:contents].to_sym)
45
+ if resolved.nil?
46
+ puts "Unresolved: #{id[:contents]} on #{parent.path}"
47
+ return {
48
+ type: :span,
49
+ contents: Markdown.inline("`#{id[:contents]}`")
50
+ }
51
+ end
52
+ {
53
+ type: :ref,
54
+ ref_type: resolved.type,
55
+ ref_path: resolved.path,
56
+ contents: id[:contents]
57
+ }
58
+ end
59
+
60
+ # Internal: Formats a given TextBlock object by expanding its contents
61
+ # into markdown annotations and attempting to resolve references and
62
+ # identifiers.
63
+ #
64
+ # Returns the updated block list.
65
+ def self.format_text_block(block, parent)
66
+ unless block[:contents].is_a? Array
67
+ block[:contents] = Markdown.inline(block[:contents])
68
+ return block
69
+ end
70
+ block[:contents].map! do |c|
71
+ case c[:type]
72
+ when :span
73
+ c[:contents] = Markdown.inline(c[:contents])
74
+ when :ref
75
+ c = process_reference(c, parent)
76
+ when :camelcase_identifier
77
+ c = process_identifier(c, parent)
78
+ end
79
+ c
80
+ end
81
+ block
82
+ end
83
+
84
+ # Public: Updates a given documentation block by augmenting markdown
85
+ # elements, references, fields and identifiers.
86
+ #
87
+ # doc - Documentation block to be processed
88
+ # parent - The parent container to which the block belongs to.
89
+ #
90
+ # Returns the updated block.
91
+ def self.prepare(doc, parent: nil)
92
+ return unless doc
93
+
94
+ doc[:contents].map! do |block|
95
+ case block[:type]
96
+ when :text_block
97
+ format_text_block(block, parent)
98
+ when :code_example
99
+ block[:contents] = Markdown.render_source(block[:contents])
100
+ when :field_block
101
+ block[:contents] = block[:contents].transform_values { |v| format_text_block(v, parent) }
102
+ else
103
+ puts "Skipped block with type #{block[:type]}"
104
+ end
105
+ block
106
+ end
107
+ doc
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class DocCompiler
5
+ # DocClass represents a documented class
6
+ class DocClass < BaseContainer
7
+ attr_accessor :inherits, :attributes
8
+
9
+ # Initializes a new instance with the provided parent, file, and object.
10
+ #
11
+ # parent - The parent container holding this class definition.
12
+ # filename - The filename defining this class.
13
+ # obj - The parsed class data.
14
+ def initialize(parent, filename, obj)
15
+ @inherits = nil
16
+ @attributes = ObjectContainer.new(self, DocAttribute, always_append: true)
17
+ super
18
+ end
19
+
20
+ def appended(filename, obj)
21
+ obj.fetch(:attr_accessor, []).each do |att|
22
+ @attributes.push(filename, att).accessor!
23
+ end
24
+
25
+ obj.fetch(:attr_reader, []).each do |att|
26
+ @attributes.push(filename, att).reader!
27
+ end
28
+
29
+ obj.fetch(:attr_writer, []).each do |att|
30
+ @attributes.push(filename, att).writer!
31
+ end
32
+ end
33
+
34
+ # Returns a Hash representation of this class definition
35
+ def to_h
36
+ super.to_h.merge({
37
+ inherits:,
38
+ attributes: merged_attributes.transform_values { |v| unpack(v) }
39
+ })
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class DocCompiler
5
+ # DocMethod represents a documented method
6
+ class DocMethod < Resolvable
7
+ attr_reader :parent, :type, :name, :args, :doc, :visibility,
8
+ :overriden_by, :defined_by
9
+
10
+ # Initializes a new DocMethod instance with a given parent, path, and
11
+ # object.
12
+ #
13
+ # parent - The object holding the represented method
14
+ # filename - The filename on which the method is defined on
15
+ # obj - The method definition itself
16
+ def initialize(parent, filename, obj)
17
+ super()
18
+ @parent = parent
19
+ @type = obj[:type]
20
+ @name = obj[:name]
21
+ @args = obj[:args]
22
+ @doc = obj[:doc]
23
+ @visibility = obj[:visibility]
24
+ @overriden_by = nil
25
+ @defined_by = FileRef.new(nil, filename, obj)
26
+ end
27
+
28
+ # Public: Marks this method as being overriden by a provided object in
29
+ # a given filename. This method initializes a new DocMethod instance using
30
+ # this instance's parent, the provided filename and object, and invokes
31
+ # #override! on it.
32
+ #
33
+ # filename - The filename defining the override for this method
34
+ # obj - The object representing the override for this method
35
+ def override(filename, obj)
36
+ override! DocMethod.new(@parent, filename, obj)
37
+ end
38
+
39
+ # Public: Marks this method as being overriden by the provided method
40
+ #
41
+ # method - DocMethod object overriding this method's implementation
42
+ def override!(method)
43
+ @overriden_by = method
44
+ end
45
+
46
+ def inspect
47
+ type = @type == :def ? "method" : "singleton method"
48
+ overriden = overriden_by.nil? ? "" : " overriden"
49
+ "#<DocMethod:#{format("0x%08x", object_id * 2)} #{visibility} #{type} #{name}#{overriden}>"
50
+ end
51
+
52
+ # Public: Returns a Hash representation of this method
53
+ def to_h
54
+ {
55
+ type:,
56
+ name:,
57
+ args:,
58
+ doc: DocBlocks.prepare(doc, parent: self),
59
+ visibility:,
60
+ overriden_by:,
61
+ defined_by: defined_by&.to_h
62
+ }
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class DocCompiler
5
+ # DocModule represents a documented Module object
6
+ class DocModule < BaseContainer
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class DocCompiler
5
+ # FileRef represents a file reference. This class is used to represent
6
+ # definition metadata of objects in files. Instances of this class
7
+ # indicates filenames and boundaries for a represented object.
8
+ class FileRef
9
+ attr_reader :filename, :start_at, :end_at
10
+
11
+ # Initializes a new FileRef instance.
12
+ #
13
+ # _parent - Unused.
14
+ # filename - Path to the file being referenced
15
+ # obj - The object being referenced in the provided filename
16
+ #
17
+ def initialize(_parent, filename, obj)
18
+ @filename = filename
19
+ @start_at, @end_at = obj.values_at(:start_at, :end_at)
20
+ end
21
+
22
+ # Public: Returns the source code portion of the represented reference
23
+ def ruby_source
24
+ f = File.read(filename).split("\n")[start_at - 1..end_at - 1]
25
+ f.join("\n")
26
+ end
27
+
28
+ # Public: Returns the reference's Hash representation
29
+ def to_h
30
+ source = ruby_source
31
+ {
32
+ filename:,
33
+ start_at:,
34
+ end_at:,
35
+ source:,
36
+ markdown_source: Markdown.render_source(source)
37
+ }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class DocCompiler
5
+ # ObjectContainer implements utilities for representing a container of
6
+ # objects with a common type.
7
+ class ObjectContainer
8
+ extend Forwardable
9
+
10
+ attr_reader :objects
11
+
12
+ # Initializes a new container with a given parent, containing objects with
13
+ # a provided class and options.
14
+ #
15
+ # parent - The container's parent
16
+ # cls - The class of represented items
17
+ # **opts - Options list. Currently, only `always_append` is supported,
18
+ # which indicates that all objects provided to the instance must
19
+ # be appended regardless of their type.
20
+ def initialize(parent, cls, **opts)
21
+ @cls = cls
22
+ @objects = []
23
+ @opts = opts
24
+ @parent = parent
25
+ end
26
+
27
+ def_delegators :@objects, :length, :each, :map, :find, :filter, :first, :[], :to_json
28
+
29
+ # Pushes a new object into the container. May raise ArgumentError in case
30
+ # the object being pushed is not compatible with the container's base
31
+ # object.
32
+ #
33
+ # filename - Name of the file which defines the object being pushed
34
+ # obj - The object being pushed
35
+ #
36
+ # Returns the new instance representing the pushed object.
37
+ def push(filename, obj)
38
+ if @cls.method_defined?(:name) &&
39
+ (instance = @objects.find { |o| o.name == obj[:name] }) &&
40
+ !@opts.fetch(:always_append, false)
41
+
42
+ return instance.merge(filename, obj) if instance.respond_to? :merge
43
+ return instance.override(filename, obj) if instance.respond_to? :override
44
+ return instance.append(filename, obj) if instance.respond_to? :append
45
+
46
+ raise ArgumentError, "cannot handle existing object #{obj[:name]} for container of type #{@cls.name}"
47
+ end
48
+
49
+ inst = @cls.new(@parent, filename, obj)
50
+ @objects << inst
51
+ inst
52
+ end
53
+
54
+ def inspect
55
+ opts = []
56
+ opts << "always_append" if @opts[:always_append]
57
+ count = if @objects.count.zero?
58
+ "empty"
59
+ else
60
+ "#{@objects.length} object#{@objects.length == 1 ? "" : "s"}"
61
+ end
62
+ obj_id = format("0x%08x", object_id * 2)
63
+ opts_repr = opts.empty? ? "" : " #{opts.join(", ")}"
64
+ "#<ObjectContainer:#{obj_id} containing #{@cls.name}, #{count}#{opts_repr}>"
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ # DocCompiler implements utilities to post-process documentation data
5
+ # generated by RubyParser.
6
+ class DocCompiler < Resolvable
7
+ autoload :FileRef, "docrb/doc_compiler/file_ref"
8
+ autoload :BaseContainer, "docrb/doc_compiler/base_container"
9
+ autoload :DocClass, "docrb/doc_compiler/doc_class"
10
+ autoload :DocModule, "docrb/doc_compiler/doc_module"
11
+ autoload :DocMethod, "docrb/doc_compiler/doc_method"
12
+ autoload :DocAttribute, "docrb/doc_compiler/doc_attribute"
13
+ autoload :ObjectContainer, "docrb/doc_compiler/object_container"
14
+ autoload :DocBlocks, "docrb/doc_compiler/doc_blocks"
15
+
16
+ attr_reader :classes, :modules, :methods, :parent
17
+
18
+ def initialize
19
+ super
20
+ @classes = ObjectContainer.new(self, DocClass)
21
+ @modules = ObjectContainer.new(self, DocModule)
22
+ @methods = ObjectContainer.new(self, DocMethod)
23
+ @parent = nil
24
+ end
25
+
26
+ # Appends a given object to the compiler collection. Raises ArgumentError in
27
+ # case the object cannot be appended to the current container.
28
+ #
29
+ # obj - Object to be appended
30
+ def append(obj)
31
+ filename = obj[:filename]
32
+ target = case obj[:type]
33
+ when :module
34
+ @modules
35
+ when :class
36
+ @classes
37
+ when :def, :defs
38
+ @methods
39
+ end
40
+
41
+ raise ArgumentError, "cannot append obj of type #{obj[:type]}" if target.nil?
42
+
43
+ target.push(filename, obj)
44
+ end
45
+
46
+ # Transforms the content's of the parser into a Hash representation
47
+ def to_h
48
+ {
49
+ modules: @modules.map(&:to_h),
50
+ classes: @classes.map(&:to_h),
51
+ methods: @methods.map(&:to_h)
52
+ }
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ # Renderer provides a Redcarpet renderer with Rouge extensions
5
+ class Renderer < Redcarpet::Render::HTML
6
+ def initialize(extensions = {})
7
+ super extensions.merge(link_attributes: { target: "_blank" })
8
+ end
9
+ include Rouge::Plugins::Redcarpet
10
+ end
11
+
12
+ # InlineRenderer provides a renderer for inline contents. This renderer
13
+ # does not emit paragraph tags.
14
+ class InlineRenderer < Renderer
15
+ def paragraph(text)
16
+ text
17
+ end
18
+ end
19
+
20
+ # Markdown provides utilities for generating HTML from markdown contents
21
+ class Markdown
22
+ # Internal: Creates a new renderer based on a provided type by setting
23
+ # sensible defaults.
24
+ #
25
+ # type - Type of the renderer to be initialised. Use Docrb::Renderer or
26
+ # InlineRenderer
27
+ def self.make_render(type)
28
+ Redcarpet::Markdown.new(
29
+ type,
30
+ fenced_code_blocks: true,
31
+ autolink: true
32
+ )
33
+ end
34
+
35
+ # Renders a given input using the default renderer.
36
+ #
37
+ # input - Markdown content to be rendered
38
+ #
39
+ # Returns an HTML string containing the rendered Markdown content
40
+ def self.render(input)
41
+ make_render(Renderer).render(input)
42
+ end
43
+
44
+ # Renders a given input using the inline renderer.
45
+ #
46
+ # input - Markdown content to be rendered
47
+ #
48
+ # Returns an HTML string containing the rendered Markdown content
49
+ def self.inline(input)
50
+ make_render(InlineRenderer).render(input)
51
+ end
52
+
53
+ # Renders a given Ruby source code into HTML
54
+ #
55
+ # source - Source code to be rendered
56
+ #
57
+ # Returns an HTML string containing the rendered source code
58
+ def self.render_source(source)
59
+ render("```ruby\n#{source}\n```")
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ unless Class.respond_to? :try?
4
+ class Object
5
+ def try?(method, *args, **kwargs, &)
6
+ send(method, *args, **kwargs, &) if respond_to?(method)
7
+ end
8
+
9
+ def self.try?(method, *args, **kwargs, &)
10
+ send(method, *args, **kwargs, &) if respond_to?(method)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ # Resolvable implements utilities for resolving names and class paths on a
5
+ # container context (module/class)
6
+ class Resolvable
7
+ # Returns a matcher for a provided name
8
+ #
9
+ # name - Name to match against
10
+ def by_name(name)
11
+ ->(obj) { obj.name == name }
12
+ end
13
+
14
+ # Returns a matcher for a provided name and type
15
+ #
16
+ # name - Name to match against
17
+ # type - Object type to match against (:def/:sdef/:class/:module...)
18
+ def by_name_and_type(name, type)
19
+ ->(obj) { obj.name == name && obj.type == type }
20
+ end
21
+
22
+ # Attempts to resolve a method with a provided name in the container's
23
+ # context. This method attempts to match for instance methods, class
24
+ # methods, and finally by recursively calling this method on the inherited
25
+ # members, if any.
26
+ #
27
+ # name - Name of the method to be resolved.
28
+ #
29
+ # Returns a method structure, or nil, in case none is found.
30
+ def resolve_method(name)
31
+ if (local = try?(:defs)&.find(&by_name(name)))
32
+ return local
33
+ end
34
+
35
+ if (local = try?(:sdefs)&.find(&by_name(name)))
36
+ return local
37
+ end
38
+
39
+ # Inherited?
40
+ if (inherited = try?(:inherits)&.try?(:resolve_method, name, type))
41
+ return inherited
42
+ end
43
+
44
+ nil
45
+ end
46
+
47
+ # Resolves a container using a provided reference.
48
+ #
49
+ # ref - Either a reference hash (containing :class_path, :target, :name), or
50
+ # a symbol representing the name of the container being resolved.
51
+ #
52
+ # Returns a matched container or nil, in case none is found.
53
+ def resolve_container(ref)
54
+ if ref.is_a? Hash
55
+ path = ref
56
+ .slice(:class_path, :target, :name)
57
+ .values
58
+ .flatten
59
+ .compact
60
+ return resolve_container(path)
61
+ end
62
+
63
+ ref = [ref] if ref.is_a? Symbol
64
+
65
+ if ref.length == 1
66
+ name = ref.first
67
+ # module?
68
+ if (mod = try?(:modules)&.find(&by_name(name)))
69
+ return mod
70
+ end
71
+
72
+ # class?
73
+ if (cls = try?(:classes)&.find(&by_name(name)))
74
+ return cls
75
+ end
76
+
77
+ # parent?
78
+ return @parent&.resolve_container(ref)
79
+ end
80
+
81
+ obj = self
82
+ while ref.length.positive?
83
+ obj = resolve_container(ref.shift)
84
+ return nil if obj.nil?
85
+ end
86
+
87
+ obj
88
+ end
89
+
90
+ # Resolves a provided name in the current containter's context. This method
91
+ # will attempt to return an object matching the provided name by looking for
92
+ # it in the following subcontainers: modules, classes, methods, attributes,
93
+ # and recursively performing the resolution on the container's parent, if
94
+ # any.
95
+ #
96
+ # name - Name of the object being resolved
97
+ #
98
+ # Returns the first object found by the provided name, or nil, in case none
99
+ # is found.
100
+ def resolve(name)
101
+ return nil if name.nil?
102
+
103
+ name = name.to_sym
104
+
105
+ # module?
106
+ if (mod = try?(:modules)&.find(&by_name(name)))
107
+ return mod
108
+ end
109
+
110
+ # class?
111
+ if (cls = try?(:classes)&.find(&by_name(name)))
112
+ return cls
113
+ end
114
+
115
+ # method?
116
+ if (met = resolve_method(name))
117
+ return met
118
+ end
119
+
120
+ # attribute?
121
+ if (att = try?(:attributes)&.find(&by_name(name)))
122
+ return att
123
+ end
124
+
125
+ if (obj = @parent&.resolve(name))
126
+ return obj
127
+ end
128
+
129
+ nil
130
+ end
131
+
132
+ # Resolves a provided ref by creating its path string representation and
133
+ # calling #resolve_qualified
134
+ #
135
+ # ref - Hash representing the reference being resolved.
136
+ #
137
+ # Returns the object under the provided reference, or nil.
138
+ def resolve_ref(ref)
139
+ path = ref
140
+ .slice(:class_path, :target, :name)
141
+ .values
142
+ .flatten
143
+ .compact
144
+ return resolve_qualified(path.join("::")) if path.length > 1
145
+
146
+ resolve(path[0])
147
+ end
148
+
149
+ # Resolves a qualified path under the current container's context.
150
+ #
151
+ # path - Path of the object being resolved. Must be a string containing the
152
+ # object name being searched, or a classpath for it (e.g.
153
+ # `Foo::Bar::Baz`)
154
+ def resolve_qualified(path)
155
+ components = path.split("::").map(&:to_sym)
156
+ obj = root
157
+ until components.empty?
158
+ obj = obj.resolve(components.shift)
159
+ break if obj.nil?
160
+ end
161
+ obj
162
+ end
163
+
164
+ # Returns the root object for the current container
165
+ def root
166
+ obj = self
167
+ obj = obj.parent while obj.parent
168
+ obj
169
+ end
170
+
171
+ # Returns the container's full qualified path
172
+ def path
173
+ return [] if parent.nil?
174
+
175
+ parent.path + [name]
176
+ end
177
+ end
178
+ end