lookbooklet 0.0.1

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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +234 -0
  4. data/bin/booklet +6 -0
  5. data/lib/booklet/analyzer.rb +28 -0
  6. data/lib/booklet/cli/cli.rb +14 -0
  7. data/lib/booklet/cli/commands/analyze_command.rb +45 -0
  8. data/lib/booklet/cli/commands/version_command.rb +13 -0
  9. data/lib/booklet/cli/concerns/colorful.rb +51 -0
  10. data/lib/booklet/concerns/locatable.rb +22 -0
  11. data/lib/booklet/file.rb +90 -0
  12. data/lib/booklet/helpers.rb +19 -0
  13. data/lib/booklet/node.rb +297 -0
  14. data/lib/booklet/nodes/anon_node.rb +9 -0
  15. data/lib/booklet/nodes/asset_node.rb +15 -0
  16. data/lib/booklet/nodes/directory_node.rb +17 -0
  17. data/lib/booklet/nodes/document_node.rb +11 -0
  18. data/lib/booklet/nodes/file_node.rb +15 -0
  19. data/lib/booklet/nodes/folder_node.rb +13 -0
  20. data/lib/booklet/nodes/prose_node.rb +7 -0
  21. data/lib/booklet/nodes/scenario_node.rb +13 -0
  22. data/lib/booklet/nodes/spec_node.rb +19 -0
  23. data/lib/booklet/object.rb +23 -0
  24. data/lib/booklet/options.rb +31 -0
  25. data/lib/booklet/value.rb +16 -0
  26. data/lib/booklet/values/code_snippet.rb +25 -0
  27. data/lib/booklet/values/method_snippet.rb +20 -0
  28. data/lib/booklet/values/node_type.rb +55 -0
  29. data/lib/booklet/values/parser_result.rb +11 -0
  30. data/lib/booklet/values/source_location.rb +17 -0
  31. data/lib/booklet/values/text_snippet.rb +21 -0
  32. data/lib/booklet/values.rb +31 -0
  33. data/lib/booklet/version.rb +5 -0
  34. data/lib/booklet/visitor.rb +58 -0
  35. data/lib/booklet/visitors/ascii_tree_renderer.rb +36 -0
  36. data/lib/booklet/visitors/entity_transformer.rb +18 -0
  37. data/lib/booklet/visitors/filesystem_loader.rb +18 -0
  38. data/lib/booklet/visitors/frontmatter_extractor.rb +10 -0
  39. data/lib/booklet/visitors/preview_class_parser.rb +42 -0
  40. data/lib/booklet/yard_parser.rb +23 -0
  41. data/lib/booklet.rb +29 -0
  42. data/lib/lookbooklet.rb +3 -0
  43. metadata +182 -0
@@ -0,0 +1,297 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class Node < Booklet::Object
5
+ include Enumerable
6
+ include Comparable
7
+ include Values
8
+
9
+ prop :ref, String, :positional, reader: :public do |value|
10
+ value.to_s unless value.nil?
11
+ end
12
+
13
+ attr_reader :parent
14
+ protected attr_writer :parent
15
+
16
+ after_initialize do
17
+ @parent = nil
18
+ @children = []
19
+ end
20
+
21
+ def ref_path
22
+ @ref_path ||= [ancestors&.map(&:ref)&.reverse, ref].flatten.compact.join("/")
23
+ end
24
+
25
+ # @!group Ancestry
26
+
27
+ def root
28
+ root = self
29
+ root = root.parent until root.root?
30
+ root
31
+ end
32
+
33
+ def root?
34
+ parent.nil?
35
+ end
36
+
37
+ def ancestors
38
+ return nil if root?
39
+
40
+ ancestors = []
41
+ prev_parent = parent
42
+ while prev_parent
43
+ ancestors << prev_parent
44
+ prev_parent = prev_parent.parent
45
+ end
46
+ ancestors
47
+ end
48
+
49
+ # @!endgroup
50
+
51
+ # @!group Descendants
52
+
53
+ def children(&block)
54
+ if block_given?
55
+ @children.each(&block)
56
+ self
57
+ else
58
+ @children.clone
59
+ end
60
+ end
61
+
62
+ delegate :[], to: :children
63
+
64
+ def children?
65
+ children.any?
66
+ end
67
+
68
+ def depth
69
+ ancestors&.size || 0
70
+ end
71
+
72
+ def descendants
73
+ filter { _1 != self }
74
+ end
75
+
76
+ def first_child
77
+ @children.first
78
+ end
79
+
80
+ def last_child
81
+ @children.last
82
+ end
83
+
84
+ def first_child?
85
+ self == first_child
86
+ end
87
+
88
+ def last_child?
89
+ self == last_child
90
+ end
91
+
92
+ def has_child?(node)
93
+ children.find { _1.ref == node.ref && _1.type == node.type }
94
+ end
95
+
96
+ def leaf?
97
+ !children?
98
+ end
99
+
100
+ def branch?
101
+ !leaf?
102
+ end
103
+
104
+ # @!endgroup
105
+
106
+ # @!group Siblings
107
+
108
+ def siblings
109
+ siblings = root? ? [] : parent.children.filter { _1 != self }
110
+ if block_given?
111
+ children.each(&block)
112
+ self
113
+ else
114
+ siblings
115
+ end
116
+ end
117
+
118
+ def first_sibling
119
+ root? ? self : parent.children.first
120
+ end
121
+
122
+ def first_sibling?
123
+ self == first_sibling
124
+ end
125
+
126
+ def last_sibling
127
+ root? ? self : parent.children.last
128
+ end
129
+
130
+ def last_sibling?
131
+ self == last_sibling
132
+ end
133
+
134
+ def next_sibling
135
+ return nil if root?
136
+
137
+ position = parent.children.index(self)
138
+ parent.children.at(position + 1) if position
139
+ end
140
+
141
+ def previous_sibling
142
+ return nil if root?
143
+
144
+ position = parent.children.index(self)
145
+ parent.children.at(position - 1) if position&.positive?
146
+ end
147
+
148
+ # @!endgroup
149
+
150
+ # @!group Adding children
151
+
152
+ def add(node)
153
+ validate_child!(node)
154
+
155
+ @children << node
156
+ node.parent = self
157
+ node
158
+ end
159
+
160
+ alias_method :<<, :add
161
+
162
+ def validate_child!(node)
163
+ raise ArgumentError, "Only Node instances can be added as children" unless node.is_a?(Node)
164
+ raise ArgumentError, "`#{node.ref}` is already a child of node `#{ref}`" if has_child?(node)
165
+ raise ArgumentError, "`#{node.ref}` is already attached to another node" unless node.root?
166
+
167
+ raise ArgumentError, "Parent node does not accept children" if valid_child_types.nil?
168
+
169
+ unless valid_child_types.include?(node.type)
170
+ raise TypeError, "Invalid node type '#{node.type}' - must be one of [#{valid_child_types.join(", ")}]"
171
+ end
172
+ end
173
+
174
+ def push(*children)
175
+ children.each { add(_1) }
176
+ self
177
+ end
178
+
179
+ # @!endgroup
180
+
181
+ # @!group Iteration
182
+
183
+ def each_node
184
+ return to_enum unless block_given?
185
+
186
+ node_stack = [self]
187
+ until node_stack.empty?
188
+ current = node_stack.shift
189
+ next unless current
190
+ yield current
191
+ node_stack = current.children.concat(node_stack)
192
+ end
193
+
194
+ self if block_given?
195
+ end
196
+
197
+ protected alias_method :each, :each_node
198
+
199
+ # @!endgroup
200
+
201
+ # @!group Comparison
202
+
203
+ def <=>(other)
204
+ return nil if other.nil? || !other.is_a?(Node)
205
+
206
+ self_index = root.each_node.to_a.index(self)
207
+ other_index = root.each_node.to_a.index(other)
208
+
209
+ return nil if other_index.nil?
210
+
211
+ other_index <=> self_index
212
+ end
213
+
214
+ # @!group Visting
215
+
216
+ def accept(visitor)
217
+ class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
218
+ def accept(visitor)
219
+ visitor.visit_#{type}(self)
220
+ end
221
+ RUBY
222
+
223
+ accept(visitor)
224
+ end
225
+
226
+ # @!endgroup
227
+
228
+ # @!group Type & type checking
229
+
230
+ def type
231
+ @type ||= NodeType(self.class)
232
+ end
233
+
234
+ def method_missing(name, ...)
235
+ if name.end_with?("?")
236
+ type.public_send(name)
237
+ elsif name.start_with?("each_")
238
+ filter_type = name.to_s.delete_prefix("each_")
239
+ filter { _1.type == filter_type }
240
+ else
241
+ super
242
+ end
243
+ end
244
+
245
+ def respond_to_missing?(name, ...)
246
+ name.end_with?("?") || name.start_with?("each_") || super
247
+ end
248
+
249
+ # @!endgroup
250
+
251
+ # @!group Utilities
252
+
253
+ def inspect
254
+ "#<#{self.class.name} @ref=#{ref}>"
255
+ end
256
+
257
+ # @!endgroup
258
+
259
+ # @!group Child type constraints
260
+
261
+ class_attribute :valid_child_types,
262
+ instance_predicate: false,
263
+ default: []
264
+
265
+ class_attribute :file_matcher,
266
+ instance_reader: false,
267
+ instance_writer: false,
268
+ instance_predicate: false,
269
+ default: lambda { false }
270
+
271
+ def permit_child_types(*args)
272
+ args.flatten!
273
+ self.valid_child_types = args.map { NodeType(_1) } unless args.first.nil?
274
+ end
275
+
276
+ class << self
277
+ def permit_child_types(*args)
278
+ args.flatten!
279
+ self.valid_child_types = args.map { NodeType(_1) } unless args.first.nil?
280
+ end
281
+
282
+ def type
283
+ NodeType.new(self)
284
+ end
285
+
286
+ def match(&block)
287
+ self.file_matcher = block
288
+ end
289
+
290
+ def matches?(file)
291
+ file_matcher.call(file)
292
+ end
293
+ end
294
+
295
+ permit_child_types Node
296
+ end
297
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class AnonNode < Node
5
+ include Locatable
6
+
7
+ match { true } # fallback entity node type
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class AssetNode < Node
5
+ include Locatable
6
+
7
+ MIME_TYPES = %w[text/css text/javascript]
8
+
9
+ match do |file|
10
+ return if file.directory?
11
+
12
+ file.mime_type.in?(MIME_TYPES) || file.mime_type.start_with?("image/")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class DirectoryNode < Node
5
+ include Locatable
6
+
7
+ permit_child_types FileNode, DirectoryNode
8
+
9
+ def name
10
+ file.basename
11
+ end
12
+
13
+ alias_method :label, :name
14
+
15
+ delegate :file?, :directory?, :ext, :ext?, :dirname, :basename, :path_segments, :mime_type, :to_pathname, :contents, to: :file
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class DocumentNode < Node
5
+ include Locatable
6
+
7
+ match do |file|
8
+ file.ext?(".md", ".md.erb")
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class FileNode < Node
5
+ include Locatable
6
+
7
+ def name
8
+ file.basename
9
+ end
10
+
11
+ alias_method :label, :name
12
+
13
+ delegate :file?, :directory?, :ext, :ext?, :dirname, :basename, :path_segments, :mime_type, :to_pathname, :contents, to: :file
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class FolderNode < Node
5
+ include Locatable
6
+
7
+ match do |file|
8
+ file.directory?
9
+ end
10
+
11
+ permit_child_types [FolderNode, SpecNode, DocumentNode, AssetNode, AnonNode]
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ module Booklet
2
+ class ProseNode < Node
3
+ prop :snippet, TextSnippet, :positional, reader: :public, writer: :public do |value|
4
+ TextSnippet.new(value)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ module Booklet
2
+ class ScenarioNode < Node
3
+ prop :notes, _Nilable(TextSnippet), reader: :public, writer: :public
4
+ prop :source, CodeSnippet, reader: :public, writer: :public
5
+ prop :parameters, Array, reader: :public, writer: :public, default: [].freeze
6
+
7
+ alias_method :name, :ref
8
+
9
+ def label
10
+ name.titleize
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class SpecNode < Node
5
+ include Locatable
6
+
7
+ prop :notes, _Nilable(TextSnippet), reader: :public, writer: :public
8
+
9
+ permit_child_types :prose, :scenario
10
+
11
+ match do |file|
12
+ file.ext?(".rb") && file.name.end_with?("_preview")
13
+ end
14
+
15
+ def scenarios
16
+ filter(&:scenario?)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class Object < Literal::Object
5
+ include ActiveSupport::Callbacks
6
+ include Values
7
+ include Helpers
8
+
9
+ define_callbacks :initialize
10
+
11
+ def after_initialize
12
+ run_callbacks :initialize
13
+ end
14
+
15
+ class << self
16
+ include Values
17
+
18
+ def after_initialize(&)
19
+ set_callback :initialize, :after, &
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,31 @@
1
+ require "active_support/ordered_options"
2
+
3
+ module Booklet
4
+ class Options < ActiveSupport::InheritableOptions
5
+ def to_h
6
+ transform_values(&self.class.transformer)
7
+ end
8
+
9
+ class << self
10
+ def new(options)
11
+ super(options.transform_values(&transformer))
12
+ end
13
+
14
+ protected def convert_value(value, wrap = true)
15
+ if !wrap && value.respond_to?(:to_h)
16
+ value.to_h
17
+ elsif wrap && value.respond_to?(:to_h)
18
+ new(value.to_h)
19
+ elsif value.is_a?(Array)
20
+ value.map { convert_value(_1, wrap) }
21
+ else
22
+ value
23
+ end
24
+ end
25
+
26
+ protected def transformer
27
+ method(:convert_value)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class Value < Literal::Data
5
+ include Comparable
6
+ include Values
7
+ include Helpers
8
+
9
+ def ==(other)
10
+ return nil if !other.is_a?(self.class)
11
+ value == other.value
12
+ end
13
+
14
+ alias_method :eql?, :==
15
+ end
16
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class CodeSnippet < Value
5
+ prop :raw, String, :positional, reader: :protected
6
+
7
+ prop :lang, Symbol, reader: :public, default: :plain do |value|
8
+ value.to_s.underscore.to_sym unless value.nil?
9
+ end
10
+
11
+ prop :location, _Nilable(SourceLocation), reader: :public do |value|
12
+ if value.is_a?(Array)
13
+ SourceLocation(*value)
14
+ elsif value.is_a?(String) || value.is_a?(Pathname)
15
+ SourceLocation(value)
16
+ end
17
+ end
18
+
19
+ def value
20
+ strip_indent(raw)
21
+ end
22
+
23
+ alias_method :to_s, :value
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class MethodSnippet < CodeSnippet
5
+ prop :name, String, reader: :public
6
+
7
+ def body
8
+ extract_method_body(raw)
9
+ end
10
+
11
+ alias_method :value, :body
12
+ alias_method :to_s, :body
13
+
14
+ protected def extract_method_body(source)
15
+ source = strip_indent(source)
16
+ output = source.sub(/^def \w+\s?(\([^)]+\))?/m, "").split("\n")[0..-2].join("\n")
17
+ strip_whitespace(output)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class NodeType < Value
5
+ prop :type, _Class(Booklet::Node), :positional, reader: :public do |value|
6
+ if value.is_a?(String) || value.is_a?(Symbol)
7
+ value = "#{value}_node" unless value.downcase == "node"
8
+ "Booklet::#{value.classify}".constantize
9
+ else
10
+ value
11
+ end
12
+ end
13
+
14
+ def name
15
+ @type.name.to_s.demodulize.underscore.delete_suffix("_node")
16
+ end
17
+
18
+ alias_method :value, :type
19
+ alias_method :to_s, :name
20
+
21
+ delegate :to_sym, to: :name
22
+ delegate :pluralize, to: :name
23
+
24
+ def enquirer
25
+ ActiveSupport::StringInquirer.new(name)
26
+ end
27
+
28
+ # def entity?
29
+ # @type < EntityNode
30
+ # end
31
+
32
+ def file?
33
+ @type < FileNode
34
+ end
35
+
36
+ def method_missing(name, ...)
37
+ name.end_with?("?") ? enquirer.public_send(name) : super
38
+ end
39
+
40
+ def respond_to_missing?(name, ...)
41
+ name.end_with?("?") || super
42
+ end
43
+
44
+ def ==(other)
45
+ case other
46
+ when NodeType
47
+ type == other.type
48
+ when Class
49
+ type == other
50
+ else
51
+ name == other.to_s
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class ParserResult < Value
5
+ prop :path, Pathname
6
+ prop :files, DirectoryNode
7
+ prop :entities, FolderNode
8
+ prop :warnings, Array, default: [].freeze
9
+ prop :errors, Array, default: [].freeze
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class SourceLocation < Value
5
+ prop :path, Pathname, :positional, reader: :public do |value|
6
+ Pathname(value.to_s) unless value.nil?
7
+ end
8
+
9
+ prop :line, _Nilable(Integer), :positional, reader: :public
10
+
11
+ def value
12
+ [path.to_s, line].join(":")
13
+ end
14
+
15
+ alias_method :to_s, :value
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class TextSnippet < Value
5
+ include Values
6
+
7
+ prop :raw, String, :positional, reader: :protected
8
+
9
+ prop :format, Symbol, reader: :public, default: :markdown do |value|
10
+ value.to_s.underscore.to_sym unless value.nil?
11
+ end
12
+
13
+ prop :options, Hash, :**, reader: :protected
14
+
15
+ def value
16
+ strip_whitespace(raw)
17
+ end
18
+
19
+ alias_method :to_s, :value
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ module Values
5
+ def NodeRef(...)
6
+ NodeRef.new(...)
7
+ end
8
+
9
+ def NodeName(...)
10
+ NodeName.new(...)
11
+ end
12
+
13
+ def NodeType(...)
14
+ NodeType.new(...)
15
+ end
16
+
17
+ def Snippet(...)
18
+ Snippet.new(...)
19
+ end
20
+
21
+ def MethodSnippet(...)
22
+ MethodSnippet.new(...)
23
+ end
24
+
25
+ def SourceLocation(...)
26
+ SourceLocation.new(...)
27
+ end
28
+
29
+ extend self
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ VERSION = "0.0.1"
5
+ end