darkroom 0.0.3 → 0.0.4
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 +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
|