darkroom 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,32 +12,41 @@ require_relative('errors/processing_error')
12
12
  # Main class providing fast, lightweight, and straightforward web asset management.
13
13
  #
14
14
  class Darkroom
15
- DEFAULT_INTERNAL_PATTERN = nil
16
- DEFAULT_MINIFIED_PATTERN = /(\.|-)min\.\w+$/.freeze
15
+ DEFAULT_MINIFIED = /(\.|-)min\.\w+$/.freeze
16
+ TRAILING_SLASHES = /\/+$/.freeze
17
17
  PRISTINE = Set.new(%w[/favicon.ico /mask-icon.svg /humans.txt /robots.txt]).freeze
18
18
  MIN_PROCESS_INTERVAL = 0.5
19
19
 
20
- DISALLOWED_PATH_CHARS = '\'"`=<>? '
21
- INVALID_PATH = /[#{DISALLOWED_PATH_CHARS}]/.freeze
22
- TRAILING_SLASHES = /\/+$/.freeze
23
-
24
20
  @@delegates = {}
25
21
  @@glob = ''
26
22
 
27
23
  attr_reader(:error, :errors, :process_key)
28
24
 
25
+ class << self; attr_accessor(:javascript_iife) end
26
+
29
27
  ##
30
28
  # Registers an asset delegate.
31
29
  #
32
- # * +delegate+ - An HTTP MIME type string, a Hash of Delegate parameters, or a Delegate instance.
33
- # * +extensions+ - File extension(s) to associate with this delegate.
30
+ # [*extensions] One or more file extension(s) to associate with this delegate.
31
+ # [delegate] An HTTP MIME type string or a Delegate subclass.
34
32
  #
35
- def self.register(*extensions, delegate)
36
- case delegate
37
- when String
38
- delegate = Asset::Delegate.new(content_type: delegate.freeze)
39
- when Hash
40
- delegate = Asset::Delegate.new(**delegate)
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
41
50
  end
42
51
 
43
52
  extensions.each do |extension|
@@ -52,38 +61,63 @@ class Darkroom
52
61
  ##
53
62
  # Returns the delegate associated with a file extension.
54
63
  #
55
- # * +extension+ - File extension of the desired delegate.
64
+ # [extension] File extension of the desired delegate.
56
65
  #
57
66
  def self.delegate(extension)
58
67
  @@delegates[extension]
59
68
  end
60
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
+
61
79
  ##
62
80
  # Creates a new instance.
63
81
  #
64
- # * +load_paths+ - Path(s) where assets are located on disk.
65
- # * +host+ - Host(s) to prepend to paths (useful when serving from a CDN in production). If multiple hosts
66
- # are specified, they will be round-robined within each thread for each call to +#asset_path+.
67
- # * +hosts+ - Alias of +host+ parameter.
68
- # * +prefix+ - Prefix to prepend to asset paths (e.g. +/assets+).
69
- # * +pristine+ - Path(s) that should not include prefix and for which unversioned form should be provided
70
- # by default (e.g. +/favicon.ico+).
71
- # * +minify+ - Boolean specifying whether or not to minify assets.
72
- # * +minified_pattern+ - Regex used against asset paths to determine if they are already minified and
73
- # should therefore be skipped over for minification.
74
- # * +internal_pattern+ - Regex used against asset paths to determine if they should be marked as internal
75
- # and therefore made inaccessible externally.
76
- # * +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.
77
98
  #
78
- def initialize(*load_paths, host: nil, hosts: nil, prefix: nil, pristine: nil, minify: false,
79
- 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,
80
101
  min_process_interval: MIN_PROCESS_INTERVAL)
81
102
  @load_paths = load_paths.map { |load_path| File.expand_path(load_path) }
82
103
 
83
104
  @hosts = (Array(host) + Array(hosts)).map! { |host| host.sub(TRAILING_SLASHES, '') }
105
+ @entries = Array(entries)
84
106
  @minify = minify
107
+ @minified = Array(minified)
85
108
  @internal_pattern = internal_pattern
86
- @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
87
121
 
88
122
  @prefix = prefix&.sub(TRAILING_SLASHES, '')
89
123
  @prefix = nil if @prefix && @prefix.empty?
@@ -106,14 +140,15 @@ class Darkroom
106
140
 
107
141
  ##
108
142
  # Walks all load paths and refreshes any assets that have been modified on disk since the last call to
109
- # 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.
110
145
  #
111
146
  def process
112
- 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
113
148
 
114
149
  if @mutex.locked?
115
150
  @mutex.synchronize {}
116
- return
151
+ return false
117
152
  end
118
153
 
119
154
  @mutex.synchronize do
@@ -125,18 +160,22 @@ class Darkroom
125
160
  Dir.glob(File.join(load_path, @@glob)).sort.each do |file|
126
161
  path = file.sub(load_path, '')
127
162
 
128
- if index = (path =~ INVALID_PATH)
163
+ if index = (path =~ Asset::INVALID_PATH_REGEX)
129
164
  @errors << InvalidPathError.new(path, index)
130
165
  elsif found.key?(path)
131
166
  @errors << DuplicateAssetError.new(path, found[path], load_path)
132
167
  else
133
168
  found[path] = load_path
134
169
 
135
- @manifest[path] ||= Asset.new(path, file, self,
136
- prefix: (@prefix unless @pristine.include?(path)),
137
- internal: @internal_pattern && path =~ @internal_pattern,
138
- minify: @minify && path !~ @minified_pattern,
139
- )
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
140
179
  end
141
180
  end
142
181
  end
@@ -148,13 +187,15 @@ class Darkroom
148
187
  @manifest.each do |path, asset|
149
188
  asset.process
150
189
 
151
- unless asset.internal?
190
+ if asset.entry?
152
191
  @manifest_unversioned[asset.path_unversioned] = asset
153
192
  @manifest_versioned[asset.path_versioned] = asset
154
193
  end
155
194
 
156
- @errors += asset.errors
195
+ @errors.concat(asset.errors)
157
196
  end
197
+
198
+ true
158
199
  ensure
159
200
  @last_processed_at = Time.now.to_f
160
201
  @error = @errors.empty? ? nil : ProcessingError.new(@errors)
@@ -162,12 +203,13 @@ class Darkroom
162
203
  end
163
204
 
164
205
  ##
165
- # 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.
166
208
  #
167
209
  def process!
168
- process
210
+ result = process
169
211
 
170
- raise(@error) if @error
212
+ (result && @error) ? raise(@error) : result
171
213
  end
172
214
 
173
215
  ##
@@ -186,7 +228,7 @@ class Darkroom
186
228
  # darkroom.asset('/assets/js/app.<hash>.js')
187
229
  # darkroom.asset('/assets/js/app.js')
188
230
  #
189
- # * +path+ - External path of the asset.
231
+ # [path] External path of the asset.
190
232
  #
191
233
  def asset(path)
192
234
  @manifest_versioned[path] || @manifest_unversioned[path]
@@ -203,8 +245,8 @@ class Darkroom
203
245
  #
204
246
  # Raises an AssetNotFoundError if the asset doesn't exist.
205
247
  #
206
- # * +path+ - Internal path of the asset.
207
- # * +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.
208
250
  #
209
251
  def asset_path(path, versioned: !@pristine.include?(path))
210
252
  asset = @manifest[path] or raise(AssetNotFoundError.new(path))
@@ -219,8 +261,8 @@ class Darkroom
219
261
  # Returns an asset's subresource integrity string. Raises an AssetNotFoundError if the asset doesn't
220
262
  # exist.
221
263
  #
222
- # * +path+ - Internal path of the asset.
223
- # * +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).
224
266
  #
225
267
  def asset_integrity(path, algorithm = nil)
226
268
  asset = @manifest[path] or raise(AssetNotFoundError.new(path))
@@ -231,7 +273,7 @@ class Darkroom
231
273
  ##
232
274
  # Returns the asset from the manifest hash associated with the given path.
233
275
  #
234
- # * +path+ - Internal path of the asset.
276
+ # [path] Internal path of the asset.
235
277
  #
236
278
  def manifest(path)
237
279
  @manifest[path]
@@ -241,14 +283,16 @@ class Darkroom
241
283
  # Writes assets to disk. This is useful when deploying to a production environment where assets will be
242
284
  # uploaded to and served from a CDN or proxy server.
243
285
  #
244
- # * +dir+ - Directory to write the assets to.
245
- # * +clear+ - Boolean indicating whether or not the existing contents of the directory should be deleted
246
- # before performing the dump.
247
- # * +include_pristine+ - Boolean indicating whether or not to include pristine assets (when dumping for
248
- # the purpose of uploading to a CDN, assets such as /robots.txt and /favicon.ico don't need to be
249
- # 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).
250
292
  #
251
293
  def dump(dir, clear: false, include_pristine: true)
294
+ raise(@error) if @error
295
+
252
296
  require('fileutils')
253
297
 
254
298
  dir = File.expand_path(dir)
@@ -257,7 +301,6 @@ class Darkroom
257
301
  Dir.each_child(dir) { |child| FileUtils.rm_rf(File.join(dir, child)) } if clear
258
302
 
259
303
  @manifest_versioned.each do |path, asset|
260
- next if asset.internal?
261
304
  next if @pristine.include?(asset.path) && !include_pristine
262
305
 
263
306
  file_path = File.join(dir,
@@ -274,17 +317,50 @@ class Darkroom
274
317
  #
275
318
  def inspect
276
319
  "#<#{self.class}: "\
320
+ "@entries=#{@entries.inspect}, "\
277
321
  "@errors=#{@errors.inspect}, "\
278
322
  "@hosts=#{@hosts.inspect}, "\
279
323
  "@internal_pattern=#{@internal_pattern.inspect}, "\
280
324
  "@last_processed_at=#{@last_processed_at.inspect}, "\
281
325
  "@load_paths=#{@load_paths.inspect}, "\
282
326
  "@min_process_interval=#{@min_process_interval.inspect}, "\
283
- "@minified_pattern=#{@minified_pattern.inspect}, "\
327
+ "@minified=#{@minified.inspect}, "\
284
328
  "@minify=#{@minify.inspect}, "\
285
329
  "@prefix=#{@prefix.inspect}, "\
286
330
  "@pristine=#{@pristine.inspect}, "\
287
331
  "@process_key=#{@process_key.inspect}"\
288
332
  '>'
289
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
290
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