darkroom 0.0.6 → 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.
@@ -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