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