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.
@@ -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
- DEFAULT_INTERNAL_PATTERN = nil
10
- DEFAULT_MINIFIED_PATTERN = /(\.|-)min\.\w+$/.freeze
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
- # * +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.
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
- case delegate
31
- when String
32
- delegate = Asset::Delegate.new(content_type: delegate.freeze)
33
- when Hash
34
- delegate = Asset::Delegate.new(**delegate)
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
- # * +extension+ - File extension of the desired delegate.
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
- # * +load_paths+ - Path(s) where assets are located on disk.
59
- # * +host+ - Host(s) to prepend to paths (useful when serving from a CDN in production). If multiple hosts
60
- # are specified, they will be round-robined within each thread for each call to +#asset_path+.
61
- # * +hosts+ - Alias of +host+ parameter.
62
- # * +prefix+ - Prefix to prepend to asset paths (e.g. +/assets+).
63
- # * +pristine+ - Path(s) that should not include prefix and for which unversioned form should be provided
64
- # by default (e.g. +/favicon.ico+).
65
- # * +minify+ - Boolean specifying whether or not to minify assets.
66
- # * +minified_pattern+ - Regex used against asset paths to determine if they are already minified and
67
- # should therefore be skipped over for minification.
68
- # * +internal_pattern+ - Regex used against asset paths to determine if they should be marked as internal
69
- # and therefore made inaccessible externally.
70
- # * +min_process_interval+ - Minimum time required between one run of asset processing and another.
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, minify: false,
73
- minified_pattern: DEFAULT_MINIFIED_PATTERN, internal_pattern: DEFAULT_INTERNAL_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
- @minified_pattern = minified_pattern
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 =~ INVALID_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[path] ||= Asset.new(path, file, self,
130
- prefix: (@prefix unless @pristine.include?(path)),
131
- internal: @internal_pattern && path =~ @internal_pattern,
132
- minify: @minify && path !~ @minified_pattern,
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
- unless asset.internal?
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 += asset.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
- # Does the same thing as #process, but raises an exception if any errors were encountered.
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
- raise(@error) if @error
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
- # * +path+ - External path of the asset.
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
- # * +path+ - Internal path of the asset.
201
- # * +versioned+ - Boolean indicating whether the versioned or unversioned path should be returned.
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
- # * +path+ - Internal path of the asset.
217
- # * +algorithm+ - Hash algorithm to use to generate the integrity string (see Asset#integrity).
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
- # * +path+ - Internal path of the asset.
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
- # * +dir+ - Directory to write the assets to.
239
- # * +clear+ - Boolean indicating whether or not the existing contents of the directory should be deleted
240
- # before performing the dump.
241
- # * +include_pristine+ - Boolean indicating whether or not to include pristine assets (when dumping for
242
- # the purpose of uploading to a CDN, assets such as /robots.txt and /favicon.ico don't need to be
243
- # included).
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
- "@minified_pattern=#{@minified_pattern.inspect}, "\
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 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
- )
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