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.
- 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
|