darkroom 0.0.2 → 0.0.5

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