darkroom 0.0.5 → 0.0.7
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/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
|