darkroom 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +127 -41
- data/VERSION +1 -1
- data/lib/darkroom.rb +21 -28
- data/lib/darkroom/asset.rb +247 -82
- data/lib/darkroom/darkroom.rb +54 -13
- data/lib/darkroom/delegates/css.rb +39 -0
- data/lib/darkroom/delegates/html.rb +53 -0
- data/lib/darkroom/delegates/htx.rb +21 -0
- data/lib/darkroom/delegates/javascript.rb +16 -0
- data/lib/darkroom/errors/asset_error.rb +33 -0
- data/lib/darkroom/errors/asset_not_found_error.rb +10 -21
- data/lib/darkroom/errors/circular_reference_error.rb +21 -0
- data/lib/darkroom/errors/duplicate_asset_error.rb +4 -4
- data/lib/darkroom/errors/invalid_path_error.rb +28 -0
- data/lib/darkroom/errors/missing_library_error.rb +3 -3
- data/lib/darkroom/errors/unrecognized_extension_error.rb +21 -0
- data/lib/darkroom/version.rb +1 -1
- metadata +10 -3
- data/lib/darkroom/errors/spec_not_defined_error.rb +0 -28
data/lib/darkroom/darkroom.rb
CHANGED
@@ -11,10 +11,47 @@ class Darkroom
|
|
11
11
|
PRISTINE = Set.new(%w[/favicon.ico /mask-icon.svg /humans.txt /robots.txt]).freeze
|
12
12
|
MIN_PROCESS_INTERVAL = 0.5
|
13
13
|
|
14
|
+
DISALLOWED_PATH_CHARS = '\'"`=<>? '
|
15
|
+
INVALID_PATH = /[#{DISALLOWED_PATH_CHARS}]/.freeze
|
14
16
|
TRAILING_SLASHES = /\/+$/.freeze
|
15
17
|
|
18
|
+
@@delegates = {}
|
19
|
+
@@glob = ''
|
20
|
+
|
16
21
|
attr_reader(:error, :errors, :process_key)
|
17
22
|
|
23
|
+
##
|
24
|
+
# Registers an asset delegate.
|
25
|
+
#
|
26
|
+
# * +delegate+ - An HTTP MIME type string, a Hash of Delegate parameters, or a Delegate instance.
|
27
|
+
# * +extensions+ - File extension(s) to associate with this delegate.
|
28
|
+
#
|
29
|
+
def self.register(*extensions, delegate)
|
30
|
+
case delegate
|
31
|
+
when String
|
32
|
+
delegate = Asset::Delegate.new(content_type: delegate.freeze)
|
33
|
+
when Hash
|
34
|
+
delegate = Asset::Delegate.new(**delegate)
|
35
|
+
end
|
36
|
+
|
37
|
+
extensions.each do |extension|
|
38
|
+
@@delegates[extension] = delegate
|
39
|
+
end
|
40
|
+
|
41
|
+
@@glob = "**/*{#{@@delegates.keys.sort.join(',')}}"
|
42
|
+
|
43
|
+
delegate
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Returns the delegate associated with a file extension.
|
48
|
+
#
|
49
|
+
# * +extension+ - File extension of the desired delegate.
|
50
|
+
#
|
51
|
+
def self.delegate(extension)
|
52
|
+
@@delegates[extension]
|
53
|
+
end
|
54
|
+
|
18
55
|
##
|
19
56
|
# Creates a new instance.
|
20
57
|
#
|
@@ -35,9 +72,7 @@ class Darkroom
|
|
35
72
|
def initialize(*load_paths, host: nil, hosts: nil, prefix: nil, pristine: nil, minify: false,
|
36
73
|
minified_pattern: DEFAULT_MINIFIED_PATTERN, internal_pattern: DEFAULT_INTERNAL_PATTERN,
|
37
74
|
min_process_interval: MIN_PROCESS_INTERVAL)
|
38
|
-
@
|
39
|
-
globs[path.chomp('/')] = File.join(path, '**', "*{#{Asset.extensions.join(',')}}")
|
40
|
-
end
|
75
|
+
@load_paths = load_paths.map { |load_path| load_path.chomp('/') }
|
41
76
|
|
42
77
|
@hosts = (Array(host) + Array(hosts)).map! { |host| host.sub(TRAILING_SLASHES, '') }
|
43
78
|
@minify = minify
|
@@ -58,6 +93,8 @@ class Darkroom
|
|
58
93
|
@manifest_unversioned = {}
|
59
94
|
@manifest_versioned = {}
|
60
95
|
|
96
|
+
@errors = []
|
97
|
+
|
61
98
|
Thread.current[:darkroom_host_index] = -1 unless @hosts.empty?
|
62
99
|
end
|
63
100
|
|
@@ -75,14 +112,16 @@ class Darkroom
|
|
75
112
|
|
76
113
|
@mutex.synchronize do
|
77
114
|
@process_key += 1
|
78
|
-
@errors
|
115
|
+
@errors.clear
|
79
116
|
found = {}
|
80
117
|
|
81
|
-
@
|
82
|
-
Dir.glob(glob).sort.each do |file|
|
118
|
+
@load_paths.each do |load_path|
|
119
|
+
Dir.glob(File.join(load_path, @@glob)).sort.each do |file|
|
83
120
|
path = file.sub(load_path, '')
|
84
121
|
|
85
|
-
if
|
122
|
+
if index = (path =~ INVALID_PATH)
|
123
|
+
@errors << InvalidPathError.new(path, index)
|
124
|
+
elsif found.key?(path)
|
86
125
|
@errors << DuplicateAssetError.new(path, found[path], load_path)
|
87
126
|
else
|
88
127
|
found[path] = load_path
|
@@ -141,7 +180,7 @@ class Darkroom
|
|
141
180
|
# darkroom.asset('/assets/js/app.<hash>.js')
|
142
181
|
# darkroom.asset('/assets/js/app.js')
|
143
182
|
#
|
144
|
-
# * +path+ -
|
183
|
+
# * +path+ - External path of the asset.
|
145
184
|
#
|
146
185
|
def asset(path)
|
147
186
|
@manifest_versioned[path] || @manifest_unversioned[path]
|
@@ -158,7 +197,7 @@ class Darkroom
|
|
158
197
|
#
|
159
198
|
# Raises an AssetNotFoundError if the asset doesn't exist.
|
160
199
|
#
|
161
|
-
# * +path+ -
|
200
|
+
# * +path+ - Internal path of the asset.
|
162
201
|
# * +versioned+ - Boolean indicating whether the versioned or unversioned path should be returned.
|
163
202
|
#
|
164
203
|
def asset_path(path, versioned: !@pristine.include?(path))
|
@@ -174,8 +213,8 @@ class Darkroom
|
|
174
213
|
# Returns an asset's subresource integrity string. Raises an AssetNotFoundError if the asset doesn't
|
175
214
|
# exist.
|
176
215
|
#
|
177
|
-
# * +path+ -
|
178
|
-
# * +algorithm+ -
|
216
|
+
# * +path+ - Internal path of the asset.
|
217
|
+
# * +algorithm+ - Hash algorithm to use to generate the integrity string (see Asset#integrity).
|
179
218
|
#
|
180
219
|
def asset_integrity(path, algorithm = nil)
|
181
220
|
asset = @manifest[path] or raise(AssetNotFoundError.new(path))
|
@@ -186,7 +225,7 @@ class Darkroom
|
|
186
225
|
##
|
187
226
|
# Returns the asset from the manifest hash associated with the given path.
|
188
227
|
#
|
189
|
-
# * +path+ -
|
228
|
+
# * +path+ - Internal path of the asset.
|
190
229
|
#
|
191
230
|
def manifest(path)
|
192
231
|
@manifest[path]
|
@@ -204,6 +243,8 @@ class Darkroom
|
|
204
243
|
# included).
|
205
244
|
#
|
206
245
|
def dump(dir, clear: false, include_pristine: true)
|
246
|
+
require('fileutils')
|
247
|
+
|
207
248
|
dir = File.expand_path(dir)
|
208
249
|
|
209
250
|
FileUtils.mkdir_p(dir)
|
@@ -228,10 +269,10 @@ class Darkroom
|
|
228
269
|
def inspect
|
229
270
|
"#<#{self.class}: "\
|
230
271
|
"@errors=#{@errors.inspect}, "\
|
231
|
-
"@globs=#{@globs.inspect}, "\
|
232
272
|
"@hosts=#{@hosts.inspect}, "\
|
233
273
|
"@internal_pattern=#{@internal_pattern.inspect}, "\
|
234
274
|
"@last_processed_at=#{@last_processed_at.inspect}, "\
|
275
|
+
"@load_paths=#{@load_paths.inspect}, "\
|
235
276
|
"@min_process_interval=#{@min_process_interval.inspect}, "\
|
236
277
|
"@minified_pattern=#{@minified_pattern.inspect}, "\
|
237
278
|
"@minify=#{@minify.inspect}, "\
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative('../asset')
|
4
|
+
|
5
|
+
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
|
+
)
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative('../asset')
|
4
|
+
|
5
|
+
class Darkroom
|
6
|
+
class Asset
|
7
|
+
HTMLDelegate = Delegate.new(
|
8
|
+
content_type: 'text/html',
|
9
|
+
reference_regex: %r{
|
10
|
+
<(?<tag>a|area|audio|base|embed|iframe|img|input|link|script|source|track|video)\s+[^>]*
|
11
|
+
(?<attr>href|src)=#{REFERENCE_PATH.source}[^>]*>
|
12
|
+
}x.freeze,
|
13
|
+
|
14
|
+
validate_reference: ->(asset, match, format) do
|
15
|
+
return unless format == 'displace'
|
16
|
+
|
17
|
+
if match[:tag] == 'link'
|
18
|
+
'Asset type must be text/css' unless asset.content_type == 'text/css'
|
19
|
+
elsif match[:tag] == 'script'
|
20
|
+
'Asset type must be text/javascript' unless asset.content_type == 'text/javascript'
|
21
|
+
elsif match[:tag] == 'img'
|
22
|
+
'Asset type must be image/svg+xml' unless asset.content_type == 'image/svg+xml'
|
23
|
+
else
|
24
|
+
"Cannot displace <#{match[:tag]}> tags"
|
25
|
+
end
|
26
|
+
end,
|
27
|
+
|
28
|
+
reference_content: ->(asset, match, format) do
|
29
|
+
case format
|
30
|
+
when 'displace'
|
31
|
+
if match[:tag] == 'link' && asset.content_type == 'text/css'
|
32
|
+
"<style>#{asset.content}</style>"
|
33
|
+
elsif match[:tag] == 'script' && asset.content_type == 'text/javascript'
|
34
|
+
offset = match.begin(0)
|
35
|
+
|
36
|
+
"#{match[0][0..(match.begin(:attr) - 2 - offset)]}"\
|
37
|
+
"#{match[0][(match.end(:quoted) + match[:quote].size - offset)..(match.end(0) - offset)]}"\
|
38
|
+
"#{asset.content}"
|
39
|
+
elsif match[:tag] == 'img' && asset.content_type == 'image/svg+xml'
|
40
|
+
asset.content
|
41
|
+
end
|
42
|
+
when 'utf8'
|
43
|
+
quote = match[:quote] == '' ? Asset::DEFAULT_QUOTE : match[:quote]
|
44
|
+
|
45
|
+
content = asset.content.gsub('#', '%23')
|
46
|
+
content.gsub!(quote, quote == "'" ? '"' : "'")
|
47
|
+
|
48
|
+
content
|
49
|
+
end
|
50
|
+
end,
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative('../asset')
|
4
|
+
require_relative('html')
|
5
|
+
require_relative('javascript')
|
6
|
+
|
7
|
+
class Darkroom
|
8
|
+
class Asset
|
9
|
+
HTXDelegate = Delegate.new(
|
10
|
+
content_type: JavaScriptDelegate.content_type,
|
11
|
+
import_regex: HTMLDelegate.import_regex,
|
12
|
+
reference_regex: HTMLDelegate.reference_regex,
|
13
|
+
validate_reference: HTMLDelegate.validate_reference,
|
14
|
+
reference_content: HTMLDelegate.reference_content,
|
15
|
+
compile_lib: 'htx',
|
16
|
+
compile: ->(path, content) { HTX.compile(path, content) },
|
17
|
+
minify_lib: JavaScriptDelegate.minify_lib,
|
18
|
+
minify: JavaScriptDelegate.minify,
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative('../asset')
|
4
|
+
|
5
|
+
class Darkroom
|
6
|
+
class Asset
|
7
|
+
JavaScriptDelegate = Delegate.new(
|
8
|
+
content_type: 'text/javascript',
|
9
|
+
import_regex: /^ *import +#{QUOTED_PATH.source} *;? *(\n|$)/.freeze,
|
10
|
+
minify_lib: 'uglifier',
|
11
|
+
minify: ->(content) do
|
12
|
+
Uglifier.compile(content, harmony: true)
|
13
|
+
end,
|
14
|
+
)
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Darkroom
|
4
|
+
##
|
5
|
+
# General error class used for errors encountered while processing an asset.
|
6
|
+
#
|
7
|
+
class AssetError < StandardError
|
8
|
+
attr_reader(:detail, :source_path, :source_line_num)
|
9
|
+
|
10
|
+
##
|
11
|
+
# Creates a new instance.
|
12
|
+
#
|
13
|
+
# * +message+ - Description of the error.
|
14
|
+
# * +detail+ - Additional detail about the error.
|
15
|
+
# * +source_path+ - Path of the asset that contains the error (optional).
|
16
|
+
# * +source_line_num+ - Line number in the asset where the error is located (optional).
|
17
|
+
#
|
18
|
+
def initialize(message, detail, source_path = nil, source_line_num = nil)
|
19
|
+
super(message)
|
20
|
+
|
21
|
+
@detail = detail
|
22
|
+
@source_path = source_path
|
23
|
+
@source_line_num = source_line_num
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Returns a string representation of the error.
|
28
|
+
#
|
29
|
+
def to_s
|
30
|
+
"#{"#{@source_path}:#{@source_line_num || '?'}: " if @source_path}#{super}: #{@detail}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -1,33 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative('asset_error')
|
4
|
+
|
3
5
|
class Darkroom
|
4
6
|
##
|
5
|
-
# Error class used when an asset requested explicitly or specified as a dependency of another
|
6
|
-
#
|
7
|
+
# Error class used when an asset requested explicitly or specified as a dependency of another doesn't
|
8
|
+
# exist.
|
7
9
|
#
|
8
|
-
class AssetNotFoundError <
|
9
|
-
attr_reader(:path, :referenced_from, :referenced_from_line)
|
10
|
-
|
10
|
+
class AssetNotFoundError < AssetError
|
11
11
|
##
|
12
12
|
# Creates a new instance.
|
13
13
|
#
|
14
|
-
# * +path+ -
|
15
|
-
# * +
|
16
|
-
# * +
|
17
|
-
#
|
18
|
-
def initialize(path, referenced_from = nil, referenced_from_line = nil)
|
19
|
-
@path = path
|
20
|
-
@referenced_from = referenced_from
|
21
|
-
@referenced_from_line = referenced_from_line
|
22
|
-
end
|
23
|
-
|
24
|
-
##
|
25
|
-
# Returns a string representation of the error.
|
14
|
+
# * +path+ - Path of asset that doesn't exist.
|
15
|
+
# * +source_path+ - Path of the asset that contains the error (optional).
|
16
|
+
# * +source_line_num+ - Line number in the asset where the error is located (optional).
|
26
17
|
#
|
27
|
-
def
|
28
|
-
|
29
|
-
" (referenced from #{@referenced_from}:#{@referenced_from_line || '?'})" if @referenced_from
|
30
|
-
}: #{@path}"
|
18
|
+
def initialize(path, source_path = nil, source_line_num = nil)
|
19
|
+
super('Asset not found', path, source_path, source_line_num)
|
31
20
|
end
|
32
21
|
end
|
33
22
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative('asset_error')
|
4
|
+
|
5
|
+
class Darkroom
|
6
|
+
##
|
7
|
+
# Error class used when an asset reference results in a circular reference chain.
|
8
|
+
#
|
9
|
+
class CircularReferenceError < AssetError
|
10
|
+
##
|
11
|
+
# Creates a new instance.
|
12
|
+
#
|
13
|
+
# * +snippet+ - Snippet showing the reference.
|
14
|
+
# * +source_path+ - Path of the asset that contains the error.
|
15
|
+
# * +source_line_num+ - Line number in the asset where the error is located.
|
16
|
+
#
|
17
|
+
def initialize(snippet, source_path, source_line_num)
|
18
|
+
super('Reference would result in a circular reference chain', snippet, source_path, source_line_num)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
class Darkroom
|
4
4
|
##
|
5
|
-
# Error class used when an asset exists
|
5
|
+
# Error class used when an asset exists under multiple load paths.
|
6
6
|
#
|
7
7
|
class DuplicateAssetError < StandardError
|
8
8
|
attr_reader(:path, :first_load_path, :second_load_path)
|
@@ -10,9 +10,9 @@ class Darkroom
|
|
10
10
|
##
|
11
11
|
# Creates a new instance.
|
12
12
|
#
|
13
|
-
# * +path+ -
|
14
|
-
# * +first_load_path+ -
|
15
|
-
# * +second_load_path+ -
|
13
|
+
# * +path+ - Path of the asset that exists under multiple load paths.
|
14
|
+
# * +first_load_path+ - Load path where the asset was first found.
|
15
|
+
# * +second_load_path+ - Load path where the asset was subsequently found.
|
16
16
|
#
|
17
17
|
def initialize(path, first_load_path, second_load_path)
|
18
18
|
@path = path
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Darkroom
|
4
|
+
##
|
5
|
+
# Error class used when an asset's path contains one or more invalid characters.
|
6
|
+
#
|
7
|
+
class InvalidPathError < StandardError
|
8
|
+
attr_reader(:path, :index)
|
9
|
+
|
10
|
+
##
|
11
|
+
# Creates a new instance.
|
12
|
+
#
|
13
|
+
# * +path+ - Path of the asset with the invalid character(s).
|
14
|
+
# * +index+ - Position of the first bad character in the path.
|
15
|
+
#
|
16
|
+
def initialize(path, index)
|
17
|
+
@path = path
|
18
|
+
@index = index
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# Returns a string representation of the error.
|
23
|
+
#
|
24
|
+
def to_s
|
25
|
+
"Asset path contains one or more invalid characters (#{DISALLOWED_PATH_CHARS}): #{@path}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -10,9 +10,9 @@ class Darkroom
|
|
10
10
|
##
|
11
11
|
# Creates a new instance.
|
12
12
|
#
|
13
|
-
# * +library+ -
|
14
|
-
# * +need+ -
|
15
|
-
# * +extension+ -
|
13
|
+
# * +library+ - Name of the library that's missing.
|
14
|
+
# * +need+ - Reason the library is needed ('compile' or 'minify').
|
15
|
+
# * +extension+ - Extension of the type of asset that needs the library.
|
16
16
|
#
|
17
17
|
def initialize(library, need, extension)
|
18
18
|
@library = library
|