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.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +127 -41
- data/VERSION +1 -1
- data/lib/darkroom/asset.rb +253 -100
- data/lib/darkroom/darkroom.rb +60 -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 +14 -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 +16 -32
- metadata +11 -4
- data/lib/darkroom/errors/spec_not_defined_error.rb +0 -28
data/lib/darkroom/asset.rb
CHANGED
@@ -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
|
-
|
15
|
-
|
16
|
-
BODY_END = '(?<!\\\\)(\\\\\\\\)*'
|
17
|
-
CLOSE_QUOTE = '\k<quote>'
|
22
|
+
IMPORT_JOINER = "\n"
|
23
|
+
DEFAULT_QUOTE = '\''
|
18
24
|
|
19
|
-
|
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
|
-
|
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
|
-
# * +
|
30
|
-
#
|
31
|
-
# * +
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
75
|
-
|
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+ -
|
82
|
-
# * +path+ -
|
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
|
87
|
-
#
|
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
|
-
@
|
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,
|
108
|
-
#
|
109
|
-
#
|
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
|
-
@
|
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+ -
|
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
|
-
# * +
|
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(
|
225
|
-
@dependencies
|
226
|
-
next dependencies if ancestors.include?(self)
|
250
|
+
def dependencies(ignore = nil)
|
251
|
+
return @dependencies if @dependencies
|
227
252
|
|
228
|
-
|
229
|
-
|
253
|
+
dependencies = accumulate(:dependencies, ignore)
|
254
|
+
@dependencies = dependencies unless ignore
|
230
255
|
|
231
|
-
|
232
|
-
|
256
|
+
dependencies
|
257
|
+
end
|
233
258
|
|
234
|
-
|
235
|
-
|
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
|
-
|
238
|
-
|
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
|
303
|
+
# Reads the asset file into memory.
|
268
304
|
#
|
269
305
|
def read
|
270
|
-
|
271
|
-
|
272
|
-
return
|
273
|
-
end
|
306
|
+
@own_content = File.read(@file)
|
307
|
+
end
|
274
308
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
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
|
-
@
|
331
|
+
@own_dependencies << asset
|
332
|
+
@dependency_matches << [:reference, asset, match, format]
|
281
333
|
end
|
282
334
|
else
|
283
|
-
@
|
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 <<
|
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 @
|
404
|
+
if @delegate.compile
|
295
405
|
begin
|
296
|
-
@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 @
|
420
|
+
if @delegate.minify && @minify && !@internal
|
311
421
|
begin
|
312
|
-
@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('
|
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(@
|
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(@
|
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(@
|
344
|
-
raise(MissingLibraryError.new(@
|
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
|
data/lib/darkroom/darkroom.rb
CHANGED
@@ -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
|
-
@
|
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
|
-
@
|
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
|
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+ -
|
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+ -
|
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+ -
|
178
|
-
# * +algorithm+ -
|
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+ -
|
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}, "\
|