darkroom 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- @globs = load_paths.each_with_object({}) do |path, globs|
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
- @globs.each do |load_path, glob|
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 found.key?(path)
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+ - The external path of the asset.
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+ - The internal path of the asset.
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+ - The internal path of the asset.
178
- # * +algorithm+ - The hash algorithm to use to generate the integrity string (see Asset#integrity).
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+ - The internal path of the asset.
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 cannot be
6
- # found.
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 < StandardError
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+ - The path of the asset that cannot be found.
15
- # * +referenced_from+ - The path of the asset the not-found asset was referenced from.
16
- # * +referenced_from_line+ - The line number where the not-found asset was referenced.
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 to_s
28
- "Asset not found#{
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 in multiple load paths.
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+ - The path of the asset that has the same path as another asset.
14
- # * +first_load_path+ - The load path where the first asset with the path was found.
15
- # * +second_load_path+ - The load path where the second asset with the path was found.
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+ - The name of the library that's missing.
14
- # * +need+ - The reason the library is needed ('compile' or 'minify').
15
- # * +extension+ - The extenion of the type of asset that needs the library.
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