docscribe 1.2.1 → 1.3.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.
@@ -0,0 +1,309 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ module Docscribe
6
+ module CLI
7
+ # Generator for TagPlugin and CollectorPlugin boilerplate.
8
+ #
9
+ # Usage:
10
+ # docscribe generate tag MyPlugin
11
+ # docscribe generate collector MyPlugin
12
+ # docscribe generate tag MyPlugin --output lib/docscribe_plugins
13
+ # docscribe generate tag MyPlugin --stdout
14
+ module Generate
15
+ PLUGIN_TYPES = %w[tag collector].freeze
16
+
17
+ class << self
18
+ # Run the `generate` subcommand.
19
+ #
20
+ # @param [Array<String>] argv
21
+ # @raise [OptionParser::InvalidOption]
22
+ # @return [Integer] exit code
23
+ def run(argv)
24
+ opts = {
25
+ output: nil,
26
+ stdout: false
27
+ }
28
+
29
+ parser = OptionParser.new do |o|
30
+ o.banner = <<~TEXT
31
+ Usage: docscribe generate <type> <PluginName> [options]
32
+
33
+ Types:
34
+ tag Generate a TagPlugin skeleton
35
+ collector Generate a CollectorPlugin skeleton
36
+
37
+ Options:
38
+ TEXT
39
+
40
+ o.on('--output DIR', 'Directory to write the plugin file (default: .)') { |v| opts[:output] = v }
41
+ o.on('--stdout', 'Print the generated plugin to STDOUT instead of writing a file') { opts[:stdout] = true }
42
+ o.on('-h', '--help', 'Show this help') do
43
+ puts o
44
+ return 0
45
+ end
46
+ end
47
+
48
+ begin
49
+ parser.parse!(argv)
50
+ rescue OptionParser::InvalidOption => e
51
+ warn e.message
52
+ warn parser
53
+ return 1
54
+ end
55
+
56
+ plugin_type = argv.shift
57
+ class_name = argv.shift
58
+
59
+ unless plugin_type && class_name
60
+ warn 'Error: both <type> and <PluginName> are required.'
61
+ warn parser
62
+ return 1
63
+ end
64
+
65
+ unless PLUGIN_TYPES.include?(plugin_type)
66
+ warn "Error: unknown type #{plugin_type.inspect}. Must be one of: #{PLUGIN_TYPES.join(', ')}."
67
+ return 1
68
+ end
69
+
70
+ unless valid_constant?(class_name)
71
+ warn "Error: #{class_name.inspect} is not a valid Ruby constant name."
72
+ return 1
73
+ end
74
+
75
+ content = render(plugin_type, class_name)
76
+
77
+ if opts[:stdout]
78
+ puts content
79
+ return 0
80
+ end
81
+
82
+ write_plugin(content, plugin_type: plugin_type, class_name: class_name, output_dir: opts[:output] || '.')
83
+ end
84
+
85
+ private
86
+
87
+ # Check whether a string is a valid Ruby constant name.
88
+ #
89
+ # @private
90
+ # @param [String] str
91
+ # @return [Boolean]
92
+ def valid_constant?(str)
93
+ !!(str =~ /\A[A-Z][A-Za-z0-9]*(?:::[A-Z][A-Za-z0-9]*)*\z/)
94
+ end
95
+
96
+ # Render plugin boilerplate for the given type and class name.
97
+ #
98
+ # @private
99
+ # @param [String] plugin_type 'tag' or 'collector'
100
+ # @param [String] class_name CamelCase plugin class name
101
+ # @return [String]
102
+ def render(plugin_type, class_name)
103
+ case plugin_type
104
+ when 'tag' then tag_template(class_name)
105
+ when 'collector' then collector_template(class_name)
106
+ end
107
+ end
108
+
109
+ # Write the generated content to a file.
110
+ #
111
+ # @private
112
+ # @param [String] content
113
+ # @param [String] plugin_type
114
+ # @param [String] class_name
115
+ # @param [String] output_dir
116
+ # @return [Integer] exit code
117
+ def write_plugin(content, plugin_type:, class_name:, output_dir:)
118
+ filename = "#{underscore(class_name)}.rb"
119
+ path = File.join(output_dir, filename)
120
+
121
+ if File.exist?(path)
122
+ warn "Error: #{path} already exists. Remove it first or use --stdout."
123
+ return 1
124
+ end
125
+
126
+ require 'fileutils'
127
+ FileUtils.mkdir_p(output_dir)
128
+ File.write(path, content)
129
+ puts "Created: #{path}"
130
+ puts
131
+ puts next_steps(plugin_type, path)
132
+ 0
133
+ end
134
+
135
+ # Print registration instructions after file creation.
136
+ #
137
+ # @private
138
+ # @param [String] plugin_type
139
+ # @param [String] path
140
+ # @return [String]
141
+ def next_steps(plugin_type, path)
142
+ base_name = File.basename(path, '.rb').split('_').map(&:capitalize).join
143
+
144
+ implement_hint = case plugin_type
145
+ when 'tag'
146
+ 'Implement the #call method to return Array<Docscribe::Plugin::Tag>.'
147
+ when 'collector'
148
+ 'Implement the #collect method to return Array<{anchor_node:, doc:}>.'
149
+ end
150
+
151
+ <<~TEXT
152
+ Next steps:
153
+ 1. Open #{path} and implement the plugin logic.
154
+ #{implement_hint}
155
+
156
+ 2. Register the plugin in your docscribe_plugins.rb:
157
+
158
+ require_relative '#{path.delete_suffix('.rb')}'
159
+ Docscribe::Plugin::Registry.register(#{base_name}.new)
160
+
161
+ 3. Add the file to docscribe.yml:
162
+
163
+ plugins:
164
+ require:
165
+ - ./docscribe_plugins
166
+ TEXT
167
+ end
168
+
169
+ # Template for a TagPlugin.
170
+ #
171
+ # @private
172
+ # @param [String] class_name
173
+ # @return [String]
174
+ def tag_template(class_name)
175
+ <<~RUBY
176
+ # frozen_string_literal: true
177
+
178
+ require 'docscribe/plugin'
179
+
180
+ # #{class_name} — a Docscribe TagPlugin.
181
+ #
182
+ # TagPlugins hook into already-collected method insertions and append
183
+ # additional YARD tags to the generated doc block.
184
+ #
185
+ # The +#call+ method is invoked once per documented method. Return an
186
+ # empty array if this plugin has nothing to add for a particular method.
187
+ #
188
+ # @example Registration
189
+ # Docscribe::Plugin::Registry.register(#{class_name}.new)
190
+ class #{class_name} < Docscribe::Plugin::Base::TagPlugin
191
+ # Generate additional YARD tags for a documented method.
192
+ #
193
+ # Available context attributes:
194
+ # context.node # Parser::AST::Node — the :def or :defs node
195
+ # context.container # String — e.g. "MyModule::MyClass"
196
+ # context.scope # Symbol — :instance or :class
197
+ # context.visibility # Symbol — :public, :protected, or :private
198
+ # context.method_name # Symbol — method name
199
+ # context.inferred_params # Hash — { "name" => "InferredType" }
200
+ # context.inferred_return # String — inferred return type
201
+ # context.source # String — raw method source text
202
+ #
203
+ # @param [Docscribe::Plugin::Context] context method context snapshot
204
+ # @return [Array<Docscribe::Plugin::Tag>]
205
+ def call(context)
206
+ # TODO: implement plugin logic
207
+ #
208
+ # Examples:
209
+ #
210
+ # Simple text tag:
211
+ # Docscribe::Plugin::Tag.new(name: 'since', text: '1.0.0')
212
+ # # => # @since 1.0.0
213
+ #
214
+ # Tag with types:
215
+ # Docscribe::Plugin::Tag.new(name: 'raise', types: ['ArgumentError'], text: 'if invalid')
216
+ # # => # @raise [ArgumentError] if invalid
217
+ #
218
+ # Conditional tag:
219
+ # return [] unless context.visibility == :public
220
+ # [Docscribe::Plugin::Tag.new(name: 'api', text: 'public')]
221
+ []
222
+ end
223
+ end
224
+ RUBY
225
+ end
226
+
227
+ # Template for a CollectorPlugin.
228
+ #
229
+ # @private
230
+ # @param [String] class_name
231
+ # @return [String]
232
+ def collector_template(class_name)
233
+ <<~RUBY
234
+ # frozen_string_literal: true
235
+
236
+ require 'docscribe/plugin'
237
+ require 'docscribe/infer/ast_walk'
238
+
239
+ # #{class_name} — a Docscribe CollectorPlugin.
240
+ #
241
+ # CollectorPlugins receive the raw AST and source buffer for each file.
242
+ # They walk the tree independently and return insertion targets that
243
+ # Docscribe will document according to the selected strategy.
244
+ #
245
+ # Idempotency is handled automatically:
246
+ # :safe — skips insertion if a comment already exists above anchor_node
247
+ # :aggressive — removes the existing comment and inserts a fresh block
248
+ #
249
+ # Use this plugin type for non-standard constructs that Docscribe's
250
+ # built-in Collector does not recognize (DSL macros, define_method, etc.).
251
+ # For ordinary +def+ methods use TagPlugin instead.
252
+ #
253
+ # @example Registration
254
+ # Docscribe::Plugin::Registry.register(#{class_name}.new)
255
+ class #{class_name} < Docscribe::Plugin::Base::CollectorPlugin
256
+ # Walk the AST and return documentation insertion targets.
257
+ #
258
+ # Each result must be a Hash with:
259
+ # :anchor_node — Parser::AST::Node above which to insert the doc block
260
+ # :doc — String with the complete doc block (newlines included)
261
+ #
262
+ # Indentation is applied automatically from anchor_node — do not
263
+ # prefix lines manually.
264
+ #
265
+ # @param [Parser::AST::Node] ast root AST node of the file
266
+ # @param [Parser::Source::Buffer] buffer source buffer
267
+ # @return [Array<Hash>]
268
+ def collect(ast, buffer)
269
+ results = []
270
+
271
+ Docscribe::Infer::ASTWalk.walk(ast) do |node|
272
+ # TODO: replace with your target node detection
273
+ #
274
+ # Example — match bare send calls like `my_dsl_macro :name`:
275
+ #
276
+ # next unless node.type == :send
277
+ # recv, meth, name_node, *_rest = *node
278
+ # next unless recv.nil? && meth == :my_dsl_macro
279
+ # next unless name_node&.type == :sym
280
+ #
281
+ # macro_name = name_node.children.first
282
+ #
283
+ # results << {
284
+ # anchor_node: node,
285
+ # doc: "# \#{macro_name} — generated doc\\n# @return [void]\\n"
286
+ # }
287
+ end
288
+
289
+ results
290
+ end
291
+ end
292
+ RUBY
293
+ end
294
+
295
+ # Convert CamelCase to snake_case for file naming.
296
+ #
297
+ # @private
298
+ # @param [String] str
299
+ # @return [String]
300
+ def underscore(str)
301
+ str
302
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
303
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
304
+ .downcase
305
+ end
306
+ end
307
+ end
308
+ end
309
+ end
@@ -19,7 +19,8 @@ module Docscribe
19
19
  rbs: false,
20
20
  sig_dirs: [],
21
21
  sorbet: false,
22
- rbi_dirs: []
22
+ rbi_dirs: [],
23
+ rbs_collection: false
23
24
  }.freeze
24
25
 
25
26
  module_function
@@ -65,6 +66,7 @@ module Docscribe
65
66
  --sig-dir DIR Add an RBS signature directory (repeatable). Implies `--rbs`.
66
67
  --sorbet Use Sorbet signatures from inline sigs / RBI files when available
67
68
  --rbi-dir DIR Add a Sorbet RBI directory (repeatable). Implies --sorbet.
69
+ --rbs-collection Auto-discover RBS collection from rbs_collection.lock.yaml. Implies --rbs.
68
70
 
69
71
  Filtering:
70
72
  --include PATTERN Include PATTERN (method id or file path; glob or /regex/)
@@ -115,6 +117,11 @@ module Docscribe
115
117
  options[:rbi_dirs] << v
116
118
  end
117
119
 
120
+ opts.on('--rbs-collection', 'Auto-discover RBS collection from rbs_collection.lock.yaml. Implies --rbs.') do
121
+ options[:rbs] = true
122
+ options[:rbs_collection] = true
123
+ end
124
+
118
125
  opts.on('--include PATTERN', 'Include PATTERN (method id or file path; glob or /regex/)') do |v|
119
126
  route_include_exclude(options, :include, v)
120
127
  end
@@ -34,6 +34,7 @@ module Docscribe
34
34
  def run(options:, argv:)
35
35
  conf = Docscribe::Config.load(options[:config])
36
36
  conf = Docscribe::CLI::ConfigBuilder.build(conf, options)
37
+ conf.load_plugins!
37
38
 
38
39
  return run_stdin(options: options, conf: conf) if options[:mode] == :stdin
39
40
 
@@ -61,6 +62,7 @@ module Docscribe
61
62
  code,
62
63
  strategy: options[:strategy],
63
64
  config: conf,
65
+ core_rbs_provider: conf.respond_to?(:core_rbs_provider) ? conf.core_rbs_provider : nil,
64
66
  file: '(stdin)'
65
67
  )
66
68
  puts result[:output]
@@ -132,10 +134,10 @@ module Docscribe
132
134
 
133
135
  private
134
136
 
135
- # Method documentation.
137
+ # Initialize the shared state hash used throughout a run.
136
138
  #
137
139
  # @private
138
- # @return [Hash]
140
+ # @return [Hash] initial state with counters and tracking arrays
139
141
  def initial_run_state
140
142
  {
141
143
  changed: false,
@@ -150,15 +152,15 @@ module Docscribe
150
152
  }
151
153
  end
152
154
 
153
- # Method documentation.
155
+ # Process a single file: read, rewrite, and dispatch to check/write handler.
154
156
  #
155
157
  # @private
156
- # @param [Object] path Param documentation.
157
- # @param [Hash] options Param documentation.
158
- # @param [Object] conf Param documentation.
159
- # @param [Object] pwd Param documentation.
160
- # @param [Object] state Param documentation.
161
- # @return [Object]
158
+ # @param [String] path file path
159
+ # @param [Hash] options CLI options
160
+ # @param [Docscribe::Config] conf configuration
161
+ # @param [Pathname] pwd current working directory
162
+ # @param [Hash] state shared processing state
163
+ # @return [void]
162
164
  def process_one_file(path, options:, conf:, pwd:, state:)
163
165
  display_path = display_path_for(path, pwd: pwd)
164
166
 
@@ -217,16 +219,15 @@ module Docscribe
217
219
  File.basename(path.to_s)
218
220
  end
219
221
 
220
- # Method documentation.
222
+ # Read the source file and handle read errors.
221
223
  #
222
224
  # @private
223
- # @param [Object] path Param documentation.
224
- # @param [Object] display_path Param documentation.
225
- # @param [Hash] options Param documentation.
226
- # @param [Object] state Param documentation.
225
+ # @param [String] path file path to read
226
+ # @param [String] display_path path shown in CLI output
227
+ # @param [Hash] options CLI options
228
+ # @param [Hash] state shared processing state
227
229
  # @raise [StandardError]
228
- # @return [Object]
229
- # @return [nil] if StandardError
230
+ # @return [String, nil] file contents or nil on error
230
231
  def read_source_for_path(path, display_path:, options:, state:)
231
232
  File.read(path)
232
233
  rescue StandardError => e
@@ -237,23 +238,24 @@ module Docscribe
237
238
  nil
238
239
  end
239
240
 
240
- # Method documentation.
241
+ # Rewrite the source file using InlineRewriter and handle rewrite errors.
241
242
  #
242
243
  # @private
243
- # @param [Object] path Param documentation.
244
- # @param [Object] src Param documentation.
245
- # @param [Object] conf Param documentation.
246
- # @param [Object] display_path Param documentation.
247
- # @param [Hash] options Param documentation.
248
- # @param [Object] state Param documentation.
244
+ # @param [String] path file path
245
+ # @param [String] src source code
246
+ # @param [Docscribe::Config] conf configuration
247
+ # @param [String] display_path path shown in CLI output
248
+ # @param [Hash] options CLI options
249
+ # @param [Hash] state shared processing state
249
250
  # @raise [StandardError]
250
- # @return [Object]
251
- # @return [nil] if StandardError
251
+ # @return [Hash, nil] rewrite result or nil on error
252
252
  def rewrite_result_for_path(path, src:, conf:, display_path:, options:, state:)
253
+ core_rbs_provider = conf.respond_to?(:core_rbs_provider) ? conf.core_rbs_provider : nil
253
254
  Docscribe::InlineRewriter.rewrite_with_report(
254
255
  src,
255
256
  strategy: options[:strategy],
256
257
  config: conf,
258
+ core_rbs_provider: core_rbs_provider,
257
259
  file: path
258
260
  )
259
261
  rescue StandardError => e
@@ -264,17 +266,17 @@ module Docscribe
264
266
  nil
265
267
  end
266
268
 
267
- # Method documentation.
269
+ # Handle the result of an inspect (check) run.
268
270
  #
269
271
  # @private
270
- # @param [Object] path Param documentation.
271
- # @param [Object] src Param documentation.
272
- # @param [Object] out Param documentation.
273
- # @param [Object] file_changes Param documentation.
274
- # @param [Object] display_path Param documentation.
275
- # @param [Hash] options Param documentation.
276
- # @param [Object] state Param documentation.
277
- # @return [Object]
272
+ # @param [String] path file path
273
+ # @param [String] src original source code
274
+ # @param [String] out rewritten source code
275
+ # @param [Array<Hash>] file_changes structured change records
276
+ # @param [String] display_path path shown in CLI output
277
+ # @param [Hash] options CLI options
278
+ # @param [Hash] state shared processing state
279
+ # @return [void]
278
280
  def handle_check_result(path, src:, out:, file_changes:, display_path:, options:, state:)
279
281
  if out == src
280
282
  options[:verbose] ? puts("OK #{display_path}") : print('.')
@@ -299,19 +301,18 @@ module Docscribe
299
301
  state[:fail_changes][path] = file_changes
300
302
  end
301
303
 
302
- # Method documentation.
304
+ # Handle the result of an autocorrect (write) run.
303
305
  #
304
306
  # @private
305
- # @param [Object] path Param documentation.
306
- # @param [Object] src Param documentation.
307
- # @param [Object] out Param documentation.
308
- # @param [Object] file_changes Param documentation.
309
- # @param [Object] display_path Param documentation.
310
- # @param [Hash] options Param documentation.
311
- # @param [Object] state Param documentation.
307
+ # @param [String] path file path
308
+ # @param [String] src original source code
309
+ # @param [String] out rewritten source code
310
+ # @param [Array<Hash>] file_changes structured change records
311
+ # @param [String] display_path path shown in CLI output
312
+ # @param [Hash] options CLI options
313
+ # @param [Hash] state shared processing state
312
314
  # @raise [StandardError]
313
- # @return [Object]
314
- # @return [Object] if StandardError
315
+ # @return [void]
315
316
  def handle_write_result(path, src:, out:, file_changes:, display_path:, options:, state:)
316
317
  if out == src
317
318
  options[:verbose] ? puts("OK #{display_path}") : print('.')
@@ -339,12 +340,12 @@ module Docscribe
339
340
  options[:verbose] ? warn("ERR #{display_path}: #{state[:error_messages][path]}") : print('E')
340
341
  end
341
342
 
342
- # Method documentation.
343
+ # Print the check-mode summary (files OK / need updates / errors).
343
344
  #
344
345
  # @private
345
- # @param [Object] state Param documentation.
346
- # @param [Hash] options Param documentation.
347
- # @return [Object]
346
+ # @param [Hash] state shared processing state
347
+ # @param [Hash] options CLI options
348
+ # @return [void]
348
349
  def print_check_summary(state:, options:)
349
350
  puts
350
351
 
@@ -392,11 +393,11 @@ module Docscribe
392
393
  end
393
394
  end
394
395
 
395
- # Method documentation.
396
+ # Print the write-mode summary (files corrected, errors).
396
397
  #
397
398
  # @private
398
- # @param [Object] state Param documentation.
399
- # @return [Object]
399
+ # @param [Hash] state shared processing state
400
+ # @return [void]
400
401
  def print_write_summary(state:)
401
402
  puts
402
403
  puts "Docscribe: updated #{state[:corrected]} file(s)" if state[:corrected].positive?
data/lib/docscribe/cli.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'docscribe/cli/init'
4
+ require 'docscribe/cli/generate'
4
5
  require 'docscribe/cli/options'
5
6
  require 'docscribe/cli/run'
6
7
 
@@ -10,7 +11,8 @@ module Docscribe
10
11
  # Main CLI entry point.
11
12
  #
12
13
  # Dispatches:
13
- # - `docscribe init ...` to the config-template generator
14
+ # - `docscribe init ...` to the config-template generator
15
+ # - `docscribe generate ...` to the plugin skeleton generator
14
16
  # - all other commands to the main option parser and runner
15
17
  #
16
18
  # @param [Array<String>] argv raw command-line arguments
@@ -18,9 +20,13 @@ module Docscribe
18
20
  def run(argv)
19
21
  argv = argv.dup
20
22
 
21
- if argv.first == 'init'
23
+ case argv.first
24
+ when 'init'
22
25
  argv.shift
23
26
  return Docscribe::CLI::Init.run(argv)
27
+ when 'generate'
28
+ argv.shift
29
+ return Docscribe::CLI::Generate.run(argv)
24
30
  end
25
31
 
26
32
  options = Docscribe::CLI::Options.parse!(argv)
@@ -14,7 +14,9 @@ module Docscribe
14
14
  # - optional Sorbet integration
15
15
  DEFAULT = {
16
16
  'emit' => {
17
- 'header' => true,
17
+ 'header' => false,
18
+ 'include_default_message' => true,
19
+ 'include_param_documentation' => true,
18
20
  'param_tags' => true,
19
21
  'return_tag' => true,
20
22
  'visibility_tags' => true,
@@ -53,11 +55,12 @@ module Docscribe
53
55
  'exclude' => [],
54
56
  'files' => {
55
57
  'include' => [],
56
- 'exclude' => []
58
+ 'exclude' => ['spec']
57
59
  }
58
60
  },
59
61
  'rbs' => {
60
62
  'enabled' => false,
63
+ 'collection' => false,
61
64
  'sig_dirs' => ['sig'],
62
65
  'collapse_generics' => false
63
66
  },
@@ -65,6 +68,9 @@ module Docscribe
65
68
  'enabled' => false,
66
69
  'rbi_dirs' => ['sorbet/rbi', 'rbi'],
67
70
  'collapse_generics' => false
71
+ },
72
+ 'plugins' => {
73
+ 'require' => []
68
74
  }
69
75
  }.freeze
70
76
  end
@@ -16,7 +16,7 @@ module Docscribe
16
16
  exclude_patterns = normalize_file_patterns(files['exclude'])
17
17
 
18
18
  rel = begin
19
- Pathname.new(path).relative_path_from(Pathname.pwd).to_s
19
+ Pathname.new(path).expand_path.relative_path_from(Pathname.pwd).cleanpath.to_s
20
20
  rescue StandardError
21
21
  path
22
22
  end
@@ -121,7 +121,7 @@ module Docscribe
121
121
  patterns_to_try << pattern.gsub('/**/', '/') if pattern.include?('/**/')
122
122
 
123
123
  patterns_to_try.any? do |pat|
124
- File.fnmatch?(pat, path, File::FNM_EXTGLOB | File::FNM_PATHNAME)
124
+ File.fnmatch?(pat, path, File::FNM_EXTGLOB | File::FNM_DOTMATCH | File::FNM_PATHNAME)
125
125
  end
126
126
  end
127
127
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ class Config
5
+ # Load and register plugins declared under `plugins.require` in config.
6
+ #
7
+ # Each entry is expanded relative to the current working directory and
8
+ # passed to `require`. Registration is expected to happen inside the
9
+ # required file via {Docscribe::Plugin::Registry.register}.
10
+ #
11
+ # Loading failures are non-fatal: a warning is printed and the run
12
+ # continues without the plugin.
13
+ #
14
+ # @raise [LoadError]
15
+ # @return [void]
16
+ def load_plugins!
17
+ paths = Array(raw.dig('plugins', 'require')).compact
18
+ return if paths.empty?
19
+
20
+ require 'docscribe/plugin'
21
+
22
+ paths.each do |path|
23
+ require File.expand_path(path)
24
+ rescue LoadError => e
25
+ warn "Docscribe: could not load plugin #{path.inspect}: #{e.message}"
26
+ end
27
+ end
28
+ end
29
+ end