darkroom 0.0.3 → 0.0.6

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.
@@ -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}, "\