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