darkroom 0.0.2 → 0.0.5

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,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