darkroom 0.0.5 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +6 -6
- data/VERSION +1 -1
- data/lib/darkroom/asset.rb +367 -240
- data/lib/darkroom/darkroom.rb +141 -59
- data/lib/darkroom/delegate.rb +237 -0
- data/lib/darkroom/delegates/css.rb +34 -32
- data/lib/darkroom/delegates/html.rb +39 -37
- data/lib/darkroom/delegates/htx.rb +15 -13
- data/lib/darkroom/delegates/javascript.rb +180 -9
- data/lib/darkroom/errors/asset_error.rb +4 -4
- data/lib/darkroom/errors/asset_not_found_error.rb +3 -3
- data/lib/darkroom/errors/circular_reference_error.rb +3 -3
- data/lib/darkroom/errors/duplicate_asset_error.rb +3 -3
- data/lib/darkroom/errors/invalid_path_error.rb +5 -3
- data/lib/darkroom/errors/missing_library_error.rb +3 -3
- data/lib/darkroom/errors/processing_error.rb +6 -2
- data/lib/darkroom/errors/unrecognized_extension_error.rb +3 -3
- data/lib/darkroom/version.rb +1 -1
- data/lib/darkroom.rb +5 -13
- metadata +4 -3
data/lib/darkroom/darkroom.rb
CHANGED
@@ -2,36 +2,51 @@
|
|
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
|
#
|
8
14
|
class Darkroom
|
9
|
-
|
10
|
-
|
15
|
+
DEFAULT_MINIFIED = /(\.|-)min\.\w+$/.freeze
|
16
|
+
TRAILING_SLASHES = /\/+$/.freeze
|
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
|
|
14
|
-
DISALLOWED_PATH_CHARS = '\'"`=<>? '
|
15
|
-
INVALID_PATH = /[#{DISALLOWED_PATH_CHARS}]/.freeze
|
16
|
-
TRAILING_SLASHES = /\/+$/.freeze
|
17
|
-
|
18
20
|
@@delegates = {}
|
19
21
|
@@glob = ''
|
20
22
|
|
21
23
|
attr_reader(:error, :errors, :process_key)
|
22
24
|
|
25
|
+
class << self; attr_accessor(:javascript_iife) end
|
26
|
+
|
23
27
|
##
|
24
28
|
# Registers an asset delegate.
|
25
29
|
#
|
26
|
-
# *
|
27
|
-
#
|
30
|
+
# [*extensions] One or more file extension(s) to associate with this delegate.
|
31
|
+
# [delegate] An HTTP MIME type string or a Delegate subclass.
|
28
32
|
#
|
29
|
-
def self.register(*extensions, delegate)
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
33
|
+
def self.register(*extensions, delegate, &block)
|
34
|
+
if delegate.kind_of?(String)
|
35
|
+
content_type = delegate
|
36
|
+
|
37
|
+
if delegate[0] == '.'
|
38
|
+
extensions << delegate
|
39
|
+
content_type = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
delegate = Class.new(Delegate, &block)
|
43
|
+
delegate.content_type(content_type) if content_type && !delegate.content_type
|
44
|
+
elsif delegate.kind_of?(Hash)
|
45
|
+
deprecated("#{self.name}.register with a Hash is deprecated: use the Delegate DSL inside a block "\
|
46
|
+
'instead')
|
47
|
+
delegate = Delegate.deprecated_from_hash(**delegate)
|
48
|
+
elsif delegate && delegate < Delegate
|
49
|
+
delegate = block ? Class.new(delegate, &block) : delegate
|
35
50
|
end
|
36
51
|
|
37
52
|
extensions.each do |extension|
|
@@ -46,38 +61,63 @@ class Darkroom
|
|
46
61
|
##
|
47
62
|
# Returns the delegate associated with a file extension.
|
48
63
|
#
|
49
|
-
#
|
64
|
+
# [extension] File extension of the desired delegate.
|
50
65
|
#
|
51
66
|
def self.delegate(extension)
|
52
67
|
@@delegates[extension]
|
53
68
|
end
|
54
69
|
|
70
|
+
##
|
71
|
+
# Utility method that prints a warning with file and line number of a deprecated call.
|
72
|
+
#
|
73
|
+
def self.deprecated(message)
|
74
|
+
location = caller_locations(2, 1).first
|
75
|
+
|
76
|
+
warn("#{location.path}:#{location.lineno}: #{message}")
|
77
|
+
end
|
78
|
+
|
55
79
|
##
|
56
80
|
# Creates a new instance.
|
57
81
|
#
|
58
|
-
# *
|
59
|
-
#
|
60
|
-
#
|
61
|
-
#
|
62
|
-
#
|
63
|
-
#
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
82
|
+
# [*load_paths] One or more paths where assets are located on disk.
|
83
|
+
# [host:] Host(s) to prepend to paths (useful when serving from a CDN in production). If multiple hosts
|
84
|
+
# are specified, they will be round-robined within each thread for each call to +#asset_path+.
|
85
|
+
# [hosts:] Alias of +host:+.
|
86
|
+
# [prefix:] Prefix to prepend to asset paths (e.g. +/assets+).
|
87
|
+
# [pristine:] Path(s) that should not include prefix and for which unversioned form should be provided by
|
88
|
+
# default (e.g. +/favicon.ico+).
|
89
|
+
# [entries:] String, regex, or array of strings and regexes specifying entry point paths / path patterns.
|
90
|
+
# [minify:] Boolean specifying whether or not to minify assets.
|
91
|
+
# [minified:] String, regex, or array of strings and regexes specifying paths of assets that are already
|
92
|
+
# minified and thus should be skipped for minification.
|
93
|
+
# [minified_pattern:] DEPRECATED: use +minified:+ instead. Regex used against asset paths to determine if
|
94
|
+
# they are already minified and should therefore be skipped over for minification.
|
95
|
+
# [internal_pattern:] DEPRECATED: use +entries:+ instead. Regex used against asset paths to determine if
|
96
|
+
# they should be marked as internal and therefore made inaccessible externally.
|
97
|
+
# [min_process_interval:] Minimum time required between one run of asset processing and another.
|
71
98
|
#
|
72
|
-
def initialize(*load_paths, host: nil, hosts: nil, prefix: nil, pristine: nil,
|
73
|
-
minified_pattern:
|
99
|
+
def initialize(*load_paths, host: nil, hosts: nil, prefix: nil, pristine: nil, entries: nil,
|
100
|
+
minify: false, minified: DEFAULT_MINIFIED, minified_pattern: nil, internal_pattern: nil,
|
74
101
|
min_process_interval: MIN_PROCESS_INTERVAL)
|
75
102
|
@load_paths = load_paths.map { |load_path| File.expand_path(load_path) }
|
76
103
|
|
77
104
|
@hosts = (Array(host) + Array(hosts)).map! { |host| host.sub(TRAILING_SLASHES, '') }
|
105
|
+
@entries = Array(entries)
|
78
106
|
@minify = minify
|
107
|
+
@minified = Array(minified)
|
79
108
|
@internal_pattern = internal_pattern
|
80
|
-
|
109
|
+
|
110
|
+
if minified_pattern
|
111
|
+
self.class.deprecated("#{self.class.name} :minified_pattern is deprecated: use :minified instead "\
|
112
|
+
'and pass a string, regex, or array of strings and regexes')
|
113
|
+
@minified = [minified_pattern]
|
114
|
+
end
|
115
|
+
|
116
|
+
if @internal_pattern
|
117
|
+
self.class.deprecated("#{self.class.name} :internal_pattern is deprecated: use :entries to instead "\
|
118
|
+
'specify which assets are entry points (i.e. available externally) and pass a string, regex, or '\
|
119
|
+
'array of strings and regexes')
|
120
|
+
end
|
81
121
|
|
82
122
|
@prefix = prefix&.sub(TRAILING_SLASHES, '')
|
83
123
|
@prefix = nil if @prefix && @prefix.empty?
|
@@ -100,14 +140,15 @@ class Darkroom
|
|
100
140
|
|
101
141
|
##
|
102
142
|
# Walks all load paths and refreshes any assets that have been modified on disk since the last call to
|
103
|
-
# this method.
|
143
|
+
# this method. Returns false if processing was skipped due to previous call happening less than
|
144
|
+
# min_process_interval ago or because another thread was already processing; returns true otherwise.
|
104
145
|
#
|
105
146
|
def process
|
106
|
-
return if Time.now.to_f - @last_processed_at < @min_process_interval
|
147
|
+
return false if Time.now.to_f - @last_processed_at < @min_process_interval
|
107
148
|
|
108
149
|
if @mutex.locked?
|
109
150
|
@mutex.synchronize {}
|
110
|
-
return
|
151
|
+
return false
|
111
152
|
end
|
112
153
|
|
113
154
|
@mutex.synchronize do
|
@@ -119,18 +160,22 @@ class Darkroom
|
|
119
160
|
Dir.glob(File.join(load_path, @@glob)).sort.each do |file|
|
120
161
|
path = file.sub(load_path, '')
|
121
162
|
|
122
|
-
if index = (path =~
|
163
|
+
if index = (path =~ Asset::INVALID_PATH_REGEX)
|
123
164
|
@errors << InvalidPathError.new(path, index)
|
124
165
|
elsif found.key?(path)
|
125
166
|
@errors << DuplicateAssetError.new(path, found[path], load_path)
|
126
167
|
else
|
127
168
|
found[path] = load_path
|
128
169
|
|
129
|
-
@manifest
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
170
|
+
unless @manifest.key?(path)
|
171
|
+
entry = entry?(path)
|
172
|
+
|
173
|
+
@manifest[path] = Asset.new(path, file, self,
|
174
|
+
prefix: (@prefix unless @pristine.include?(path)),
|
175
|
+
entry: entry,
|
176
|
+
minify: entry && @minify && !minified?(path),
|
177
|
+
)
|
178
|
+
end
|
134
179
|
end
|
135
180
|
end
|
136
181
|
end
|
@@ -142,13 +187,15 @@ class Darkroom
|
|
142
187
|
@manifest.each do |path, asset|
|
143
188
|
asset.process
|
144
189
|
|
145
|
-
|
190
|
+
if asset.entry?
|
146
191
|
@manifest_unversioned[asset.path_unversioned] = asset
|
147
192
|
@manifest_versioned[asset.path_versioned] = asset
|
148
193
|
end
|
149
194
|
|
150
|
-
@errors
|
195
|
+
@errors.concat(asset.errors)
|
151
196
|
end
|
197
|
+
|
198
|
+
true
|
152
199
|
ensure
|
153
200
|
@last_processed_at = Time.now.to_f
|
154
201
|
@error = @errors.empty? ? nil : ProcessingError.new(@errors)
|
@@ -156,12 +203,13 @@ class Darkroom
|
|
156
203
|
end
|
157
204
|
|
158
205
|
##
|
159
|
-
#
|
206
|
+
# Calls #process. If processing was skipped, returns false. If processing was performed, raises an
|
207
|
+
# exception if any errors were encountered and returns true otherwise.
|
160
208
|
#
|
161
209
|
def process!
|
162
|
-
process
|
210
|
+
result = process
|
163
211
|
|
164
|
-
|
212
|
+
(result && @error) ? raise(@error) : result
|
165
213
|
end
|
166
214
|
|
167
215
|
##
|
@@ -180,7 +228,7 @@ class Darkroom
|
|
180
228
|
# darkroom.asset('/assets/js/app.<hash>.js')
|
181
229
|
# darkroom.asset('/assets/js/app.js')
|
182
230
|
#
|
183
|
-
#
|
231
|
+
# [path] External path of the asset.
|
184
232
|
#
|
185
233
|
def asset(path)
|
186
234
|
@manifest_versioned[path] || @manifest_unversioned[path]
|
@@ -197,8 +245,8 @@ class Darkroom
|
|
197
245
|
#
|
198
246
|
# Raises an AssetNotFoundError if the asset doesn't exist.
|
199
247
|
#
|
200
|
-
#
|
201
|
-
#
|
248
|
+
# [path] Internal path of the asset.
|
249
|
+
# [versioned:] Boolean indicating whether the versioned or unversioned path should be returned.
|
202
250
|
#
|
203
251
|
def asset_path(path, versioned: !@pristine.include?(path))
|
204
252
|
asset = @manifest[path] or raise(AssetNotFoundError.new(path))
|
@@ -213,8 +261,8 @@ class Darkroom
|
|
213
261
|
# Returns an asset's subresource integrity string. Raises an AssetNotFoundError if the asset doesn't
|
214
262
|
# exist.
|
215
263
|
#
|
216
|
-
#
|
217
|
-
#
|
264
|
+
# [path] Internal path of the asset.
|
265
|
+
# [algorithm] Hash algorithm to use to generate the integrity string (see Asset#integrity).
|
218
266
|
#
|
219
267
|
def asset_integrity(path, algorithm = nil)
|
220
268
|
asset = @manifest[path] or raise(AssetNotFoundError.new(path))
|
@@ -225,7 +273,7 @@ class Darkroom
|
|
225
273
|
##
|
226
274
|
# Returns the asset from the manifest hash associated with the given path.
|
227
275
|
#
|
228
|
-
#
|
276
|
+
# [path] Internal path of the asset.
|
229
277
|
#
|
230
278
|
def manifest(path)
|
231
279
|
@manifest[path]
|
@@ -235,14 +283,16 @@ class Darkroom
|
|
235
283
|
# Writes assets to disk. This is useful when deploying to a production environment where assets will be
|
236
284
|
# uploaded to and served from a CDN or proxy server.
|
237
285
|
#
|
238
|
-
#
|
239
|
-
#
|
240
|
-
#
|
241
|
-
#
|
242
|
-
#
|
243
|
-
#
|
286
|
+
# [dir] Directory to write the assets to.
|
287
|
+
# [clear:] Boolean indicating whether or not the existing contents of the directory should be deleted
|
288
|
+
# before performing the dump.
|
289
|
+
# [include_pristine:] Boolean indicating whether or not to include pristine assets (when dumping for the
|
290
|
+
# purpose of uploading to a CDN, assets such as /robots.txt and /favicon.ico don't
|
291
|
+
# need to be included).
|
244
292
|
#
|
245
293
|
def dump(dir, clear: false, include_pristine: true)
|
294
|
+
raise(@error) if @error
|
295
|
+
|
246
296
|
require('fileutils')
|
247
297
|
|
248
298
|
dir = File.expand_path(dir)
|
@@ -251,7 +301,6 @@ class Darkroom
|
|
251
301
|
Dir.each_child(dir) { |child| FileUtils.rm_rf(File.join(dir, child)) } if clear
|
252
302
|
|
253
303
|
@manifest_versioned.each do |path, asset|
|
254
|
-
next if asset.internal?
|
255
304
|
next if @pristine.include?(asset.path) && !include_pristine
|
256
305
|
|
257
306
|
file_path = File.join(dir,
|
@@ -268,17 +317,50 @@ class Darkroom
|
|
268
317
|
#
|
269
318
|
def inspect
|
270
319
|
"#<#{self.class}: "\
|
320
|
+
"@entries=#{@entries.inspect}, "\
|
271
321
|
"@errors=#{@errors.inspect}, "\
|
272
322
|
"@hosts=#{@hosts.inspect}, "\
|
273
323
|
"@internal_pattern=#{@internal_pattern.inspect}, "\
|
274
324
|
"@last_processed_at=#{@last_processed_at.inspect}, "\
|
275
325
|
"@load_paths=#{@load_paths.inspect}, "\
|
276
326
|
"@min_process_interval=#{@min_process_interval.inspect}, "\
|
277
|
-
"@
|
327
|
+
"@minified=#{@minified.inspect}, "\
|
278
328
|
"@minify=#{@minify.inspect}, "\
|
279
329
|
"@prefix=#{@prefix.inspect}, "\
|
280
330
|
"@pristine=#{@pristine.inspect}, "\
|
281
331
|
"@process_key=#{@process_key.inspect}"\
|
282
332
|
'>'
|
283
333
|
end
|
334
|
+
|
335
|
+
private
|
336
|
+
|
337
|
+
##
|
338
|
+
# Returns boolean indicating whether or not the provided path is an entry point.
|
339
|
+
#
|
340
|
+
# [path] Path to check.
|
341
|
+
#
|
342
|
+
def entry?(path)
|
343
|
+
if @pristine.include?(path)
|
344
|
+
true
|
345
|
+
elsif @internal_pattern && @entries.empty?
|
346
|
+
!path.match?(@internal_pattern)
|
347
|
+
elsif @entries.empty?
|
348
|
+
true
|
349
|
+
else
|
350
|
+
@entries.any? do |entry|
|
351
|
+
path == entry || (entry.kind_of?(Regexp) && path.match?(entry))
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
##
|
357
|
+
# Returns boolean indicating whether or not the asset with the provided path is already minified.
|
358
|
+
#
|
359
|
+
# [path] Path to check.
|
360
|
+
#
|
361
|
+
def minified?(path)
|
362
|
+
@minified.any? do |minified|
|
363
|
+
path == minified || (minified.kind_of?(Regexp) && path.match?(minified))
|
364
|
+
end
|
365
|
+
end
|
284
366
|
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
class Darkroom
|
2
|
+
##
|
3
|
+
# Holds asset type-specific information and functionality.
|
4
|
+
#
|
5
|
+
# [minify_lib:] Name of a library to +require+ that is needed by the +minify+ lambda (optional).
|
6
|
+
# [minify:] Lambda to call that will return the minified version of the asset's content (optional). One
|
7
|
+
# argument is passed when called:
|
8
|
+
# * +content+ - Content to minify.
|
9
|
+
#
|
10
|
+
class Delegate
|
11
|
+
[
|
12
|
+
:content_type, :parsers, :compile_lib, :compile_delegate, :compile_handler, :finalize_lib,
|
13
|
+
:finalize_handler, :minify_lib, :minify_handler
|
14
|
+
].each do |name|
|
15
|
+
var = :"@#{name}"
|
16
|
+
instance_variable_set(var, nil)
|
17
|
+
|
18
|
+
define_singleton_method(name) do
|
19
|
+
instance_variable_defined?(var) ? instance_variable_get(var) : superclass.send(name)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class << self; alias :get_content_type :content_type end
|
24
|
+
|
25
|
+
##
|
26
|
+
# Sets or returns HTTP MIME type string.
|
27
|
+
#
|
28
|
+
def self.content_type(content_type = (get = true; nil))
|
29
|
+
get ? get_content_type : (@content_type = content_type)
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Configures how imports are handled.
|
34
|
+
#
|
35
|
+
# [regex] Regex for finding import statements. Must contain a named component called +path+ (e.g.
|
36
|
+
# <tt>/^import (?<path>.*)/</tt>).
|
37
|
+
# [&handler] Block for special handling of import statements (optional). Should
|
38
|
+
# <tt>throw(:error, '...')</tt> on error. Passed three arguments:
|
39
|
+
# * +parse_data:+ - Hash for storing data across calls to this and other parsing handlers.
|
40
|
+
# * +match:+ - MatchData object from the match against +regex+.
|
41
|
+
# * +asset:+ - Asset object of the asset being imported.
|
42
|
+
# Return value is used as the substitution for the import statement, with optional second and
|
43
|
+
# third values as integers representing the start and end indexes of the match to replace.
|
44
|
+
#
|
45
|
+
def self.import(regex, &handler)
|
46
|
+
parse(:import, regex, &handler)
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Configures how references are handled.
|
51
|
+
#
|
52
|
+
# [regex] Regex for finding references. Must contain three named components:
|
53
|
+
# * +path+ - Path of the asset being referenced.
|
54
|
+
# * +entity+ - Desired entity ('path' or 'content').
|
55
|
+
# * +format+ - Format to use (see Asset::REFERENCE_FORMATS).
|
56
|
+
# [&handler] Block for special handling of references (optional). Should <tt>throw(:error, '...')</tt>
|
57
|
+
# on error. Passed four arguments:
|
58
|
+
# * +parse_data:+ - Hash for storing data across calls to this and other parsing handlers.
|
59
|
+
# * +match:+ - MatchData object from the match against +regex+.
|
60
|
+
# * +asset:+ - Asset object of the asset being imported.
|
61
|
+
# * +format:+ - Format of the reference (see Asset::REFERENCE_FORMATS).
|
62
|
+
# Return value is used as the substitution for the reference, with optional second and third
|
63
|
+
# values as integers representing the start and end indexes of the match to replace.
|
64
|
+
#
|
65
|
+
def self.reference(regex, &handler)
|
66
|
+
parse(:reference, regex, &handler)
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Configures a parser.
|
71
|
+
#
|
72
|
+
# [kind] A name to describe what is being parsed. Should be unique across all +parse+ calls. When
|
73
|
+
# subclassing another Delegate, can be used to override the parent class's regex and handler.
|
74
|
+
# [regex] Regex to match against.
|
75
|
+
# [&handler] Block for handling matches of the regex. Should <tt>throw(:error, '...')</tt>
|
76
|
+
# on error. Passed two arguments:
|
77
|
+
# * +parse_data:+ - Hash for storing data across calls to this and other parsing handlers.
|
78
|
+
# * +match:+ - MatchData object from the match against +regex+.
|
79
|
+
# Return value is used as the substitution for the reference, with optional second and third
|
80
|
+
# values as integers representing the start and end indexes of the match to replace.
|
81
|
+
#
|
82
|
+
def self.parse(kind, regex, &handler)
|
83
|
+
@parsers = parsers&.dup || {} unless @parsers
|
84
|
+
@parsers[kind] = [regex, handler]
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Configures compilation.
|
89
|
+
#
|
90
|
+
# [lib:] Name of a library to +require+ that is needed by the handler (optional).
|
91
|
+
# [delegate:] Another Delegate to be used after the asset is compiled (optional).
|
92
|
+
# [&handler] Block to call that will return the compiled version of the asset's own content. Passed
|
93
|
+
# three arguments when called:
|
94
|
+
#. * +parse_data:+ - Hash of data collected during parsing.
|
95
|
+
# * +path+ - Path of the asset being compiled.
|
96
|
+
# * +own_content+ - Asset's own content.
|
97
|
+
# Asset's own content is set to the value returned.
|
98
|
+
#
|
99
|
+
def self.compile(lib: nil, delegate: nil, &handler)
|
100
|
+
@compile_lib = lib
|
101
|
+
@compile_delegate = delegate
|
102
|
+
@compile_handler = handler
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Configures finalize behavior.
|
107
|
+
#
|
108
|
+
# [lib:] Name of a library to +require+ that is needed by the handler (optional).
|
109
|
+
# [&handler] Block to call that will return the completed version of the asset's overall content. Passed
|
110
|
+
# three arguments when called:
|
111
|
+
#. * +parse_data:+ - Hash of data collected during parsing.
|
112
|
+
# * +path+ - Path of the asset being finalized.
|
113
|
+
# * +content+ - Asset's content (with imports prepended).
|
114
|
+
# Asset's content is set to the value returned.
|
115
|
+
#
|
116
|
+
def self.finalize(lib: nil, &handler)
|
117
|
+
@finalize_lib = lib
|
118
|
+
@finalize_handler = handler
|
119
|
+
end
|
120
|
+
|
121
|
+
##
|
122
|
+
# Configures minification.
|
123
|
+
#
|
124
|
+
# [lib:] Name of a library to +require+ that is needed by the handler (optional).
|
125
|
+
# [&handler] Block to call that will return the minified version of the asset's overall content. Passed
|
126
|
+
# three arguments when called:
|
127
|
+
#. * +parse_data:+ - Hash of data collected during parsing.
|
128
|
+
# * +path+ - Path of the asset being finalized.
|
129
|
+
# * +content+ - Finalized asset's content.
|
130
|
+
# Asset's minified content is set to the value returned.
|
131
|
+
#
|
132
|
+
def self.minify(lib: nil, &handler)
|
133
|
+
@minify_lib = lib
|
134
|
+
@minify_handler = handler
|
135
|
+
end
|
136
|
+
|
137
|
+
##
|
138
|
+
# Throws +:error+ with a message.
|
139
|
+
#
|
140
|
+
# [message] Message to include with the throw.
|
141
|
+
#
|
142
|
+
def self.error(message)
|
143
|
+
throw(:error, message)
|
144
|
+
end
|
145
|
+
|
146
|
+
##
|
147
|
+
# Returns regex for a parser.
|
148
|
+
#
|
149
|
+
# [kind] Name of the parser.
|
150
|
+
#
|
151
|
+
def self.regex(kind)
|
152
|
+
parsers[kind]&.first
|
153
|
+
end
|
154
|
+
|
155
|
+
##
|
156
|
+
# Returns handler for a parser.
|
157
|
+
#
|
158
|
+
# [kind] Name of the parser.
|
159
|
+
#
|
160
|
+
def self.handler(kind)
|
161
|
+
parsers[kind]&.last
|
162
|
+
end
|
163
|
+
|
164
|
+
##
|
165
|
+
# Iterates over each parser and yields its kind, regex, and handler.
|
166
|
+
#
|
167
|
+
def self.each_parser
|
168
|
+
parsers&.each do |kind, (regex, handler)|
|
169
|
+
yield(kind, regex, handler)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
##
|
174
|
+
# DEPRECATED: subclass Delegate and use its DSL instead. Returns a subclass of Delegate configured using
|
175
|
+
# the supplied Hash.
|
176
|
+
#
|
177
|
+
def self.new(**params)
|
178
|
+
Darkroom.deprecated("#{self.name}::new is deprecated: use the DSL inside a child class or a block "\
|
179
|
+
'passed to Darkroom.register')
|
180
|
+
|
181
|
+
deprecated_from_hash(**params)
|
182
|
+
end
|
183
|
+
|
184
|
+
##
|
185
|
+
# DEPRECATED: subclass Delegate and use its DSL instead. Returns a subclass of Delegate configured using
|
186
|
+
# the supplied Hash.
|
187
|
+
#
|
188
|
+
def self.deprecated_from_hash(content_type:, import_regex: nil, reference_regex: nil,
|
189
|
+
validate_reference: nil, reference_content: nil, compile_lib: nil, compile: nil, compiled: nil,
|
190
|
+
minify_lib: nil, minify: nil)
|
191
|
+
Class.new(Delegate) do
|
192
|
+
self.content_type(content_type)
|
193
|
+
|
194
|
+
@import_regex = import_regex
|
195
|
+
@reference_regex = reference_regex
|
196
|
+
|
197
|
+
self.import(import_regex) if import_regex
|
198
|
+
|
199
|
+
if validate_reference || reference_content
|
200
|
+
@validate_reference = validate_reference
|
201
|
+
@reference_content = reference_content
|
202
|
+
|
203
|
+
self.reference(reference_regex) do |parse_data:, match:, asset:, format:|
|
204
|
+
error_message = validate_reference&.call(asset, match, format)
|
205
|
+
error(error_message) if error_message
|
206
|
+
|
207
|
+
reference_content&.call(asset, match, format)
|
208
|
+
end
|
209
|
+
elsif reference_regex
|
210
|
+
self.reference(reference_regex)
|
211
|
+
end
|
212
|
+
|
213
|
+
if compile
|
214
|
+
self.compile(lib: compile_lib, delegate: compiled) do |parse_data:, path:, own_content:|
|
215
|
+
compile.call(path, own_content)
|
216
|
+
end
|
217
|
+
elsif compile_lib || compiled
|
218
|
+
self.compile(lib: compile_lib, delegate: compiled)
|
219
|
+
end
|
220
|
+
|
221
|
+
if minify
|
222
|
+
self.minify(lib: minify_lib) do |parse_data:, path:, content:|
|
223
|
+
minify.call(content)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
##
|
230
|
+
# DEPRECATED: subclass Delegate and use its DSL instead.
|
231
|
+
#
|
232
|
+
def self.import_regex() @import_regex end
|
233
|
+
def self.reference_regex() @reference_regex end
|
234
|
+
def self.validate_reference() @validate_reference end
|
235
|
+
def self.reference_content() @reference_content end
|
236
|
+
end
|
237
|
+
end
|
@@ -1,39 +1,41 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative('../asset')
|
4
|
+
require_relative('../delegate')
|
4
5
|
|
5
6
|
class Darkroom
|
6
|
-
class
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
7
|
+
class CSSDelegate < Delegate
|
8
|
+
IMPORT_REGEX = /
|
9
|
+
(?<=^|;)[^\S\n]*
|
10
|
+
@import\s+#{Asset::QUOTED_PATH_REGEX.source}
|
11
|
+
[^\S\n]*;[^\S\n]*(\n|\Z)
|
12
|
+
/x.freeze
|
13
|
+
|
14
|
+
REFERENCE_REGEX = /url\(\s*#{Asset::REFERENCE_REGEX.source}\s*\)/x.freeze
|
15
|
+
|
16
|
+
content_type('text/css')
|
17
|
+
|
18
|
+
import(IMPORT_REGEX)
|
19
|
+
|
20
|
+
reference(REFERENCE_REGEX) do |parse_data:, match:, asset:, format:|
|
21
|
+
if format == 'displace'
|
22
|
+
error('Cannot displace in CSS files')
|
23
|
+
elsif !asset.image? && !asset.font?
|
24
|
+
error('Referenced asset must be an image or font type')
|
25
|
+
elsif format == 'utf8'
|
26
|
+
content = asset.content.dup
|
27
|
+
|
28
|
+
content.gsub!('#', '%23')
|
29
|
+
content.gsub!('\'', '\\\\\'')
|
30
|
+
content.gsub!('"', '\\"')
|
31
|
+
content.gsub!("\n", "\\\n")
|
32
|
+
|
33
|
+
content
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
minify(lib: 'sassc') do |parse_data:, path:, content:|
|
38
|
+
SassC::Engine.new(content, style: :compressed).render
|
39
|
+
end
|
38
40
|
end
|
39
41
|
end
|