darkroom 0.0.3 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,23 +2,40 @@
2
2
 
3
3
  require('base64')
4
4
  require('digest')
5
+ require('set')
6
+
7
+ require_relative('darkroom')
8
+ require_relative('errors/asset_error')
9
+ require_relative('errors/asset_not_found_error')
10
+ require_relative('errors/circular_reference_error')
11
+ require_relative('errors/missing_library_error')
12
+ require_relative('errors/processing_error')
13
+ require_relative('errors/unrecognized_extension_error')
5
14
 
6
15
  class Darkroom
7
16
  ##
8
17
  # Represents an asset.
9
18
  #
10
19
  class Asset
11
- DEPENDENCY_JOINER = "\n"
12
20
  EXTENSION_REGEX = /(?=\.\w+)/.freeze
13
21
 
14
- OPEN_QUOTE = '(?<quote>[\'"])'
15
- BODY = '((?!\k<quote>).|(?<!\\\\)\\\\(\\\\\\\\)*\k<quote>)*'
16
- BODY_END = '(?<!\\\\)(\\\\\\\\)*'
17
- CLOSE_QUOTE = '\k<quote>'
22
+ IMPORT_JOINER = "\n"
23
+ DEFAULT_QUOTE = '\''
18
24
 
19
- IMPORT_PATH_REGEX = /#{OPEN_QUOTE}(?<path>#{BODY}#{BODY_END})#{CLOSE_QUOTE}/.freeze
25
+ QUOTED_PATH = /(?<quote>['"])(?<path>[^'"]*)\k<quote>/.freeze
26
+ REFERENCE_PATH =
27
+ %r{
28
+ (?<quote>['"]?)(?<quoted>
29
+ (?<path>[^#{DISALLOWED_PATH_CHARS}]+)
30
+ \?asset-(?<entity>path|content)(=(?<format>\w*))?
31
+ )\k<quote>
32
+ }x.freeze
20
33
 
21
- @@specs = {}
34
+ # First item of each set is used as default, so order is important.
35
+ REFERENCE_FORMATS = {
36
+ 'path' => Set.new(%w[versioned unversioned]),
37
+ 'content' => Set.new(%w[base64 utf8 displace]),
38
+ }.freeze
22
39
 
23
40
  attr_reader(:content, :error, :errors, :path, :path_unversioned, :path_versioned)
24
41
 
@@ -26,65 +43,45 @@ class Darkroom
26
43
  # Holds information about how to handle a particular asset type.
27
44
  #
28
45
  # * +content_type+ - HTTP MIME type string.
29
- # * +dependency_regex+ - Regex to match lines of the file against to find dependencies. Must contain a
30
- # named component called 'path' (e.g. +/^import (?<path>.*)/+).
31
- # * +compile+ - Proc to call that will produce the compiled version of the asset's content.
32
- # * +compile_lib+ - Name of a library to +require+ that is needed by the +compile+ proc.
33
- # * +minify+ - Proc to call that will produce the minified version of the asset's content.
34
- # * +minify_lib+ - Name of a library to +require+ that is needed by the +minify+ proc.
35
- #
36
- Spec = Struct.new(:content_type, :dependency_regex, :compile, :compile_lib, :minify, :minify_lib)
37
-
38
- ##
39
- # Defines an asset spec.
40
- #
41
- # * +extensions+ - File extensions to associate with this spec.
42
- # * +content_type+ - HTTP MIME type string.
43
- # * +other+ - Optional components of the spec (see Spec struct).
44
- #
45
- def self.add_spec(*extensions, content_type, **other)
46
- spec = Spec.new(
47
- content_type.freeze,
48
- other[:dependency_regex].freeze,
49
- other[:compile].freeze,
50
- other[:compile_lib].freeze,
51
- other[:minify].freeze,
52
- other[:minify_lib].freeze,
53
- ).freeze
54
-
55
- extensions.each do |extension|
56
- @@specs[extension] = spec
57
- end
58
-
59
- spec
60
- end
61
-
62
- ##
63
- # Returns the spec associated with a file extension.
64
- #
65
- # * +extension+ - File extension of the desired spec.
66
- #
67
- def self.spec(extension)
68
- @@specs[extension]
69
- end
70
-
71
- ##
72
- # Returns an array of file extensions for which specs exist.
46
+ # * +import_regex+ - Regex to find import statements. Must contain a named component called 'path'
47
+ # (e.g. +/^import (?<path>.*)/+).
48
+ # * +reference_regex+ - Regex to find references to other assets. Must contain three named components:
49
+ # * +path+ - Path of the asset being referenced.
50
+ # * +entity+ - Desired entity (path or content).
51
+ # * +format+ - Format to use (see REFERENCE_FORMATS).
52
+ # * +validate_reference+ - Lambda to call to validate a reference. Should return nil if there are no
53
+ # errors and a string error message if validation fails. Three arguments are passed when called:
54
+ # * +asset+ - Asset object of the asset being referenced.
55
+ # * +match+ - MatchData object from the match against +reference_regex+.
56
+ # * +format+ - Format of the reference (see REFERENCE_FORMATS).
57
+ # * +reference_content+ - Lambda to call to get the content for a reference. Should return nil if the
58
+ # default behavior is desired or a string for custom content. Three arguments are passed when called:
59
+ # * +asset+ - Asset object of the asset being referenced.
60
+ # * +match+ - MatchData object from the match against +reference_regex+.
61
+ # * +format+ - Format of the reference (see REFERENCE_FORMATS).
62
+ # * +compile_lib+ - Name of a library to +require+ that is needed by the +compile+ lambda.
63
+ # * +compile+ - Lambda to call that will return the compiled version of the asset's content. Two
64
+ # arguments are passed when called:
65
+ # * +path+ - Path of the asset being compiled.
66
+ # * +content+ - Content to compile.
67
+ # * +minify_lib+ - Name of a library to +require+ that is needed by the +minify+ lambda.
68
+ # * +minify+ - Lambda to call that will return the minified version of the asset's content. One argument
69
+ # is passed when called:
70
+ # * +content+ - Content to minify.
73
71
  #
74
- def self.extensions
75
- @@specs.keys
76
- end
72
+ Delegate = Struct.new(:content_type, :import_regex, :reference_regex, :validate_reference,
73
+ :reference_content, :compile_lib, :compile, :minify_lib, :minify, keyword_init: true)
77
74
 
78
75
  ##
79
76
  # Creates a new instance.
80
77
  #
81
- # * +file+ - The path to the file on disk.
82
- # * +path+ - The path this asset will be referenced by (e.g. /js/app.js).
78
+ # * +file+ - Path of file on disk.
79
+ # * +path+ - Path this asset will be referenced by (e.g. /js/app.js).
83
80
  # * +darkroom+ - Darkroom instance that the asset is a member of.
84
81
  # * +prefix+ - Prefix to apply to unversioned and versioned paths.
85
82
  # * +minify+ - Boolean specifying whether or not the asset should be minified when processed.
86
- # * +internal+ - Boolean indicating whether or not the asset is only accessible internally (i.e. as a
87
- # dependency).
83
+ # * +internal+ - Boolean indicating whether or not the asset is only accessible internally (i.e. as an
84
+ # import or reference).
88
85
  #
89
86
  def initialize(path, file, darkroom, prefix: nil, minify: false, internal: false)
90
87
  @path = path
@@ -96,7 +93,7 @@ class Darkroom
96
93
 
97
94
  @path_unversioned = "#{@prefix}#{@path}"
98
95
  @extension = File.extname(@path).downcase
99
- @spec = self.class.spec(@extension) or raise(SpecNotDefinedError.new(@extension, @file))
96
+ @delegate = Darkroom.delegate(@extension) or raise(UnrecognizedExtensionError.new(@path))
100
97
 
101
98
  require_libs
102
99
  clear
@@ -104,9 +101,9 @@ class Darkroom
104
101
 
105
102
  ##
106
103
  # Processes the asset if modified (see #modified? for how modification is determined). File is read from
107
- # disk, any dependencies are merged into its content (if spec for the asset type allows for it), the
108
- # content is compiled (if the asset type requires compilation), and minified (if specified for this
109
- # Asset). Returns true if asset was modified since it was last processed and false otherwise.
104
+ # disk, references are substituted (if supported), content is compiled (if required), imports are
105
+ # prefixed to its content (if supported), and content is minified (if supported and enabled). Returns
106
+ # true if asset was modified since it was last processed and false otherwise.
110
107
  #
111
108
  def process
112
109
  @process_key == @darkroom.process_key ? (return @processed) : (@process_key = @darkroom.process_key)
@@ -114,6 +111,9 @@ class Darkroom
114
111
 
115
112
  clear
116
113
  read
114
+ build_imports
115
+ build_references
116
+ process_dependencies
117
117
  compile
118
118
  minify
119
119
 
@@ -131,7 +131,32 @@ class Darkroom
131
131
  # Returns the HTTP MIME type string.
132
132
  #
133
133
  def content_type
134
- @spec.content_type
134
+ @delegate.content_type
135
+ end
136
+
137
+ ##
138
+ # Returns boolean indicating whether or not the asset is binary.
139
+ #
140
+ def binary?
141
+ return @is_binary if defined?(@is_binary)
142
+
143
+ type, subtype = content_type.split('/')
144
+
145
+ @is_binary = type != 'text' && !subtype.include?('json') && !subtype.include?('xml')
146
+ end
147
+
148
+ ##
149
+ # Returns boolean indicating whether or not the asset is a font.
150
+ #
151
+ def font?
152
+ defined?(@is_font) ? @is_font : (@is_font = content_type.start_with?('font/'))
153
+ end
154
+
155
+ ##
156
+ # Returns boolean indicating whether or not the asset is an image.
157
+ #
158
+ def image?
159
+ defined?(@is_image) ? @is_image : (@is_image = content_type.start_with?('image/'))
135
160
  end
136
161
 
137
162
  ##
@@ -150,8 +175,8 @@ class Darkroom
150
175
  ##
151
176
  # Returns subresource integrity string.
152
177
  #
153
- # * +algorithm+ - The hash algorithm to use to generate the integrity string (one of sha256, sha384, or
154
- # sha512).
178
+ # * +algorithm+ - Hash algorithm to use to generate the integrity string (one of :sha256, :sha384, or
179
+ # :sha512).
155
180
  #
156
181
  def integrity(algorithm = :sha384)
157
182
  @integrity[algorithm] ||= "#{algorithm}-#{Base64.strict_encode64(
@@ -219,23 +244,31 @@ class Darkroom
219
244
  ##
220
245
  # Returns all dependencies (including dependencies of dependencies).
221
246
  #
222
- # * +ancestors+ - Ancestor chain followed to get to this asset as a dependency.
247
+ # * +ignore+ - Assets already accounted for as dependency tree is walked (to prevent infinite loops when
248
+ # circular chains are encountered).
223
249
  #
224
- def dependencies(ancestors = Set.new)
225
- @dependencies ||= @own_dependencies.inject([]) do |dependencies, own_dependency|
226
- next dependencies if ancestors.include?(self)
250
+ def dependencies(ignore = nil)
251
+ return @dependencies if @dependencies
227
252
 
228
- ancestors << self
229
- own_dependency.process
253
+ dependencies = accumulate(:dependencies, ignore)
254
+ @dependencies = dependencies unless ignore
230
255
 
231
- dependencies |= own_dependency.dependencies(ancestors)
232
- dependencies |= [own_dependency]
256
+ dependencies
257
+ end
233
258
 
234
- dependencies.delete(self)
235
- ancestors.delete(self)
259
+ ##
260
+ # Returns all imports (including imports of imports).
261
+ #
262
+ # * +ignore+ - Assets already accounted for as import tree is walked (to prevent infinite loops when
263
+ # circular chains are encountered).
264
+ #
265
+ def imports(ignore = nil)
266
+ return @imports if @imports
236
267
 
237
- dependencies
238
- end
268
+ imports = accumulate(:imports, ignore)
269
+ @imports = imports unless ignore
270
+
271
+ imports
239
272
  end
240
273
 
241
274
  ##
@@ -252,48 +285,125 @@ class Darkroom
252
285
  #
253
286
  def clear
254
287
  @dependencies = nil
288
+ @imports = nil
255
289
  @error = nil
256
290
  @fingerprint = nil
257
291
  @path_versioned = nil
258
292
 
259
- (@errors ||= []).clear
260
293
  (@own_dependencies ||= []).clear
294
+ (@own_imports ||= []).clear
295
+ (@dependency_matches ||= []).clear
296
+ (@errors ||= []).clear
261
297
  (@content ||= +'').clear
262
298
  (@own_content ||= +'').clear
263
299
  (@integrity ||= {}).clear
264
300
  end
265
301
 
266
302
  ##
267
- # Reads the asset file, building dependency array if dependencies are supported for the asset's type.
303
+ # Reads the asset file into memory.
268
304
  #
269
305
  def read
270
- unless @spec.dependency_regex
271
- @own_content = File.read(@file)
272
- return
273
- end
306
+ @own_content = File.read(@file)
307
+ end
274
308
 
275
- File.new(@file).each.with_index do |line, line_num|
276
- if (path = line[@spec.dependency_regex, :path])
277
- if (dependency = @darkroom.manifest(path))
278
- @own_dependencies << dependency
309
+ ##
310
+ # Builds reference info.
311
+ #
312
+ def build_references
313
+ return unless @delegate.reference_regex
314
+
315
+ @own_content.scan(@delegate.reference_regex) do
316
+ match = Regexp.last_match
317
+ path = match[:path]
318
+ format = match[:format]
319
+ format = REFERENCE_FORMATS[match[:entity]].first if format.nil? || format == ''
320
+
321
+ if (asset = @darkroom.manifest(path))
322
+ if !REFERENCE_FORMATS[match[:entity]].include?(format)
323
+ @errors << AssetError.new("Invalid reference format '#{format}' (must be one of "\
324
+ "'#{REFERENCE_FORMATS[match[:entity]].join("', '")}')", match[0], @path, line_num(match))
325
+ elsif match[:entity] == 'content' && format != 'base64' && asset.binary?
326
+ @errors << AssetError.new('Base64 encoding is required for binary assets', match[0], @path,
327
+ line_num(match))
328
+ elsif (error = @delegate.validate_reference&.(asset, match, format))
329
+ @errors << AssetError.new(error, match[0], @path, line_num(match))
279
330
  else
280
- @errors << AssetNotFoundError.new(path, @path, line_num + 1)
331
+ @own_dependencies << asset
332
+ @dependency_matches << [:reference, asset, match, format]
281
333
  end
282
334
  else
283
- @own_content << line
335
+ @errors << not_found_error(path, match)
336
+ end
337
+ end
338
+ end
339
+
340
+ ##
341
+ # Builds import info.
342
+ #
343
+ def build_imports
344
+ return unless @delegate.import_regex
345
+
346
+ @own_content.scan(@delegate.import_regex) do
347
+ match = Regexp.last_match
348
+ path = match[:path]
349
+
350
+ if (asset = @darkroom.manifest(path))
351
+ @own_dependencies << asset
352
+ @own_imports << asset
353
+ @dependency_matches << [:import, asset, match]
354
+ else
355
+ @errors << not_found_error(path, match)
356
+ end
357
+ end
358
+ end
359
+
360
+ ##
361
+ # Processes imports and references.
362
+ #
363
+ def process_dependencies
364
+ @dependency_matches.sort_by! { |_, __, match| -match.begin(0) }.each do |kind, asset, match, format|
365
+ if kind == :import
366
+ @own_content[match.begin(0)...match.end(0)] = ''
367
+ elsif asset.dependencies.include?(self)
368
+ @errors << CircularReferenceError.new(match[0], @path, line_num(match))
369
+ else
370
+ value, start, finish = @delegate.reference_content&.(asset, match, format)
371
+ min_start, max_finish = match.offset(0)
372
+ start ||= format == 'displace' ? min_start : match.begin(:quoted)
373
+ finish ||= format == 'displace' ? max_finish : match.end(:quoted)
374
+ start = [[start, min_start].max, max_finish].min
375
+ finish = [[finish, max_finish].min, min_start].max
376
+
377
+ @own_content[start...finish] =
378
+ case "#{match[:entity]}-#{format}"
379
+ when 'path-versioned'
380
+ value || asset.path_versioned
381
+ when 'path-unversioned'
382
+ value || asset.path_unversioned
383
+ when 'content-base64'
384
+ quote = DEFAULT_QUOTE if match[:quote] == ''
385
+ data = Base64.strict_encode64(value || asset.content)
386
+ "#{quote}data:#{asset.content_type};base64,#{data}#{quote}"
387
+ when 'content-utf8'
388
+ quote = DEFAULT_QUOTE if match[:quote] == ''
389
+ "#{quote}data:#{asset.content_type};utf8,#{value || asset.content}#{quote}"
390
+ when 'content-displace'
391
+ value || asset.content
392
+ end
284
393
  end
285
394
  end
286
395
 
287
- @content << dependencies.map { |d| d.own_content }.join(DEPENDENCY_JOINER)
396
+ @content << imports.map { |d| d.own_content }.join(IMPORT_JOINER)
288
397
  end
289
398
 
290
399
  ##
291
- # Compiles the asset if compilation is supported for the asset's type.
400
+ # Compiles the asset if compilation is supported for the asset's type and appends the asset's own
401
+ # content to the overall content string.
292
402
  #
293
403
  def compile
294
- if @spec.compile
404
+ if @delegate.compile
295
405
  begin
296
- @own_content = @spec.compile.call(@path, @own_content)
406
+ @own_content = @delegate.compile.(@path, @own_content)
297
407
  rescue => e
298
408
  @errors << e
299
409
  end
@@ -307,9 +417,9 @@ class Darkroom
307
417
  # (i.e. it's not already minified), and the asset is not marked as internal-only.
308
418
  #
309
419
  def minify
310
- if @spec.minify && @minify && !@internal
420
+ if @delegate.minify && @minify && !@internal
311
421
  begin
312
- @content = @spec.minify.call(@content)
422
+ @content = @delegate.minify.(@content)
313
423
  rescue => e
314
424
  @errors << e
315
425
  end
@@ -325,23 +435,66 @@ class Darkroom
325
435
  # Darkroom does not explicitly depend on any libraries necessary for asset compilation or minification,
326
436
  # since not every app will use every kind of asset or use minification. It is instead up to each app
327
437
  # using Darkroom to specify any needed compilation and minification libraries as direct dependencies
328
- # (e.g. specify +gem('uglifier')+ in the app's Gemfile if JavaScript minification is desired).
438
+ # (e.g. specify +gem('terser')+ in the app's Gemfile if JavaScript minification is desired).
329
439
  #
330
440
  def require_libs
331
441
  begin
332
- require(@spec.compile_lib) if @spec.compile_lib
442
+ require(@delegate.compile_lib) if @delegate.compile_lib
333
443
  rescue LoadError
334
444
  compile_load_error = true
335
445
  end
336
446
 
337
447
  begin
338
- require(@spec.minify_lib) if @spec.minify_lib && @minify
448
+ require(@delegate.minify_lib) if @delegate.minify_lib && @minify
339
449
  rescue LoadError
340
450
  minify_load_error = true
341
451
  end
342
452
 
343
- raise(MissingLibraryError.new(@spec.compile_lib, 'compile', @extension)) if compile_load_error
344
- raise(MissingLibraryError.new(@spec.minify_lib, 'minify', @extension)) if minify_load_error
453
+ raise(MissingLibraryError.new(@delegate.compile_lib, 'compile', @extension)) if compile_load_error
454
+ raise(MissingLibraryError.new(@delegate.minify_lib, 'minify', @extension)) if minify_load_error
455
+ end
456
+
457
+ ##
458
+ # Utility method used by #dependencies and #imports to recursively build arrays.
459
+ #
460
+ # * +name+ - Name of the array to accumulate (:dependencies or :imports).
461
+ # * +ignore+ - Set of assets already accumulated which can be ignored (used to avoid infinite loops when
462
+ # circular references are encountered).
463
+ #
464
+ def accumulate(name, ignore)
465
+ ignore ||= Set.new
466
+ ignore << self
467
+
468
+ process
469
+
470
+ instance_variable_get(:"@own_#{name}").each_with_object([]) do |asset, assets|
471
+ next if ignore.include?(asset)
472
+
473
+ asset.process
474
+ assets.push(*asset.send(name, ignore), asset)
475
+ assets.uniq!
476
+ assets.delete(self)
477
+ end
478
+ end
479
+
480
+ ##
481
+ # Utility method that returns the appropriate error for a dependency that doesn't exist.
482
+ #
483
+ # * +path+ - Path of the asset which cannot be found.
484
+ # * +match+ - MatchData object of the regex for the asset that cannot be found.
485
+ #
486
+ def not_found_error(path, match)
487
+ klass = Darkroom.delegate(File.extname(path)) ? AssetNotFoundError : UnrecognizedExtensionError
488
+ klass.new(path, @path, line_num(match))
489
+ end
490
+
491
+ ##
492
+ # Utility method that returns the line number where a regex match was found.
493
+ #
494
+ # * +match+ - MatchData object of the regex.
495
+ #
496
+ def line_num(match)
497
+ @own_content[0..match.begin(:path)].count("\n") + 1
345
498
  end
346
499
  end
347
500
  end
@@ -2,6 +2,12 @@
2
2
 
3
3
  require('set')
4
4
 
5
+ require_relative('asset')
6
+ require_relative('errors/asset_not_found_error')
7
+ require_relative('errors/duplicate_asset_error')
8
+ require_relative('errors/invalid_path_error')
9
+ require_relative('errors/processing_error')
10
+
5
11
  ##
6
12
  # Main class providing fast, lightweight, and straightforward web asset management.
7
13
  #
@@ -11,10 +17,47 @@ class Darkroom
11
17
  PRISTINE = Set.new(%w[/favicon.ico /mask-icon.svg /humans.txt /robots.txt]).freeze
12
18
  MIN_PROCESS_INTERVAL = 0.5
13
19
 
20
+ DISALLOWED_PATH_CHARS = '\'"`=<>? '
21
+ INVALID_PATH = /[#{DISALLOWED_PATH_CHARS}]/.freeze
14
22
  TRAILING_SLASHES = /\/+$/.freeze
15
23
 
24
+ @@delegates = {}
25
+ @@glob = ''
26
+
16
27
  attr_reader(:error, :errors, :process_key)
17
28
 
29
+ ##
30
+ # Registers an asset delegate.
31
+ #
32
+ # * +delegate+ - An HTTP MIME type string, a Hash of Delegate parameters, or a Delegate instance.
33
+ # * +extensions+ - File extension(s) to associate with this delegate.
34
+ #
35
+ def self.register(*extensions, delegate)
36
+ case delegate
37
+ when String
38
+ delegate = Asset::Delegate.new(content_type: delegate.freeze)
39
+ when Hash
40
+ delegate = Asset::Delegate.new(**delegate)
41
+ end
42
+
43
+ extensions.each do |extension|
44
+ @@delegates[extension] = delegate
45
+ end
46
+
47
+ @@glob = "**/*{#{@@delegates.keys.sort.join(',')}}"
48
+
49
+ delegate
50
+ end
51
+
52
+ ##
53
+ # Returns the delegate associated with a file extension.
54
+ #
55
+ # * +extension+ - File extension of the desired delegate.
56
+ #
57
+ def self.delegate(extension)
58
+ @@delegates[extension]
59
+ end
60
+
18
61
  ##
19
62
  # Creates a new instance.
20
63
  #
@@ -35,9 +78,7 @@ class Darkroom
35
78
  def initialize(*load_paths, host: nil, hosts: nil, prefix: nil, pristine: nil, minify: false,
36
79
  minified_pattern: DEFAULT_MINIFIED_PATTERN, internal_pattern: DEFAULT_INTERNAL_PATTERN,
37
80
  min_process_interval: MIN_PROCESS_INTERVAL)
38
- @globs = load_paths.each_with_object({}) do |path, globs|
39
- globs[path.chomp('/')] = File.join(path, '**', "*{#{Asset.extensions.join(',')}}")
40
- end
81
+ @load_paths = load_paths.map { |load_path| File.expand_path(load_path) }
41
82
 
42
83
  @hosts = (Array(host) + Array(hosts)).map! { |host| host.sub(TRAILING_SLASHES, '') }
43
84
  @minify = minify
@@ -58,6 +99,8 @@ class Darkroom
58
99
  @manifest_unversioned = {}
59
100
  @manifest_versioned = {}
60
101
 
102
+ @errors = []
103
+
61
104
  Thread.current[:darkroom_host_index] = -1 unless @hosts.empty?
62
105
  end
63
106
 
@@ -75,14 +118,16 @@ class Darkroom
75
118
 
76
119
  @mutex.synchronize do
77
120
  @process_key += 1
78
- @errors = []
121
+ @errors.clear
79
122
  found = {}
80
123
 
81
- @globs.each do |load_path, glob|
82
- Dir.glob(glob).sort.each do |file|
124
+ @load_paths.each do |load_path|
125
+ Dir.glob(File.join(load_path, @@glob)).sort.each do |file|
83
126
  path = file.sub(load_path, '')
84
127
 
85
- if found.key?(path)
128
+ if index = (path =~ INVALID_PATH)
129
+ @errors << InvalidPathError.new(path, index)
130
+ elsif found.key?(path)
86
131
  @errors << DuplicateAssetError.new(path, found[path], load_path)
87
132
  else
88
133
  found[path] = load_path
@@ -141,7 +186,7 @@ class Darkroom
141
186
  # darkroom.asset('/assets/js/app.<hash>.js')
142
187
  # darkroom.asset('/assets/js/app.js')
143
188
  #
144
- # * +path+ - The external path of the asset.
189
+ # * +path+ - External path of the asset.
145
190
  #
146
191
  def asset(path)
147
192
  @manifest_versioned[path] || @manifest_unversioned[path]
@@ -158,7 +203,7 @@ class Darkroom
158
203
  #
159
204
  # Raises an AssetNotFoundError if the asset doesn't exist.
160
205
  #
161
- # * +path+ - The internal path of the asset.
206
+ # * +path+ - Internal path of the asset.
162
207
  # * +versioned+ - Boolean indicating whether the versioned or unversioned path should be returned.
163
208
  #
164
209
  def asset_path(path, versioned: !@pristine.include?(path))
@@ -174,8 +219,8 @@ class Darkroom
174
219
  # Returns an asset's subresource integrity string. Raises an AssetNotFoundError if the asset doesn't
175
220
  # exist.
176
221
  #
177
- # * +path+ - The internal path of the asset.
178
- # * +algorithm+ - The hash algorithm to use to generate the integrity string (see Asset#integrity).
222
+ # * +path+ - Internal path of the asset.
223
+ # * +algorithm+ - Hash algorithm to use to generate the integrity string (see Asset#integrity).
179
224
  #
180
225
  def asset_integrity(path, algorithm = nil)
181
226
  asset = @manifest[path] or raise(AssetNotFoundError.new(path))
@@ -186,7 +231,7 @@ class Darkroom
186
231
  ##
187
232
  # Returns the asset from the manifest hash associated with the given path.
188
233
  #
189
- # * +path+ - The internal path of the asset.
234
+ # * +path+ - Internal path of the asset.
190
235
  #
191
236
  def manifest(path)
192
237
  @manifest[path]
@@ -204,6 +249,8 @@ class Darkroom
204
249
  # included).
205
250
  #
206
251
  def dump(dir, clear: false, include_pristine: true)
252
+ require('fileutils')
253
+
207
254
  dir = File.expand_path(dir)
208
255
 
209
256
  FileUtils.mkdir_p(dir)
@@ -228,10 +275,10 @@ class Darkroom
228
275
  def inspect
229
276
  "#<#{self.class}: "\
230
277
  "@errors=#{@errors.inspect}, "\
231
- "@globs=#{@globs.inspect}, "\
232
278
  "@hosts=#{@hosts.inspect}, "\
233
279
  "@internal_pattern=#{@internal_pattern.inspect}, "\
234
280
  "@last_processed_at=#{@last_processed_at.inspect}, "\
281
+ "@load_paths=#{@load_paths.inspect}, "\
235
282
  "@min_process_interval=#{@min_process_interval.inspect}, "\
236
283
  "@minified_pattern=#{@minified_pattern.inspect}, "\
237
284
  "@minify=#{@minify.inspect}, "\