darkroom 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +3 -3
- data/VERSION +1 -1
- data/lib/darkroom/asset.rb +358 -249
- data/lib/darkroom/darkroom.rb +135 -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 -7
- 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 -4
- metadata +4 -3
data/lib/darkroom/asset.rb
CHANGED
@@ -4,7 +4,6 @@ require('base64')
|
|
4
4
|
require('digest')
|
5
5
|
require('set')
|
6
6
|
|
7
|
-
require_relative('darkroom')
|
8
7
|
require_relative('errors/asset_error')
|
9
8
|
require_relative('errors/asset_not_found_error')
|
10
9
|
require_relative('errors/circular_reference_error')
|
@@ -18,18 +17,16 @@ class Darkroom
|
|
18
17
|
#
|
19
18
|
class Asset
|
20
19
|
EXTENSION_REGEX = /(?=\.\w+)/.freeze
|
21
|
-
|
22
|
-
IMPORT_JOINER = "\n"
|
23
20
|
DEFAULT_QUOTE = '\''
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
21
|
+
DISALLOWED_PATH_CHARS = '\'"`=<>? '
|
22
|
+
INVALID_PATH_REGEX = /[#{DISALLOWED_PATH_CHARS}]/.freeze
|
23
|
+
PATH_REGEX = /(?<path>[^#{DISALLOWED_PATH_CHARS}]*)/.freeze
|
24
|
+
QUOTED_PATH_REGEX = /(?<quote>['"])#{PATH_REGEX.source}\k<quote>/.freeze
|
25
|
+
REFERENCE_REGEX = /
|
26
|
+
(?<quote>['"]?)
|
27
|
+
(?<quoted>#{PATH_REGEX.source}\?asset-(?<entity>path|content)(=(?<format>\w*))?)
|
28
|
+
\k<quote>
|
29
|
+
/x.freeze
|
33
30
|
|
34
31
|
# First item of each set is used as default, so order is important.
|
35
32
|
REFERENCE_FORMATS = {
|
@@ -37,94 +34,60 @@ class Darkroom
|
|
37
34
|
'content' => Set.new(%w[base64 utf8 displace]),
|
38
35
|
}.freeze
|
39
36
|
|
40
|
-
attr_reader(:
|
41
|
-
|
42
|
-
##
|
43
|
-
# Holds information about how to handle a particular asset type.
|
44
|
-
#
|
45
|
-
# * +content_type+ - HTTP MIME type string.
|
46
|
-
# * +import_regex+ - Regex to find import statements. Must contain a named component called 'path'
|
47
|
-
# (e.g. +/^import (?<path>.*)/+).
|
48
|
-
# * +reference_regex+ - Regex to find references to other assets. Must contain three named components:
|
49
|
-
# * +path+ - Path of the asset being referenced.
|
50
|
-
# * +entity+ - Desired entity (path or content).
|
51
|
-
# * +format+ - Format to use (see REFERENCE_FORMATS).
|
52
|
-
# * +validate_reference+ - Lambda to call to validate a reference. Should return nil if there are no
|
53
|
-
# errors and a string error message if validation fails. Three arguments are passed when called:
|
54
|
-
# * +asset+ - Asset object of the asset being referenced.
|
55
|
-
# * +match+ - MatchData object from the match against +reference_regex+.
|
56
|
-
# * +format+ - Format of the reference (see REFERENCE_FORMATS).
|
57
|
-
# * +reference_content+ - Lambda to call to get the content for a reference. Should return nil if the
|
58
|
-
# default behavior is desired or a string for custom content. Three arguments are passed when called:
|
59
|
-
# * +asset+ - Asset object of the asset being referenced.
|
60
|
-
# * +match+ - MatchData object from the match against +reference_regex+.
|
61
|
-
# * +format+ - Format of the reference (see REFERENCE_FORMATS).
|
62
|
-
# * +compile_lib+ - Name of a library to +require+ that is needed by the +compile+ lambda.
|
63
|
-
# * +compile+ - Lambda to call that will return the compiled version of the asset's content. Two
|
64
|
-
# arguments are passed when called:
|
65
|
-
# * +path+ - Path of the asset being compiled.
|
66
|
-
# * +content+ - Content to compile.
|
67
|
-
# * +minify_lib+ - Name of a library to +require+ that is needed by the +minify+ lambda.
|
68
|
-
# * +minify+ - Lambda to call that will return the minified version of the asset's content. One argument
|
69
|
-
# is passed when called:
|
70
|
-
# * +content+ - Content to minify.
|
71
|
-
#
|
72
|
-
Delegate = Struct.new(:content_type, :import_regex, :reference_regex, :validate_reference,
|
73
|
-
:reference_content, :compile_lib, :compile, :minify_lib, :minify, keyword_init: true)
|
37
|
+
attr_reader(:errors, :path, :path_unversioned)
|
74
38
|
|
75
39
|
##
|
76
40
|
# Creates a new instance.
|
77
41
|
#
|
78
|
-
#
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
83
|
-
#
|
84
|
-
#
|
42
|
+
# [file] Path of file on disk.
|
43
|
+
# [path] Path this asset will be referenced by (e.g. /js/app.js).
|
44
|
+
# [darkroom] Darkroom instance that the asset is a member of.
|
45
|
+
# [prefix:] Prefix to apply to unversioned and versioned paths.
|
46
|
+
# [entry:] Boolean indicating whether or not the asset is an entry point (i.e. accessible externally).
|
47
|
+
# [minify:] Boolean specifying whether or not the asset should be minified when processed.
|
48
|
+
# [intermediate:] Boolean indicating whether or not the asset exists solely to provide an intermediate
|
49
|
+
# form (e.g. compiled) to another asset instance.
|
85
50
|
#
|
86
|
-
def initialize(path, file, darkroom, prefix: nil, minify: false,
|
51
|
+
def initialize(path, file, darkroom, prefix: nil, entry: true, minify: false, intermediate: false)
|
87
52
|
@path = path
|
53
|
+
@dir = File.dirname(path)
|
88
54
|
@file = file
|
89
55
|
@darkroom = darkroom
|
90
56
|
@prefix = prefix
|
57
|
+
@entry = entry
|
91
58
|
@minify = minify
|
92
|
-
@internal = internal
|
93
59
|
|
94
60
|
@path_unversioned = "#{@prefix}#{@path}"
|
95
61
|
@extension = File.extname(@path).downcase
|
96
62
|
@delegate = Darkroom.delegate(@extension) or raise(UnrecognizedExtensionError.new(@path))
|
97
63
|
|
64
|
+
@keys = {}
|
65
|
+
|
66
|
+
if @delegate.compile_delegate && !intermediate
|
67
|
+
@delegate = @delegate.compile_delegate
|
68
|
+
@intermediate_asset = Asset.new(@path, @file, @darkroom,
|
69
|
+
prefix: @prefix,
|
70
|
+
entry: false,
|
71
|
+
minify: false,
|
72
|
+
intermediate: true,
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
98
76
|
require_libs
|
99
77
|
clear
|
100
78
|
end
|
101
79
|
|
102
80
|
##
|
103
|
-
# Processes the asset if modified (see #modified? for how modification is
|
104
|
-
# disk, references are substituted (if supported), content is compiled
|
105
|
-
# prefixed to its content (if supported), and content is minified
|
106
|
-
#
|
81
|
+
# Processes the asset if modified since the last run (see #modified? for how modification is
|
82
|
+
# determined). File is read from disk, references are substituted (if supported), content is compiled
|
83
|
+
# (if required), imports are prefixed to its content (if supported), and content is minified
|
84
|
+
# (if supported and enabled and the asset is an entry point).
|
107
85
|
#
|
108
86
|
def process
|
109
|
-
|
110
|
-
modified? ? (@processed = true) : (return @processed = false)
|
87
|
+
return if ran?(:process)
|
111
88
|
|
112
|
-
clear
|
113
|
-
read
|
114
|
-
build_imports
|
115
|
-
build_references
|
116
|
-
process_dependencies
|
117
89
|
compile
|
118
|
-
|
119
|
-
|
120
|
-
@fingerprint = Digest::MD5.hexdigest(@content)
|
121
|
-
@path_versioned = "#{@prefix}#{@path.sub(EXTENSION_REGEX, "-#{@fingerprint}")}"
|
122
|
-
|
123
|
-
@processed
|
124
|
-
rescue Errno::ENOENT
|
125
|
-
# File was deleted. Do nothing.
|
126
|
-
ensure
|
127
|
-
@error = @errors.empty? ? nil : ProcessingError.new(@errors)
|
90
|
+
content if entry?
|
128
91
|
end
|
129
92
|
|
130
93
|
##
|
@@ -159,49 +122,139 @@ class Darkroom
|
|
159
122
|
defined?(@is_image) ? @is_image : (@is_image = content_type.start_with?('image/'))
|
160
123
|
end
|
161
124
|
|
125
|
+
##
|
126
|
+
# Returns boolean indicating whether or not the asset is an entry point.
|
127
|
+
#
|
128
|
+
def entry?
|
129
|
+
@entry
|
130
|
+
end
|
131
|
+
|
132
|
+
##
|
133
|
+
# DEPRECATED: use #entry? instead. Returns boolean indicating whether or not the asset is marked as
|
134
|
+
# internal.
|
135
|
+
#
|
136
|
+
def internal?
|
137
|
+
Darkroom.deprecated("#{self.class.name}#internal? is deprecated: use #entry? instead")
|
138
|
+
|
139
|
+
!entry?
|
140
|
+
end
|
141
|
+
|
142
|
+
##
|
143
|
+
# Returns boolean indicating whether or not an error was encountered the last time the asset was
|
144
|
+
# processed.
|
145
|
+
#
|
146
|
+
def error?
|
147
|
+
!@errors.empty?
|
148
|
+
end
|
149
|
+
|
150
|
+
##
|
151
|
+
# Returns ProcessingError wrapper of all errors if any exist, or nil if there are none.
|
152
|
+
#
|
153
|
+
def error
|
154
|
+
@error ||= @errors.empty? ? nil : ProcessingError.new(@errors)
|
155
|
+
end
|
156
|
+
|
157
|
+
##
|
158
|
+
# Returns hash of content.
|
159
|
+
#
|
160
|
+
def fingerprint
|
161
|
+
content
|
162
|
+
|
163
|
+
@fingerprint
|
164
|
+
end
|
165
|
+
|
166
|
+
##
|
167
|
+
# Returns versioned path.
|
168
|
+
#
|
169
|
+
def path_versioned
|
170
|
+
content
|
171
|
+
|
172
|
+
@path_versioned
|
173
|
+
end
|
174
|
+
|
162
175
|
##
|
163
176
|
# Returns appropriate HTTP headers.
|
164
177
|
#
|
165
|
-
#
|
178
|
+
# [versioned:] Uses Cache-Control header with max-age if +true+ and ETag header if +false+.
|
166
179
|
#
|
167
180
|
def headers(versioned: true)
|
168
181
|
{
|
169
182
|
'Content-Type' => content_type,
|
170
183
|
'Cache-Control' => ('public, max-age=31536000' if versioned),
|
171
|
-
'ETag' => ("\"#{
|
184
|
+
'ETag' => ("\"#{fingerprint}\"" if !versioned),
|
172
185
|
}.compact!
|
173
186
|
end
|
174
187
|
|
175
188
|
##
|
176
189
|
# Returns subresource integrity string.
|
177
190
|
#
|
178
|
-
#
|
179
|
-
#
|
191
|
+
# [algorithm] Hash algorithm to use to generate the integrity string (one of +:sha256+, +:sha384+, or
|
192
|
+
# +:sha512+).
|
180
193
|
#
|
181
194
|
def integrity(algorithm = :sha384)
|
182
195
|
@integrity[algorithm] ||= "#{algorithm}-#{Base64.strict_encode64(
|
183
196
|
case algorithm
|
184
|
-
when :sha256 then Digest::SHA256.digest(
|
185
|
-
when :sha384 then Digest::SHA384.digest(
|
186
|
-
when :sha512 then Digest::SHA512.digest(
|
197
|
+
when :sha256 then Digest::SHA256.digest(content)
|
198
|
+
when :sha384 then Digest::SHA384.digest(content)
|
199
|
+
when :sha512 then Digest::SHA512.digest(content)
|
187
200
|
else raise("Unrecognized integrity algorithm: #{algorithm}")
|
188
201
|
end
|
189
202
|
)}".freeze
|
190
203
|
end
|
191
204
|
|
192
205
|
##
|
193
|
-
# Returns
|
206
|
+
# Returns full asset content.
|
194
207
|
#
|
195
|
-
|
196
|
-
@internal
|
197
|
-
end
|
198
|
-
|
199
|
-
##
|
200
|
-
# Returns boolean indicating whether or not an error was encountered the last time the asset was
|
201
|
-
# processed.
|
208
|
+
# [minified:] Boolean indicating whether or not to return minified version if it is available.
|
202
209
|
#
|
203
|
-
def
|
204
|
-
|
210
|
+
def content(minified: @minify)
|
211
|
+
unless ran?(:content)
|
212
|
+
compile
|
213
|
+
|
214
|
+
@content =
|
215
|
+
if imports.empty?
|
216
|
+
@own_content
|
217
|
+
else
|
218
|
+
(0..imports.size).inject(+'') do |content, i|
|
219
|
+
own_content = (imports[i] || self).own_content
|
220
|
+
|
221
|
+
content << "\n" unless (content[-1] == "\n" && own_content[0] == "\n") || content.empty?
|
222
|
+
content << own_content
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
begin
|
227
|
+
finalized = @delegate.finalize_handler&.call(
|
228
|
+
parse_data: @parse_data,
|
229
|
+
path: @path,
|
230
|
+
content: @content,
|
231
|
+
)
|
232
|
+
|
233
|
+
@content = finalized if finalized.kind_of?(String)
|
234
|
+
rescue => e
|
235
|
+
@errors << e
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
if @delegate.minify_handler && !@content_minified && (minified || @minify)
|
240
|
+
begin
|
241
|
+
@content_minified = @delegate.minify_handler.call(
|
242
|
+
parse_data: @parse_data,
|
243
|
+
path: @path,
|
244
|
+
content: @content,
|
245
|
+
)
|
246
|
+
rescue => e
|
247
|
+
@errors << e
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
@fingerprint ||= Digest::MD5.hexdigest((@minify && @content_minified) || @content).freeze
|
252
|
+
@path_versioned ||= "#{@prefix}#{@path.sub(EXTENSION_REGEX, "-#{@fingerprint}")}"
|
253
|
+
|
254
|
+
(minified && @content_minified) || @content
|
255
|
+
ensure
|
256
|
+
@content.freeze
|
257
|
+
@content_minified.freeze
|
205
258
|
end
|
206
259
|
|
207
260
|
##
|
@@ -209,11 +262,11 @@ class Darkroom
|
|
209
262
|
#
|
210
263
|
def inspect
|
211
264
|
"#<#{self.class}: "\
|
265
|
+
"@entry=#{@entry.inspect}, "\
|
212
266
|
"@errors=#{@errors.inspect}, "\
|
213
267
|
"@extension=#{@extension.inspect}, "\
|
214
268
|
"@file=#{@file.inspect}, "\
|
215
269
|
"@fingerprint=#{@fingerprint.inspect}, "\
|
216
|
-
"@internal=#{@internal.inspect}, "\
|
217
270
|
"@minify=#{@minify.inspect}, "\
|
218
271
|
"@mtime=#{@mtime.inspect}, "\
|
219
272
|
"@path=#{@path.inspect}, "\
|
@@ -230,204 +283,242 @@ class Darkroom
|
|
230
283
|
# error was recorded during the last processing run.
|
231
284
|
#
|
232
285
|
def modified?
|
233
|
-
|
286
|
+
return @modified if ran?(:modified)
|
234
287
|
|
235
288
|
begin
|
236
289
|
@modified = !!@error
|
237
|
-
@modified ||= @mtime != (@mtime = File.mtime(@file))
|
238
|
-
@modified ||=
|
290
|
+
@modified ||= (@mtime != (@mtime = File.mtime(@file)))
|
291
|
+
@modified ||= @intermediate_asset.modified? if @intermediate_asset
|
292
|
+
@modified ||= @dependencies.any? { |d| d.modified? } if @dependencies
|
293
|
+
@modified
|
239
294
|
rescue Errno::ENOENT
|
240
295
|
@modified = true
|
241
296
|
end
|
242
297
|
end
|
243
298
|
|
244
299
|
##
|
245
|
-
#
|
246
|
-
#
|
247
|
-
# * +ignore+ - Assets already accounted for as dependency tree is walked (to prevent infinite loops when
|
248
|
-
# circular chains are encountered).
|
300
|
+
# Clears content, dependencies, and errors so asset is ready for (re)processing.
|
249
301
|
#
|
250
|
-
def
|
251
|
-
return
|
302
|
+
def clear
|
303
|
+
return if ran?(:clear)
|
252
304
|
|
253
|
-
|
254
|
-
@
|
305
|
+
@own_dependencies = []
|
306
|
+
@own_imports = []
|
307
|
+
@parse_matches = []
|
308
|
+
@parse_data = {}
|
255
309
|
|
256
|
-
dependencies
|
310
|
+
@dependencies = nil
|
311
|
+
@imports = nil
|
312
|
+
|
313
|
+
@own_content = nil
|
314
|
+
@content = nil
|
315
|
+
@content_minified = nil
|
316
|
+
|
317
|
+
@fingerprint = nil
|
318
|
+
@path_versioned = nil
|
319
|
+
@integrity = {}
|
320
|
+
|
321
|
+
@error = nil
|
322
|
+
@errors = []
|
257
323
|
end
|
258
324
|
|
259
325
|
##
|
260
|
-
#
|
261
|
-
#
|
262
|
-
# * +ignore+ - Assets already accounted for as import tree is walked (to prevent infinite loops when
|
263
|
-
# circular chains are encountered).
|
326
|
+
# Reads the asset file into memory.
|
264
327
|
#
|
265
|
-
def
|
266
|
-
return
|
328
|
+
def read
|
329
|
+
return if ran?(:read)
|
267
330
|
|
268
|
-
|
269
|
-
@imports = imports unless ignore
|
331
|
+
clear
|
270
332
|
|
271
|
-
|
333
|
+
if @intermediate_asset
|
334
|
+
@own_content = @intermediate_asset.own_content.dup
|
335
|
+
@errors.concat(@intermediate_asset.errors)
|
336
|
+
else
|
337
|
+
begin
|
338
|
+
@own_content = File.read(@file)
|
339
|
+
rescue Errno::ENOENT
|
340
|
+
# Gracefully handle file deletion.
|
341
|
+
@own_content = ''
|
342
|
+
end
|
343
|
+
end
|
272
344
|
end
|
273
345
|
|
274
346
|
##
|
275
|
-
#
|
347
|
+
# Parses own content to build list of imports and references.
|
276
348
|
#
|
277
|
-
def
|
278
|
-
|
279
|
-
end
|
349
|
+
def parse
|
350
|
+
return if ran?(:parse)
|
280
351
|
|
281
|
-
|
352
|
+
read
|
353
|
+
|
354
|
+
@delegate.each_parser do |kind, regex, _|
|
355
|
+
@own_content.scan(regex) do
|
356
|
+
match = Regexp.last_match
|
357
|
+
asset = nil
|
358
|
+
|
359
|
+
if kind == :import || kind == :reference
|
360
|
+
path = File.expand_path(match[:path], @dir)
|
361
|
+
asset = @darkroom.manifest(path)
|
362
|
+
|
363
|
+
@own_dependencies << asset if asset
|
364
|
+
@own_imports << asset if asset && kind == :import
|
365
|
+
end
|
366
|
+
|
367
|
+
@parse_matches << [kind, match, asset]
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
282
371
|
|
283
372
|
##
|
284
|
-
#
|
373
|
+
# Returns direct dependencies (ones explicitly specified in the asset's own content.)
|
285
374
|
#
|
286
|
-
def
|
287
|
-
|
288
|
-
@imports = nil
|
289
|
-
@error = nil
|
290
|
-
@fingerprint = nil
|
291
|
-
@path_versioned = nil
|
375
|
+
def own_dependencies
|
376
|
+
parse
|
292
377
|
|
293
|
-
|
294
|
-
(@own_imports ||= []).clear
|
295
|
-
(@dependency_matches ||= []).clear
|
296
|
-
(@errors ||= []).clear
|
297
|
-
(@content ||= +'').clear
|
298
|
-
(@own_content ||= +'').clear
|
299
|
-
(@integrity ||= {}).clear
|
378
|
+
@own_dependencies
|
300
379
|
end
|
301
380
|
|
302
381
|
##
|
303
|
-
#
|
382
|
+
# Returns all dependencies (including dependencies of dependencies).
|
304
383
|
#
|
305
|
-
def
|
306
|
-
@
|
384
|
+
def dependencies
|
385
|
+
@dependencies = accumulate(:own_dependencies) unless ran?(:dependencies)
|
386
|
+
@dependencies
|
307
387
|
end
|
308
388
|
|
309
389
|
##
|
310
|
-
#
|
390
|
+
# Returns direct imports (ones explicitly specified in the asset's own content.)
|
311
391
|
#
|
312
|
-
def
|
313
|
-
|
392
|
+
def own_imports
|
393
|
+
parse
|
314
394
|
|
315
|
-
@
|
316
|
-
match = Regexp.last_match
|
317
|
-
path = match[:path]
|
318
|
-
format = match[:format]
|
319
|
-
format = REFERENCE_FORMATS[match[:entity]].first if format.nil? || format == ''
|
320
|
-
|
321
|
-
if (asset = @darkroom.manifest(path))
|
322
|
-
if !REFERENCE_FORMATS[match[:entity]].include?(format)
|
323
|
-
@errors << AssetError.new("Invalid reference format '#{format}' (must be one of "\
|
324
|
-
"'#{REFERENCE_FORMATS[match[:entity]].join("', '")}')", match[0], @path, line_num(match))
|
325
|
-
elsif match[:entity] == 'content' && format != 'base64' && asset.binary?
|
326
|
-
@errors << AssetError.new('Base64 encoding is required for binary assets', match[0], @path,
|
327
|
-
line_num(match))
|
328
|
-
elsif (error = @delegate.validate_reference&.(asset, match, format))
|
329
|
-
@errors << AssetError.new(error, match[0], @path, line_num(match))
|
330
|
-
else
|
331
|
-
@own_dependencies << asset
|
332
|
-
@dependency_matches << [:reference, asset, match, format]
|
333
|
-
end
|
334
|
-
else
|
335
|
-
@errors << not_found_error(path, match)
|
336
|
-
end
|
337
|
-
end
|
395
|
+
@own_imports
|
338
396
|
end
|
339
397
|
|
340
398
|
##
|
341
|
-
#
|
399
|
+
# Returns all imports (including imports of imports).
|
342
400
|
#
|
343
|
-
def
|
344
|
-
|
345
|
-
|
346
|
-
@own_content.scan(@delegate.import_regex) do
|
347
|
-
match = Regexp.last_match
|
348
|
-
path = match[:path]
|
349
|
-
|
350
|
-
if (asset = @darkroom.manifest(path))
|
351
|
-
@own_dependencies << asset
|
352
|
-
@own_imports << asset
|
353
|
-
@dependency_matches << [:import, asset, match]
|
354
|
-
else
|
355
|
-
@errors << not_found_error(path, match)
|
356
|
-
end
|
357
|
-
end
|
401
|
+
def imports
|
402
|
+
@imports = accumulate(:own_imports) unless ran?(:imports)
|
403
|
+
@imports
|
358
404
|
end
|
359
405
|
|
360
406
|
##
|
361
|
-
#
|
362
|
-
#
|
363
|
-
def
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
407
|
+
# Performs import and reference substitutions based on parse matches.
|
408
|
+
#
|
409
|
+
def substitute
|
410
|
+
return if ran?(:substitute)
|
411
|
+
|
412
|
+
parse
|
413
|
+
substitutions = []
|
414
|
+
|
415
|
+
@parse_matches.sort_by! { |_, match, __| match.begin(0) }.each do |kind, match, asset|
|
416
|
+
format = nil
|
417
|
+
|
418
|
+
handler = @delegate.handler(kind)
|
419
|
+
handler_args = {
|
420
|
+
parse_data: @parse_data,
|
421
|
+
match: match,
|
422
|
+
}
|
423
|
+
handler_args[:asset] = asset if asset
|
424
|
+
|
425
|
+
if !asset && (kind == :import || kind == :reference)
|
426
|
+
add_parse_error(match, AssetNotFoundError)
|
427
|
+
next
|
428
|
+
elsif kind == :reference
|
429
|
+
format = match[:format]
|
430
|
+
format = REFERENCE_FORMATS[match[:entity]].first if format.nil? || format == ''
|
431
|
+
|
432
|
+
handler_args[:format] = format
|
433
|
+
|
434
|
+
if asset.dependencies.include?(self)
|
435
|
+
add_parse_error(match, CircularReferenceError)
|
436
|
+
next
|
437
|
+
elsif !REFERENCE_FORMATS[match[:entity]].include?(format)
|
438
|
+
formats = REFERENCE_FORMATS[match[:entity]].join("', '")
|
439
|
+
add_parse_error(match, "Invalid reference format '#{format}' (must be one of '#{formats}')")
|
440
|
+
next
|
441
|
+
elsif match[:entity] == 'content' && format != 'base64' && asset.binary?
|
442
|
+
add_parse_error(match, 'Base64 encoding is required for binary assets')
|
443
|
+
next
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
error = catch(:error) do
|
448
|
+
substitution, start, finish = handler&.call(**handler_args)
|
449
|
+
|
371
450
|
min_start, max_finish = match.offset(0)
|
372
|
-
start ||= format == 'displace' ? min_start : match.begin(:quoted)
|
373
|
-
finish ||= format == 'displace' ? max_finish : match.end(:quoted)
|
451
|
+
start ||= (!format || format == 'displace') ? min_start : match.begin(:quoted)
|
452
|
+
finish ||= (!format || format == 'displace') ? max_finish : match.end(:quoted)
|
374
453
|
start = [[start, min_start].max, max_finish].min
|
375
454
|
finish = [[finish, max_finish].min, min_start].max
|
376
455
|
|
377
|
-
|
456
|
+
if kind == :reference
|
378
457
|
case "#{match[:entity]}-#{format}"
|
379
458
|
when 'path-versioned'
|
380
|
-
|
459
|
+
substitution ||= asset.path_versioned
|
381
460
|
when 'path-unversioned'
|
382
|
-
|
461
|
+
substitution ||= asset.path_unversioned
|
383
462
|
when 'content-base64'
|
384
463
|
quote = DEFAULT_QUOTE if match[:quote] == ''
|
385
|
-
data = Base64.strict_encode64(
|
386
|
-
"#{quote}data:#{asset.content_type};base64,#{data}#{quote}"
|
464
|
+
data = Base64.strict_encode64(substitution || asset.content)
|
465
|
+
substitution = "#{quote}data:#{asset.content_type};base64,#{data}#{quote}"
|
387
466
|
when 'content-utf8'
|
388
467
|
quote = DEFAULT_QUOTE if match[:quote] == ''
|
389
|
-
|
468
|
+
data = substitution || asset.content
|
469
|
+
substitution = "#{quote}data:#{asset.content_type};utf8,#{data}#{quote}"
|
390
470
|
when 'content-displace'
|
391
|
-
|
471
|
+
substitution ||= asset.content
|
392
472
|
end
|
473
|
+
end
|
474
|
+
|
475
|
+
substitutions << [substitution || '', start, finish]
|
476
|
+
nil
|
393
477
|
end
|
478
|
+
|
479
|
+
add_parse_error(match, error) if error
|
394
480
|
end
|
395
481
|
|
396
|
-
|
482
|
+
substitutions.reverse_each do |content, start, finish|
|
483
|
+
@own_content[start...finish] = content
|
484
|
+
end
|
397
485
|
end
|
398
486
|
|
399
487
|
##
|
400
|
-
# Compiles the asset if compilation is supported for the asset's type
|
401
|
-
# content to the overall content string.
|
488
|
+
# Compiles the asset if compilation is supported for the asset's type.
|
402
489
|
#
|
403
490
|
def compile
|
404
|
-
if
|
405
|
-
|
406
|
-
|
407
|
-
rescue => e
|
408
|
-
@errors << e
|
409
|
-
end
|
410
|
-
end
|
491
|
+
return if ran?(:compile)
|
492
|
+
|
493
|
+
substitute
|
411
494
|
|
412
|
-
|
495
|
+
begin
|
496
|
+
compiled = @delegate.compile_handler&.call(
|
497
|
+
parse_data: @parse_data,
|
498
|
+
path: @path,
|
499
|
+
own_content: @own_content
|
500
|
+
)
|
501
|
+
|
502
|
+
@own_content = compiled if compiled.kind_of?(String)
|
503
|
+
rescue => e
|
504
|
+
@errors << e
|
505
|
+
end
|
506
|
+
ensure
|
507
|
+
@own_content.freeze
|
508
|
+
dependencies # Ensure dependency array gets built.
|
413
509
|
end
|
414
510
|
|
415
511
|
##
|
416
|
-
#
|
417
|
-
# (i.e. it's not already minified), and the asset is not marked as internal-only.
|
512
|
+
# Returns the processed content of the asset without dependencies concatenated.
|
418
513
|
#
|
419
|
-
def
|
420
|
-
|
421
|
-
begin
|
422
|
-
@content = @delegate.minify.(@content)
|
423
|
-
rescue => e
|
424
|
-
@errors << e
|
425
|
-
end
|
426
|
-
end
|
514
|
+
def own_content
|
515
|
+
compile
|
427
516
|
|
428
|
-
@
|
517
|
+
@own_content
|
429
518
|
end
|
430
519
|
|
520
|
+
private
|
521
|
+
|
431
522
|
##
|
432
523
|
# Requires any libraries necessary for compiling and minifying the asset based on its type. Raises a
|
433
524
|
# MissingLibraryError if library cannot be loaded.
|
@@ -455,46 +546,64 @@ class Darkroom
|
|
455
546
|
end
|
456
547
|
|
457
548
|
##
|
458
|
-
#
|
549
|
+
# Returns boolean indicating if a method has already been run during the current round of processing.
|
459
550
|
#
|
460
|
-
#
|
461
|
-
# * +ignore+ - Set of assets already accumulated which can be ignored (used to avoid infinite loops when
|
462
|
-
# circular references are encountered).
|
551
|
+
# [name] Name of the method.
|
463
552
|
#
|
464
|
-
def
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
instance_variable_get(:"@own_#{name}").each_with_object([]) do |asset, assets|
|
471
|
-
next if ignore.include?(asset)
|
472
|
-
|
473
|
-
asset.process
|
474
|
-
assets.push(*asset.send(name, ignore), asset)
|
475
|
-
assets.uniq!
|
476
|
-
assets.delete(self)
|
553
|
+
def ran?(name)
|
554
|
+
if @keys[name] == @darkroom.process_key
|
555
|
+
true
|
556
|
+
else
|
557
|
+
@keys[name] = @darkroom.process_key
|
558
|
+
name == :modified ? false : !modified?
|
477
559
|
end
|
478
560
|
end
|
479
561
|
|
480
562
|
##
|
481
|
-
# Utility method
|
563
|
+
# Utility method used by #dependencies and #imports to recursively build arrays.
|
482
564
|
#
|
483
|
-
#
|
484
|
-
# * +match+ - MatchData object of the regex for the asset that cannot be found.
|
565
|
+
# [name] Name of the array to accumulate (:dependencies or :imports).
|
485
566
|
#
|
486
|
-
def
|
487
|
-
|
488
|
-
|
567
|
+
def accumulate(name)
|
568
|
+
done = Set.new
|
569
|
+
assets = [self]
|
570
|
+
index = 0
|
571
|
+
|
572
|
+
while index
|
573
|
+
asset = assets[index]
|
574
|
+
done << asset
|
575
|
+
additional = asset.send(name).reject { |a| done.include?(a) }
|
576
|
+
assets.insert(index, *additional).uniq!
|
577
|
+
index = assets.index { |a| !done.include?(a) }
|
578
|
+
end
|
579
|
+
|
580
|
+
assets.delete(self)
|
581
|
+
assets
|
489
582
|
end
|
490
583
|
|
491
584
|
##
|
492
|
-
# Utility method
|
493
|
-
#
|
494
|
-
#
|
495
|
-
#
|
496
|
-
|
497
|
-
|
585
|
+
# Utility method to create a parse error of the appropriate class and append it to the errors array.
|
586
|
+
#
|
587
|
+
# [match] MatchData object for where the parse error occurred.
|
588
|
+
# [error] Error class or message.
|
589
|
+
#
|
590
|
+
def add_parse_error(match, error)
|
591
|
+
klass = error
|
592
|
+
args = []
|
593
|
+
|
594
|
+
if error == AssetNotFoundError
|
595
|
+
klass = UnrecognizedExtensionError unless Darkroom.delegate(File.extname(match[:path]))
|
596
|
+
args << match[:path]
|
597
|
+
else
|
598
|
+
if error.kind_of?(String)
|
599
|
+
klass = AssetError
|
600
|
+
args << error
|
601
|
+
end
|
602
|
+
|
603
|
+
args << match[0].strip
|
604
|
+
end
|
605
|
+
|
606
|
+
@errors << klass.new(*args, @path, @own_content[0..match.begin(:path)].count("\n") + 1)
|
498
607
|
end
|
499
608
|
end
|
500
609
|
end
|