darkroom 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +6 -6
- data/VERSION +1 -1
- data/lib/darkroom/asset.rb +367 -240
- data/lib/darkroom/darkroom.rb +141 -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 -9
- 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 -13
- metadata +4 -3
data/lib/darkroom/asset.rb
CHANGED
@@ -4,7 +4,12 @@ require('base64')
|
|
4
4
|
require('digest')
|
5
5
|
require('set')
|
6
6
|
|
7
|
-
require_relative('
|
7
|
+
require_relative('errors/asset_error')
|
8
|
+
require_relative('errors/asset_not_found_error')
|
9
|
+
require_relative('errors/circular_reference_error')
|
10
|
+
require_relative('errors/missing_library_error')
|
11
|
+
require_relative('errors/processing_error')
|
12
|
+
require_relative('errors/unrecognized_extension_error')
|
8
13
|
|
9
14
|
class Darkroom
|
10
15
|
##
|
@@ -12,18 +17,16 @@ class Darkroom
|
|
12
17
|
#
|
13
18
|
class Asset
|
14
19
|
EXTENSION_REGEX = /(?=\.\w+)/.freeze
|
15
|
-
|
16
|
-
IMPORT_JOINER = "\n"
|
17
20
|
DEFAULT_QUOTE = '\''
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
27
30
|
|
28
31
|
# First item of each set is used as default, so order is important.
|
29
32
|
REFERENCE_FORMATS = {
|
@@ -31,94 +34,60 @@ class Darkroom
|
|
31
34
|
'content' => Set.new(%w[base64 utf8 displace]),
|
32
35
|
}.freeze
|
33
36
|
|
34
|
-
attr_reader(:
|
35
|
-
|
36
|
-
##
|
37
|
-
# Holds information about how to handle a particular asset type.
|
38
|
-
#
|
39
|
-
# * +content_type+ - HTTP MIME type string.
|
40
|
-
# * +import_regex+ - Regex to find import statements. Must contain a named component called 'path'
|
41
|
-
# (e.g. +/^import (?<path>.*)/+).
|
42
|
-
# * +reference_regex+ - Regex to find references to other assets. Must contain three named components:
|
43
|
-
# * +path+ - Path of the asset being referenced.
|
44
|
-
# * +entity+ - Desired entity (path or content).
|
45
|
-
# * +format+ - Format to use (see REFERENCE_FORMATS).
|
46
|
-
# * +validate_reference+ - Lambda to call to validate a reference. Should return nil if there are no
|
47
|
-
# errors and a string error message if validation fails. Three arguments are passed when called:
|
48
|
-
# * +asset+ - Asset object of the asset being referenced.
|
49
|
-
# * +match+ - MatchData object from the match against +reference_regex+.
|
50
|
-
# * +format+ - Format of the reference (see REFERENCE_FORMATS).
|
51
|
-
# * +reference_content+ - Lambda to call to get the content for a reference. Should return nil if the
|
52
|
-
# default behavior is desired or a string for custom content. Three arguments are passed when called:
|
53
|
-
# * +asset+ - Asset object of the asset being referenced.
|
54
|
-
# * +match+ - MatchData object from the match against +reference_regex+.
|
55
|
-
# * +format+ - Format of the reference (see REFERENCE_FORMATS).
|
56
|
-
# * +compile_lib+ - Name of a library to +require+ that is needed by the +compile+ lambda.
|
57
|
-
# * +compile+ - Lambda to call that will return the compiled version of the asset's content. Two
|
58
|
-
# arguments are passed when called:
|
59
|
-
# * +path+ - Path of the asset being compiled.
|
60
|
-
# * +content+ - Content to compile.
|
61
|
-
# * +minify_lib+ - Name of a library to +require+ that is needed by the +minify+ lambda.
|
62
|
-
# * +minify+ - Lambda to call that will return the minified version of the asset's content. One argument
|
63
|
-
# is passed when called:
|
64
|
-
# * +content+ - Content to minify.
|
65
|
-
#
|
66
|
-
Delegate = Struct.new(:content_type, :import_regex, :reference_regex, :validate_reference,
|
67
|
-
:reference_content, :compile_lib, :compile, :minify_lib, :minify, keyword_init: true)
|
37
|
+
attr_reader(:errors, :path, :path_unversioned)
|
68
38
|
|
69
39
|
##
|
70
40
|
# Creates a new instance.
|
71
41
|
#
|
72
|
-
#
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
76
|
-
#
|
77
|
-
#
|
78
|
-
#
|
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.
|
79
50
|
#
|
80
|
-
def initialize(path, file, darkroom, prefix: nil, minify: false,
|
51
|
+
def initialize(path, file, darkroom, prefix: nil, entry: true, minify: false, intermediate: false)
|
81
52
|
@path = path
|
53
|
+
@dir = File.dirname(path)
|
82
54
|
@file = file
|
83
55
|
@darkroom = darkroom
|
84
56
|
@prefix = prefix
|
57
|
+
@entry = entry
|
85
58
|
@minify = minify
|
86
|
-
@internal = internal
|
87
59
|
|
88
60
|
@path_unversioned = "#{@prefix}#{@path}"
|
89
61
|
@extension = File.extname(@path).downcase
|
90
62
|
@delegate = Darkroom.delegate(@extension) or raise(UnrecognizedExtensionError.new(@path))
|
91
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
|
+
|
92
76
|
require_libs
|
93
77
|
clear
|
94
78
|
end
|
95
79
|
|
96
80
|
##
|
97
|
-
# Processes the asset if modified (see #modified? for how modification is
|
98
|
-
# disk, references are substituted (if supported), content is compiled
|
99
|
-
# prefixed to its content (if supported), and content is minified
|
100
|
-
#
|
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).
|
101
85
|
#
|
102
86
|
def process
|
103
|
-
|
104
|
-
modified? ? (@processed = true) : (return @processed = false)
|
87
|
+
return if ran?(:process)
|
105
88
|
|
106
|
-
clear
|
107
|
-
read
|
108
|
-
build_imports
|
109
|
-
build_references
|
110
|
-
process_dependencies
|
111
89
|
compile
|
112
|
-
|
113
|
-
|
114
|
-
@fingerprint = Digest::MD5.hexdigest(@content)
|
115
|
-
@path_versioned = "#{@prefix}#{@path.sub(EXTENSION_REGEX, "-#{@fingerprint}")}"
|
116
|
-
|
117
|
-
@processed
|
118
|
-
rescue Errno::ENOENT
|
119
|
-
# File was deleted. Do nothing.
|
120
|
-
ensure
|
121
|
-
@error = @errors.empty? ? nil : ProcessingError.new(@errors)
|
90
|
+
content if entry?
|
122
91
|
end
|
123
92
|
|
124
93
|
##
|
@@ -153,49 +122,139 @@ class Darkroom
|
|
153
122
|
defined?(@is_image) ? @is_image : (@is_image = content_type.start_with?('image/'))
|
154
123
|
end
|
155
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
|
+
|
156
175
|
##
|
157
176
|
# Returns appropriate HTTP headers.
|
158
177
|
#
|
159
|
-
#
|
178
|
+
# [versioned:] Uses Cache-Control header with max-age if +true+ and ETag header if +false+.
|
160
179
|
#
|
161
180
|
def headers(versioned: true)
|
162
181
|
{
|
163
182
|
'Content-Type' => content_type,
|
164
183
|
'Cache-Control' => ('public, max-age=31536000' if versioned),
|
165
|
-
'ETag' => ("\"#{
|
184
|
+
'ETag' => ("\"#{fingerprint}\"" if !versioned),
|
166
185
|
}.compact!
|
167
186
|
end
|
168
187
|
|
169
188
|
##
|
170
189
|
# Returns subresource integrity string.
|
171
190
|
#
|
172
|
-
#
|
173
|
-
#
|
191
|
+
# [algorithm] Hash algorithm to use to generate the integrity string (one of +:sha256+, +:sha384+, or
|
192
|
+
# +:sha512+).
|
174
193
|
#
|
175
194
|
def integrity(algorithm = :sha384)
|
176
195
|
@integrity[algorithm] ||= "#{algorithm}-#{Base64.strict_encode64(
|
177
196
|
case algorithm
|
178
|
-
when :sha256 then Digest::SHA256.digest(
|
179
|
-
when :sha384 then Digest::SHA384.digest(
|
180
|
-
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)
|
181
200
|
else raise("Unrecognized integrity algorithm: #{algorithm}")
|
182
201
|
end
|
183
202
|
)}".freeze
|
184
203
|
end
|
185
204
|
|
186
205
|
##
|
187
|
-
# Returns
|
206
|
+
# Returns full asset content.
|
188
207
|
#
|
189
|
-
|
190
|
-
@internal
|
191
|
-
end
|
192
|
-
|
193
|
-
##
|
194
|
-
# Returns boolean indicating whether or not an error was encountered the last time the asset was
|
195
|
-
# processed.
|
208
|
+
# [minified:] Boolean indicating whether or not to return minified version if it is available.
|
196
209
|
#
|
197
|
-
def
|
198
|
-
|
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
|
199
258
|
end
|
200
259
|
|
201
260
|
##
|
@@ -203,11 +262,11 @@ class Darkroom
|
|
203
262
|
#
|
204
263
|
def inspect
|
205
264
|
"#<#{self.class}: "\
|
265
|
+
"@entry=#{@entry.inspect}, "\
|
206
266
|
"@errors=#{@errors.inspect}, "\
|
207
267
|
"@extension=#{@extension.inspect}, "\
|
208
268
|
"@file=#{@file.inspect}, "\
|
209
269
|
"@fingerprint=#{@fingerprint.inspect}, "\
|
210
|
-
"@internal=#{@internal.inspect}, "\
|
211
270
|
"@minify=#{@minify.inspect}, "\
|
212
271
|
"@mtime=#{@mtime.inspect}, "\
|
213
272
|
"@path=#{@path.inspect}, "\
|
@@ -224,194 +283,242 @@ class Darkroom
|
|
224
283
|
# error was recorded during the last processing run.
|
225
284
|
#
|
226
285
|
def modified?
|
227
|
-
|
286
|
+
return @modified if ran?(:modified)
|
228
287
|
|
229
288
|
begin
|
230
289
|
@modified = !!@error
|
231
|
-
@modified ||= @mtime != (@mtime = File.mtime(@file))
|
232
|
-
@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
|
233
294
|
rescue Errno::ENOENT
|
234
295
|
@modified = true
|
235
296
|
end
|
236
297
|
end
|
237
298
|
|
238
|
-
##
|
239
|
-
# Returns all dependencies (including dependencies of dependencies).
|
240
|
-
#
|
241
|
-
# * +ignore+ - Assets already accounted for as dependency tree is walked (to prevent infinite loops when
|
242
|
-
# circular chains are encountered).
|
243
|
-
#
|
244
|
-
def dependencies(ignore = Set.new)
|
245
|
-
@dependencies ||= accumulate(:dependencies, ignore)
|
246
|
-
end
|
247
|
-
|
248
|
-
##
|
249
|
-
# Returns all imports (including imports of imports).
|
250
|
-
#
|
251
|
-
# * +ignore+ - Assets already accounted for as import tree is walked (to prevent infinite loops when
|
252
|
-
# circular chains are encountered).
|
253
|
-
#
|
254
|
-
def imports(ignore = Set.new)
|
255
|
-
@imports ||= accumulate(:imports, ignore)
|
256
|
-
end
|
257
|
-
|
258
|
-
##
|
259
|
-
# Returns the processed content of the asset without dependencies concatenated.
|
260
|
-
#
|
261
|
-
def own_content
|
262
|
-
@own_content
|
263
|
-
end
|
264
|
-
|
265
|
-
private
|
266
|
-
|
267
299
|
##
|
268
300
|
# Clears content, dependencies, and errors so asset is ready for (re)processing.
|
269
301
|
#
|
270
302
|
def clear
|
303
|
+
return if ran?(:clear)
|
304
|
+
|
305
|
+
@own_dependencies = []
|
306
|
+
@own_imports = []
|
307
|
+
@parse_matches = []
|
308
|
+
@parse_data = {}
|
309
|
+
|
271
310
|
@dependencies = nil
|
272
311
|
@imports = nil
|
273
|
-
|
312
|
+
|
313
|
+
@own_content = nil
|
314
|
+
@content = nil
|
315
|
+
@content_minified = nil
|
316
|
+
|
274
317
|
@fingerprint = nil
|
275
318
|
@path_versioned = nil
|
319
|
+
@integrity = {}
|
276
320
|
|
277
|
-
|
278
|
-
|
279
|
-
(@dependency_matches ||= []).clear
|
280
|
-
(@errors ||= []).clear
|
281
|
-
(@content ||= +'').clear
|
282
|
-
(@own_content ||= +'').clear
|
283
|
-
(@integrity ||= {}).clear
|
321
|
+
@error = nil
|
322
|
+
@errors = []
|
284
323
|
end
|
285
324
|
|
286
325
|
##
|
287
326
|
# Reads the asset file into memory.
|
288
327
|
#
|
289
328
|
def read
|
290
|
-
|
329
|
+
return if ran?(:read)
|
330
|
+
|
331
|
+
clear
|
332
|
+
|
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
|
291
344
|
end
|
292
345
|
|
293
346
|
##
|
294
|
-
#
|
347
|
+
# Parses own content to build list of imports and references.
|
295
348
|
#
|
296
|
-
def
|
297
|
-
return
|
349
|
+
def parse
|
350
|
+
return if ran?(:parse)
|
351
|
+
|
352
|
+
read
|
298
353
|
|
299
|
-
@
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
format = REFERENCE_FORMATS[match[:entity]].first if format.nil? || format == ''
|
354
|
+
@delegate.each_parser do |kind, regex, _|
|
355
|
+
@own_content.scan(regex) do
|
356
|
+
match = Regexp.last_match
|
357
|
+
asset = nil
|
304
358
|
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
@
|
311
|
-
line_num(match))
|
312
|
-
elsif (error = @delegate.validate_reference&.(asset, match, format))
|
313
|
-
@errors << AssetError.new(error, match[0], @path, line_num(match))
|
314
|
-
else
|
315
|
-
@own_dependencies << asset
|
316
|
-
@dependency_matches << [:reference, asset, match, format]
|
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
|
317
365
|
end
|
318
|
-
|
319
|
-
@
|
366
|
+
|
367
|
+
@parse_matches << [kind, match, asset]
|
320
368
|
end
|
321
369
|
end
|
322
370
|
end
|
323
371
|
|
324
372
|
##
|
325
|
-
#
|
373
|
+
# Returns direct dependencies (ones explicitly specified in the asset's own content.)
|
326
374
|
#
|
327
|
-
def
|
328
|
-
|
375
|
+
def own_dependencies
|
376
|
+
parse
|
329
377
|
|
330
|
-
@
|
331
|
-
|
332
|
-
path = match[:path]
|
378
|
+
@own_dependencies
|
379
|
+
end
|
333
380
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
381
|
+
##
|
382
|
+
# Returns all dependencies (including dependencies of dependencies).
|
383
|
+
#
|
384
|
+
def dependencies
|
385
|
+
@dependencies = accumulate(:own_dependencies) unless ran?(:dependencies)
|
386
|
+
@dependencies
|
387
|
+
end
|
388
|
+
|
389
|
+
##
|
390
|
+
# Returns direct imports (ones explicitly specified in the asset's own content.)
|
391
|
+
#
|
392
|
+
def own_imports
|
393
|
+
parse
|
394
|
+
|
395
|
+
@own_imports
|
396
|
+
end
|
397
|
+
|
398
|
+
##
|
399
|
+
# Returns all imports (including imports of imports).
|
400
|
+
#
|
401
|
+
def imports
|
402
|
+
@imports = accumulate(:own_imports) unless ran?(:imports)
|
403
|
+
@imports
|
342
404
|
end
|
343
405
|
|
344
406
|
##
|
345
|
-
#
|
346
|
-
#
|
347
|
-
def
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
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
|
+
|
355
450
|
min_start, max_finish = match.offset(0)
|
356
|
-
start ||= format == 'displace' ? min_start : match.begin(:quoted)
|
357
|
-
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)
|
358
453
|
start = [[start, min_start].max, max_finish].min
|
359
454
|
finish = [[finish, max_finish].min, min_start].max
|
360
455
|
|
361
|
-
|
456
|
+
if kind == :reference
|
362
457
|
case "#{match[:entity]}-#{format}"
|
363
458
|
when 'path-versioned'
|
364
|
-
|
459
|
+
substitution ||= asset.path_versioned
|
365
460
|
when 'path-unversioned'
|
366
|
-
|
461
|
+
substitution ||= asset.path_unversioned
|
367
462
|
when 'content-base64'
|
368
463
|
quote = DEFAULT_QUOTE if match[:quote] == ''
|
369
|
-
data = Base64.strict_encode64(
|
370
|
-
"#{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}"
|
371
466
|
when 'content-utf8'
|
372
467
|
quote = DEFAULT_QUOTE if match[:quote] == ''
|
373
|
-
|
468
|
+
data = substitution || asset.content
|
469
|
+
substitution = "#{quote}data:#{asset.content_type};utf8,#{data}#{quote}"
|
374
470
|
when 'content-displace'
|
375
|
-
|
471
|
+
substitution ||= asset.content
|
376
472
|
end
|
473
|
+
end
|
474
|
+
|
475
|
+
substitutions << [substitution || '', start, finish]
|
476
|
+
nil
|
377
477
|
end
|
478
|
+
|
479
|
+
add_parse_error(match, error) if error
|
378
480
|
end
|
379
481
|
|
380
|
-
|
482
|
+
substitutions.reverse_each do |content, start, finish|
|
483
|
+
@own_content[start...finish] = content
|
484
|
+
end
|
381
485
|
end
|
382
486
|
|
383
487
|
##
|
384
|
-
# Compiles the asset if compilation is supported for the asset's type
|
385
|
-
# content to the overall content string.
|
488
|
+
# Compiles the asset if compilation is supported for the asset's type.
|
386
489
|
#
|
387
490
|
def compile
|
388
|
-
if
|
389
|
-
begin
|
390
|
-
@own_content = @delegate.compile.(@path, @own_content)
|
391
|
-
rescue => e
|
392
|
-
@errors << e
|
393
|
-
end
|
394
|
-
end
|
491
|
+
return if ran?(:compile)
|
395
492
|
|
396
|
-
|
493
|
+
substitute
|
494
|
+
|
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.
|
397
509
|
end
|
398
510
|
|
399
511
|
##
|
400
|
-
#
|
401
|
-
# (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.
|
402
513
|
#
|
403
|
-
def
|
404
|
-
|
405
|
-
begin
|
406
|
-
@content = @delegate.minify.(@content)
|
407
|
-
rescue => e
|
408
|
-
@errors << e
|
409
|
-
end
|
410
|
-
end
|
514
|
+
def own_content
|
515
|
+
compile
|
411
516
|
|
412
|
-
@
|
517
|
+
@own_content
|
413
518
|
end
|
414
519
|
|
520
|
+
private
|
521
|
+
|
415
522
|
##
|
416
523
|
# Requires any libraries necessary for compiling and minifying the asset based on its type. Raises a
|
417
524
|
# MissingLibraryError if library cannot be loaded.
|
@@ -419,7 +526,7 @@ class Darkroom
|
|
419
526
|
# Darkroom does not explicitly depend on any libraries necessary for asset compilation or minification,
|
420
527
|
# since not every app will use every kind of asset or use minification. It is instead up to each app
|
421
528
|
# using Darkroom to specify any needed compilation and minification libraries as direct dependencies
|
422
|
-
# (e.g. specify +gem('
|
529
|
+
# (e.g. specify +gem('terser')+ in the app's Gemfile if JavaScript minification is desired).
|
423
530
|
#
|
424
531
|
def require_libs
|
425
532
|
begin
|
@@ -439,44 +546,64 @@ class Darkroom
|
|
439
546
|
end
|
440
547
|
|
441
548
|
##
|
442
|
-
#
|
549
|
+
# Returns boolean indicating if a method has already been run during the current round of processing.
|
443
550
|
#
|
444
|
-
#
|
445
|
-
# * +ignore+ - Set of assets already accumulated which can be ignored (used to avoid infinite loops when
|
446
|
-
# circular references are encountered).
|
551
|
+
# [name] Name of the method.
|
447
552
|
#
|
448
|
-
def
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
asset.process
|
456
|
-
assets.push(*asset.send(name, ignore), asset)
|
457
|
-
assets.uniq!
|
458
|
-
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?
|
459
559
|
end
|
460
560
|
end
|
461
561
|
|
462
562
|
##
|
463
|
-
# Utility method
|
563
|
+
# Utility method used by #dependencies and #imports to recursively build arrays.
|
464
564
|
#
|
465
|
-
#
|
466
|
-
# * +match+ - MatchData object of the regex for the asset that cannot be found.
|
565
|
+
# [name] Name of the array to accumulate (:dependencies or :imports).
|
467
566
|
#
|
468
|
-
def
|
469
|
-
|
470
|
-
|
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
|
471
582
|
end
|
472
583
|
|
473
584
|
##
|
474
|
-
# Utility method
|
475
|
-
#
|
476
|
-
#
|
477
|
-
#
|
478
|
-
|
479
|
-
|
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)
|
480
607
|
end
|
481
608
|
end
|
482
609
|
end
|