docrb 0.2.0

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