darkroom 0.0.2 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +141 -21
- data/VERSION +1 -1
- data/lib/darkroom/asset.rb +239 -97
- data/lib/darkroom/darkroom.rb +54 -13
- data/lib/darkroom/delegates/css.rb +39 -0
- data/lib/darkroom/delegates/html.rb +53 -0
- data/lib/darkroom/delegates/htx.rb +21 -0
- data/lib/darkroom/delegates/javascript.rb +16 -0
- data/lib/darkroom/errors/asset_error.rb +33 -0
- data/lib/darkroom/errors/asset_not_found_error.rb +10 -21
- data/lib/darkroom/errors/circular_reference_error.rb +21 -0
- data/lib/darkroom/errors/duplicate_asset_error.rb +4 -4
- data/lib/darkroom/errors/invalid_path_error.rb +28 -0
- data/lib/darkroom/errors/missing_library_error.rb +3 -3
- data/lib/darkroom/errors/unrecognized_extension_error.rb +21 -0
- data/lib/darkroom/version.rb +1 -1
- data/lib/darkroom.rb +21 -28
- metadata +15 -6
- data/lib/darkroom/errors/spec_not_defined_error.rb +0 -28
data/lib/darkroom/asset.rb
CHANGED
@@ -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
|
-
|
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
|
-
# * +
|
23
|
-
#
|
24
|
-
# * +
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
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
|
-
|
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+ -
|
75
|
-
# * +path+ -
|
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
|
80
|
-
#
|
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
|
-
@
|
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,
|
101
|
-
#
|
102
|
-
#
|
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
|
-
@
|
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+ -
|
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
|
-
# * +
|
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(
|
218
|
-
@dependencies ||=
|
219
|
-
|
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
|
-
|
231
|
-
|
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
|
287
|
+
# Reads the asset file into memory.
|
261
288
|
#
|
262
289
|
def read
|
263
|
-
|
264
|
-
|
265
|
-
return
|
266
|
-
end
|
290
|
+
@own_content = File.read(@file)
|
291
|
+
end
|
267
292
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
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
|
-
@
|
315
|
+
@own_dependencies << asset
|
316
|
+
@dependency_matches << [:reference, asset, match, format]
|
274
317
|
end
|
275
318
|
else
|
276
|
-
@
|
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 <<
|
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 @
|
388
|
+
if @delegate.compile
|
288
389
|
begin
|
289
|
-
@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 @
|
404
|
+
if @delegate.minify && @minify && !@internal
|
304
405
|
begin
|
305
|
-
@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(@
|
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(@
|
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(@
|
337
|
-
raise(MissingLibraryError.new(@
|
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
|
data/lib/darkroom/darkroom.rb
CHANGED
@@ -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
|
-
@
|
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
|
-
@
|
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
|
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+ -
|
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+ -
|
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+ -
|
178
|
-
# * +algorithm+ -
|
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+ -
|
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
|