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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5c415007207739d2b2309bad3372784ac664278c9e4eb02e7b3568f4b41ac5ce
|
4
|
+
data.tar.gz: 9faf193286ae14cca9e406cf1f4c9639ef7f5eddf0cd919bd3d5b2f9a87175ef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e0210152f97f5e96acb9f6da9e5999681cf9b7deeb1e8aaa81e4f609e2d97d04d17216d8ee7950ec4343247632dfa87785e8e9477891ad705d5518142e5ac054
|
7
|
+
data.tar.gz: b25709a4b7aab61a1710402a655043792f14f742793744cea3549cc43aa39ab47740456f9bfcad7f65c4fb859faa9366a0e75eec77bb5e563686ba3fe27f407c
|
data/README.md
CHANGED
@@ -9,19 +9,20 @@ each language's native import statement syntax.
|
|
9
9
|
The following file types are supported out of the box, though support for others can be added (see the
|
10
10
|
[Extending](#extending) section):
|
11
11
|
|
12
|
-
| Name | Content Type
|
13
|
-
|
14
|
-
| CSS | text/css
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
| JPEG | image/jpeg
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
| Name | Content Type | Extension(s) |
|
13
|
+
|------------|------------------|--------------|
|
14
|
+
| CSS | text/css | .css |
|
15
|
+
| HTML | text/html | .htm, .html |
|
16
|
+
| HTX | text/javascript | .htx |
|
17
|
+
| ICO | image/x-icon | .ico |
|
18
|
+
| JavaScript | text/javascript | .js |
|
19
|
+
| JPEG | image/jpeg | .jpg, .jpeg |
|
20
|
+
| JSON | application/json | .json |
|
21
|
+
| PNG | image/png | .png |
|
22
|
+
| SVG | image/svg+xml | .svg |
|
23
|
+
| Text | text/plain | .txt |
|
24
|
+
| WOFF | font/woff | .woff |
|
25
|
+
| WOFF2 | font/woff2 | .woff2 |
|
25
26
|
|
26
27
|
## Installation
|
27
28
|
|
@@ -37,6 +38,17 @@ Or install manually on the command line:
|
|
37
38
|
gem install darkroom
|
38
39
|
```
|
39
40
|
|
41
|
+
Darkroom depends on a few other gems for compilation and minification of certain asset types, but does not
|
42
|
+
explicitly include them as dependencies since need for them varies from project to project. As such, if your
|
43
|
+
project includes HTX templates or you wish to minify CSS and/or JavaScript assets, the following will need
|
44
|
+
to be added to your Gemfile:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
gem('htx') # HTX compilation
|
48
|
+
gem('sassc') # CSS minification
|
49
|
+
gem('uglifier') # JavaScript and HTX minification
|
50
|
+
```
|
51
|
+
|
40
52
|
## Usage
|
41
53
|
|
42
54
|
To create and start using a Darkroom instance, specify one or more load paths (all other arguments are
|
@@ -44,50 +56,70 @@ optional):
|
|
44
56
|
|
45
57
|
```ruby
|
46
58
|
darkroom = Darkroom.new('app/assets', 'vendor/assets', '...',
|
47
|
-
hosts: [
|
59
|
+
hosts: [ # Hosts to prepend to asset paths (useful in production
|
60
|
+
'https://cname1.cdn.com', # when assets are served from a CDN with multiple
|
61
|
+
'https://cname2.cdn.com', # cnames); hosts are chosen round-robin per thread
|
62
|
+
'...',
|
63
|
+
],
|
48
64
|
prefix: '/static', # Prefix to add to all asset paths
|
49
|
-
pristine: ['/google-verify.html'], # Paths with no prefix or versioning (
|
65
|
+
pristine: ['/google-verify.html'], # Paths with no prefix or versioning (/favicon.ico,
|
66
|
+
# /mask-icon.svg, /humans.txt, and /robots.txt are
|
67
|
+
# included automatically)
|
50
68
|
minify: true, # Minify assets that can be minified
|
51
|
-
minified_pattern: /(\.|-)min\.\w+$/, # Files
|
52
|
-
internal_pattern: /^\/components\//, # Files
|
69
|
+
minified_pattern: /(\.|-)min\.\w+$/, # Files to skip minification on when minify: true
|
70
|
+
internal_pattern: /^\/components\//, # Files to disallow direct external access to (they can
|
71
|
+
# still be imported into other assets)
|
53
72
|
min_process_interval: 1, # Minimum time that must elapse between process calls
|
54
73
|
)
|
74
|
+
```
|
55
75
|
|
56
|
-
|
57
|
-
|
76
|
+
Note that assets paths across all load path directories must be globally unique (e.g. the existence of both
|
77
|
+
`app/assets/app.js` and `vendor/assets/app.js` will result in an error).
|
78
|
+
|
79
|
+
Darkroom will never update assets without explicitly being told to do so. The following call should be made
|
80
|
+
once when the app is started and additionally at the beginning of each web request in development to refresh
|
81
|
+
any modified assets:
|
82
|
+
|
83
|
+
```ruby
|
58
84
|
darkroom.process
|
85
|
+
```
|
86
|
+
|
87
|
+
Alternatively, assets can be dumped to disk when deploying to a production environment where assets will be
|
88
|
+
uploaded to and served from a CDN or proxy server:
|
59
89
|
|
60
|
-
|
61
|
-
# uploaded to and served from a CDN or proxy server.
|
90
|
+
```ruby
|
62
91
|
darkroom.dump('output/dir',
|
63
92
|
clear: true, # Delete contents of output/dir before dumping
|
64
93
|
include_pristine: true, # Include pristine assets (if preparing for CDN upload, files like
|
65
94
|
) # /favicon.ico or /robots.txt should be left out)
|
66
95
|
```
|
67
96
|
|
68
|
-
Note that assets paths across all load path directories must be globally unique (e.g. the existence of both
|
69
|
-
`app/assets/app.js` and `vendor/assets/app.js` will result in an error).
|
70
|
-
|
71
97
|
To work with assets:
|
72
98
|
|
73
99
|
```ruby
|
74
|
-
#
|
75
|
-
path = darkroom.asset_path('/js/app.js')
|
100
|
+
# A Darkroom instance has a few convenience helper methods.
|
101
|
+
path = darkroom.asset_path('/js/app.js') # => '/static/js/app-[fingerprint].js'
|
102
|
+
integrity = darkroom.asset_integrity('/js/app.js') # => 'sha384-[hash]'
|
76
103
|
|
77
104
|
# Retrieve the Asset object associated with a path.
|
78
105
|
asset = darkroom.asset(path)
|
79
106
|
|
80
|
-
#
|
81
|
-
assest.path
|
82
|
-
assest.
|
107
|
+
# Prefix (if set on the Darkroom instance) is included in the unversioned and versioned paths.
|
108
|
+
assest.path # => '/js/app.js'
|
109
|
+
assest.path_unversioned # => '/static/js/app.js'
|
110
|
+
assest.path_versioned # => '/static/js/app-[fingerprint].js'
|
83
111
|
|
84
|
-
asset.content_type
|
85
|
-
asset.content
|
112
|
+
asset.content_type # => 'text/javascript'
|
113
|
+
asset.content # Content of processed /js/app.js file
|
86
114
|
|
87
|
-
asset.headers # => {'Content-Type' => '
|
115
|
+
asset.headers # => {'Content-Type' => 'text/javascript',
|
88
116
|
# 'Cache-Control' => 'public, max-age=31536000'}
|
89
|
-
asset.headers(versioned: false) # => {'Content-Type' => '
|
90
|
-
# 'ETag' => '
|
117
|
+
asset.headers(versioned: false) # => {'Content-Type' => 'text/javascript',
|
118
|
+
# 'ETag' => '[fingerprint]'}
|
119
|
+
|
120
|
+
asset.integrity # => 'sha384-[hash]'
|
121
|
+
asset.integrity(:sha256) # => 'sha256-[hash]'
|
122
|
+
asset.integrity(:sha512) # => 'sha512-[hash]'
|
91
123
|
```
|
92
124
|
|
93
125
|
## Asset Bundling
|
@@ -146,19 +178,73 @@ Imports can even be cyclical. If `asset-a.css` imports `asset-b.css` and vice-ve
|
|
146
178
|
contain the content of both of those assets (though order will be different as an asset's own content always
|
147
179
|
comes after any imported assets' contents).
|
148
180
|
|
181
|
+
## Asset References
|
182
|
+
|
183
|
+
Asset paths and content can be inserted into an asset by referencing an asset's path and including a query
|
184
|
+
parameter.
|
185
|
+
|
186
|
+
| String | Result |
|
187
|
+
|----------------------------------|-----------------------------------|
|
188
|
+
| /logo.svg?asset-path | /prefix/logo-[fingerprint].svg |
|
189
|
+
| /logo.svg?asset-path=versioned | /prefix/logo-[fingerprint].svg |
|
190
|
+
| /logo.svg?asset-path=unversioned | /prefix/logo.svg |
|
191
|
+
| /logo.svg?asset-content | data:image/svg+xml;base64,[data] |
|
192
|
+
| /logo.svg?asset-content=base64 | data:image/svg+xml;base64,[data] |
|
193
|
+
| /logo.svg?asset-content=utf8 | data:image/svg+xml;utf8,\<svg>... |
|
194
|
+
|
195
|
+
Where these get recognized is specific to each asset type.
|
196
|
+
|
197
|
+
* **CSS** - Within `url(...)`, which may be unquoted or quoted with single or double quotes.
|
198
|
+
* **HTML** - Values of `href` and `src` attributes on `a`, `area`, `audio`, `base`, `embed`, `iframe`,
|
199
|
+
`img`, `input`, `link`, `script`, `source`, `track`, and `video` tags.
|
200
|
+
* **HTX** - Same behavior as HTML.
|
201
|
+
|
202
|
+
HTML assets additionally support the `?asset-content=displace` query parameter for use with `<link>`,
|
203
|
+
`<script>`, and `<img>` tags with CSS, JavaScript, and SVG asset references, respectively. The entire tag is
|
204
|
+
replaced appropriately.
|
205
|
+
|
206
|
+
```html
|
207
|
+
<!-- Source -->
|
208
|
+
<head>
|
209
|
+
<title>My App</title>
|
210
|
+
<link href='/app.css?asset-content=displace' type='text/css'>
|
211
|
+
<script src='/app.js?asset-content=displace'></script>
|
212
|
+
</head>
|
213
|
+
|
214
|
+
<body>
|
215
|
+
<img src='/logo.svg?asset-content-displace'>
|
216
|
+
</body>
|
217
|
+
|
218
|
+
<!-- Result -->
|
219
|
+
<head>
|
220
|
+
<title>My App</title>
|
221
|
+
<style>/* Content of /app.css */</style>
|
222
|
+
<script>/* Content of /app.js */</script>
|
223
|
+
</head>
|
224
|
+
|
225
|
+
<body>
|
226
|
+
<svg><!-- ... --></svg>
|
227
|
+
</body>
|
228
|
+
```
|
229
|
+
|
149
230
|
## Extending
|
150
231
|
|
151
232
|
Darkroom is extensible. Support for arbitrary file types can be added as follows (all named parameters are
|
152
233
|
optional):
|
153
234
|
|
154
235
|
```ruby
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
236
|
+
# Simple type with no special behavior.
|
237
|
+
Darkroom.register('.extension1', 'extension2', '...', 'content/type')
|
238
|
+
|
239
|
+
# Complex type with special behavior.
|
240
|
+
Darkroom::Asset.register('.extension1', 'extension2', '...',
|
241
|
+
content_type: 'content/type', # HTTP MIME type string
|
242
|
+
import_regex: /import (?<path>.*)/, # Regex for identifying imports for bundling
|
243
|
+
reference_regex: /ref=(?<path>.*)/, # Regex for identifying references to other assets
|
244
|
+
compile_lib: 'some-compile-lib', # Name of library required for compilation
|
245
|
+
compile: ->(path, content) { '...' }, # Lambda that returns compiled content
|
246
|
+
minify_lib: 'some-minify-lib', # Name of library required for minification
|
247
|
+
minify: ->(content) { '...' }, # Lambda that returns minified content
|
162
248
|
)
|
163
249
|
|
164
250
|
```
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.4
|
data/lib/darkroom.rb
CHANGED
@@ -4,36 +4,29 @@ require('darkroom/asset')
|
|
4
4
|
require('darkroom/darkroom')
|
5
5
|
require('darkroom/version')
|
6
6
|
|
7
|
+
require('darkroom/delegates/css')
|
8
|
+
require('darkroom/delegates/html')
|
9
|
+
require('darkroom/delegates/htx')
|
10
|
+
require('darkroom/delegates/javascript')
|
11
|
+
|
12
|
+
require('darkroom/errors/asset_error')
|
7
13
|
require('darkroom/errors/asset_not_found_error')
|
14
|
+
require('darkroom/errors/circular_reference_error')
|
8
15
|
require('darkroom/errors/duplicate_asset_error')
|
16
|
+
require('darkroom/errors/invalid_path_error')
|
9
17
|
require('darkroom/errors/missing_library_error')
|
10
18
|
require('darkroom/errors/processing_error')
|
11
|
-
require('darkroom/errors/
|
12
|
-
|
13
|
-
Darkroom::Asset.add_spec('.css', 'text/css',
|
14
|
-
dependency_regex: /^ *@import +#{Darkroom::Asset::IMPORT_PATH_REGEX} *; *$/,
|
15
|
-
minify: -> (content) { SassC::Engine.new(content, style: :compressed).render },
|
16
|
-
minify_lib: 'sassc',
|
17
|
-
)
|
18
|
-
|
19
|
-
Darkroom::Asset.add_spec('.js', 'application/javascript',
|
20
|
-
dependency_regex: /^ *import +#{Darkroom::Asset::IMPORT_PATH_REGEX} *;? *$/,
|
21
|
-
minify: -> (content) { Uglifier.compile(content, harmony: true) },
|
22
|
-
minify_lib: 'uglifier',
|
23
|
-
)
|
24
|
-
|
25
|
-
Darkroom::Asset.add_spec('.htx', 'application/javascript',
|
26
|
-
compile: -> (path, content) { HTX.compile(path, content) },
|
27
|
-
compile_lib: 'htx',
|
28
|
-
minify: Darkroom::Asset.spec('.js').minify,
|
29
|
-
minify_lib: Darkroom::Asset.spec('.js').minify_lib,
|
30
|
-
)
|
19
|
+
require('darkroom/errors/unrecognized_extension_error')
|
31
20
|
|
32
|
-
Darkroom
|
33
|
-
Darkroom
|
34
|
-
Darkroom
|
35
|
-
Darkroom
|
36
|
-
Darkroom
|
37
|
-
Darkroom
|
38
|
-
Darkroom
|
39
|
-
Darkroom
|
21
|
+
Darkroom.register('.css', Darkroom::Asset::CSSDelegate)
|
22
|
+
Darkroom.register('.htm', '.html', Darkroom::Asset::HTMLDelegate)
|
23
|
+
Darkroom.register('.htx', Darkroom::Asset::HTXDelegate)
|
24
|
+
Darkroom.register('.ico', 'image/x-icon')
|
25
|
+
Darkroom.register('.jpg', '.jpeg', 'image/jpeg')
|
26
|
+
Darkroom.register('.js', Darkroom::Asset::JavaScriptDelegate)
|
27
|
+
Darkroom.register('.json', 'application/json')
|
28
|
+
Darkroom.register('.png', 'image/png')
|
29
|
+
Darkroom.register('.svg', 'image/svg+xml')
|
30
|
+
Darkroom.register('.txt', 'text/plain')
|
31
|
+
Darkroom.register('.woff', 'font/woff')
|
32
|
+
Darkroom.register('.woff2', 'font/woff2')
|
data/lib/darkroom/asset.rb
CHANGED
@@ -2,23 +2,34 @@
|
|
2
2
|
|
3
3
|
require('base64')
|
4
4
|
require('digest')
|
5
|
+
require('set')
|
6
|
+
|
7
|
+
require_relative('darkroom')
|
5
8
|
|
6
9
|
class Darkroom
|
7
10
|
##
|
8
11
|
# Represents an asset.
|
9
12
|
#
|
10
13
|
class Asset
|
11
|
-
DEPENDENCY_JOINER = "\n"
|
12
14
|
EXTENSION_REGEX = /(?=\.\w+)/.freeze
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
BODY_END = '(?<!\\\\)(\\\\\\\\)*'
|
17
|
-
CLOSE_QUOTE = '\k<quote>'
|
16
|
+
IMPORT_JOINER = "\n"
|
17
|
+
DEFAULT_QUOTE = '\''
|
18
18
|
|
19
|
-
|
19
|
+
QUOTED_PATH = /(?<quote>['"])(?<path>[^'"]*)\k<quote>/.freeze
|
20
|
+
REFERENCE_PATH =
|
21
|
+
%r{
|
22
|
+
(?<quote>['"]?)(?<quoted>
|
23
|
+
(?<path>[^#{DISALLOWED_PATH_CHARS}]+)
|
24
|
+
\?asset-(?<entity>path|content)(=(?<format>\w*))?
|
25
|
+
)\k<quote>
|
26
|
+
}x.freeze
|
20
27
|
|
21
|
-
|
28
|
+
# First item of each set is used as default, so order is important.
|
29
|
+
REFERENCE_FORMATS = {
|
30
|
+
'path' => Set.new(%w[versioned unversioned]),
|
31
|
+
'content' => Set.new(%w[base64 utf8 displace]),
|
32
|
+
}.freeze
|
22
33
|
|
23
34
|
attr_reader(:content, :error, :errors, :path, :path_unversioned, :path_versioned)
|
24
35
|
|
@@ -26,16 +37,38 @@ class Darkroom
|
|
26
37
|
# Holds information about how to handle a particular asset type.
|
27
38
|
#
|
28
39
|
# * +content_type+ - HTTP MIME type string.
|
29
|
-
# * +
|
30
|
-
#
|
31
|
-
# * +
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
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.
|
35
65
|
#
|
36
|
-
|
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
68
|
|
38
69
|
##
|
70
|
+
# DEPRECATED. Use Darkroom.register instead.
|
71
|
+
#
|
39
72
|
# Defines an asset spec.
|
40
73
|
#
|
41
74
|
# * +extensions+ - File extensions to associate with this spec.
|
@@ -43,48 +76,38 @@ class Darkroom
|
|
43
76
|
# * +other+ - Optional components of the spec (see Spec struct).
|
44
77
|
#
|
45
78
|
def self.add_spec(*extensions, content_type, **other)
|
46
|
-
|
47
|
-
content_type.freeze,
|
48
|
-
other[:dependency_regex].freeze,
|
49
|
-
other[:compile].freeze,
|
50
|
-
other[:compile_lib].freeze,
|
51
|
-
other[:minify].freeze,
|
52
|
-
other[:minify_lib].freeze,
|
53
|
-
).freeze
|
54
|
-
|
55
|
-
extensions.each do |extension|
|
56
|
-
@@specs[extension] = spec
|
57
|
-
end
|
79
|
+
warn("#{self}.add_spec is deprecated and will be removed soon (use Darkroom.register instead)")
|
58
80
|
|
59
|
-
|
81
|
+
params = other.dup
|
82
|
+
params[:content_type] = content_type
|
83
|
+
params[:import_regex] = params.delete(:dependency_regex) if params.key?(:dependency_regex)
|
84
|
+
|
85
|
+
Darkroom.register(*extensions, params)
|
60
86
|
end
|
61
87
|
|
62
88
|
##
|
89
|
+
# DEPRECATED. Use Darkroom.delegate instead.
|
90
|
+
#
|
63
91
|
# Returns the spec associated with a file extension.
|
64
92
|
#
|
65
93
|
# * +extension+ - File extension of the desired spec.
|
66
94
|
#
|
67
95
|
def self.spec(extension)
|
68
|
-
|
69
|
-
end
|
96
|
+
warn("#{self}.spec is deprecated and will be removed soon (use Darkroom.delegate instead)")
|
70
97
|
|
71
|
-
|
72
|
-
# Returns an array of file extensions for which specs exist.
|
73
|
-
#
|
74
|
-
def self.extensions
|
75
|
-
@@specs.keys
|
98
|
+
Darkroom.delegate(extension)
|
76
99
|
end
|
77
100
|
|
78
101
|
##
|
79
102
|
# Creates a new instance.
|
80
103
|
#
|
81
|
-
# * +file+ -
|
82
|
-
# * +path+ -
|
104
|
+
# * +file+ - Path of file on disk.
|
105
|
+
# * +path+ - Path this asset will be referenced by (e.g. /js/app.js).
|
83
106
|
# * +darkroom+ - Darkroom instance that the asset is a member of.
|
84
107
|
# * +prefix+ - Prefix to apply to unversioned and versioned paths.
|
85
108
|
# * +minify+ - Boolean specifying whether or not the asset should be minified when processed.
|
86
|
-
# * +internal+ - Boolean indicating whether or not the asset is only accessible internally (i.e. as
|
87
|
-
#
|
109
|
+
# * +internal+ - Boolean indicating whether or not the asset is only accessible internally (i.e. as an
|
110
|
+
# import or reference).
|
88
111
|
#
|
89
112
|
def initialize(path, file, darkroom, prefix: nil, minify: false, internal: false)
|
90
113
|
@path = path
|
@@ -96,7 +119,7 @@ class Darkroom
|
|
96
119
|
|
97
120
|
@path_unversioned = "#{@prefix}#{@path}"
|
98
121
|
@extension = File.extname(@path).downcase
|
99
|
-
@
|
122
|
+
@delegate = Darkroom.delegate(@extension) or raise(UnrecognizedExtensionError.new(@path))
|
100
123
|
|
101
124
|
require_libs
|
102
125
|
clear
|
@@ -104,9 +127,9 @@ class Darkroom
|
|
104
127
|
|
105
128
|
##
|
106
129
|
# Processes the asset if modified (see #modified? for how modification is determined). File is read from
|
107
|
-
# disk,
|
108
|
-
#
|
109
|
-
#
|
130
|
+
# disk, references are substituted (if supported), content is compiled (if required), imports are
|
131
|
+
# prefixed to its content (if supported), and content is minified (if supported and enabled). Returns
|
132
|
+
# true if asset was modified since it was last processed and false otherwise.
|
110
133
|
#
|
111
134
|
def process
|
112
135
|
@process_key == @darkroom.process_key ? (return @processed) : (@process_key = @darkroom.process_key)
|
@@ -114,6 +137,9 @@ class Darkroom
|
|
114
137
|
|
115
138
|
clear
|
116
139
|
read
|
140
|
+
build_imports
|
141
|
+
build_references
|
142
|
+
process_dependencies
|
117
143
|
compile
|
118
144
|
minify
|
119
145
|
|
@@ -131,7 +157,32 @@ class Darkroom
|
|
131
157
|
# Returns the HTTP MIME type string.
|
132
158
|
#
|
133
159
|
def content_type
|
134
|
-
@
|
160
|
+
@delegate.content_type
|
161
|
+
end
|
162
|
+
|
163
|
+
##
|
164
|
+
# Returns boolean indicating whether or not the asset is binary.
|
165
|
+
#
|
166
|
+
def binary?
|
167
|
+
return @is_binary if defined?(@is_binary)
|
168
|
+
|
169
|
+
type, subtype = content_type.split('/')
|
170
|
+
|
171
|
+
@is_binary = type != 'text' && !subtype.include?('json') && !subtype.include?('xml')
|
172
|
+
end
|
173
|
+
|
174
|
+
##
|
175
|
+
# Returns boolean indicating whether or not the asset is a font.
|
176
|
+
#
|
177
|
+
def font?
|
178
|
+
defined?(@is_font) ? @is_font : (@is_font = content_type.start_with?('font/'))
|
179
|
+
end
|
180
|
+
|
181
|
+
##
|
182
|
+
# Returns boolean indicating whether or not the asset is an image.
|
183
|
+
#
|
184
|
+
def image?
|
185
|
+
defined?(@is_image) ? @is_image : (@is_image = content_type.start_with?('image/'))
|
135
186
|
end
|
136
187
|
|
137
188
|
##
|
@@ -150,8 +201,8 @@ class Darkroom
|
|
150
201
|
##
|
151
202
|
# Returns subresource integrity string.
|
152
203
|
#
|
153
|
-
# * +algorithm+ -
|
154
|
-
# sha512).
|
204
|
+
# * +algorithm+ - Hash algorithm to use to generate the integrity string (one of :sha256, :sha384, or
|
205
|
+
# :sha512).
|
155
206
|
#
|
156
207
|
def integrity(algorithm = :sha384)
|
157
208
|
@integrity[algorithm] ||= "#{algorithm}-#{Base64.strict_encode64(
|
@@ -219,23 +270,21 @@ class Darkroom
|
|
219
270
|
##
|
220
271
|
# Returns all dependencies (including dependencies of dependencies).
|
221
272
|
#
|
222
|
-
# * +
|
273
|
+
# * +ignore+ - Assets already accounted for as dependency tree is walked (to prevent infinite loops when
|
274
|
+
# circular chains are encountered).
|
223
275
|
#
|
224
|
-
def dependencies(
|
225
|
-
@dependencies ||=
|
226
|
-
|
227
|
-
|
228
|
-
ancestors << self
|
229
|
-
own_dependency.process
|
230
|
-
|
231
|
-
dependencies |= own_dependency.dependencies(ancestors)
|
232
|
-
dependencies |= [own_dependency]
|
233
|
-
|
234
|
-
dependencies.delete(self)
|
235
|
-
ancestors.delete(self)
|
276
|
+
def dependencies(ignore = Set.new)
|
277
|
+
@dependencies ||= accumulate(:dependencies, ignore)
|
278
|
+
end
|
236
279
|
|
237
|
-
|
238
|
-
|
280
|
+
##
|
281
|
+
# Returns all imports (including imports of imports).
|
282
|
+
#
|
283
|
+
# * +ignore+ - Assets already accounted for as import tree is walked (to prevent infinite loops when
|
284
|
+
# circular chains are encountered).
|
285
|
+
#
|
286
|
+
def imports(ignore = Set.new)
|
287
|
+
@imports ||= accumulate(:imports, ignore)
|
239
288
|
end
|
240
289
|
|
241
290
|
##
|
@@ -252,48 +301,124 @@ class Darkroom
|
|
252
301
|
#
|
253
302
|
def clear
|
254
303
|
@dependencies = nil
|
304
|
+
@imports = nil
|
255
305
|
@error = nil
|
256
306
|
@fingerprint = nil
|
257
307
|
@path_versioned = nil
|
258
308
|
|
259
|
-
(@errors ||= []).clear
|
260
309
|
(@own_dependencies ||= []).clear
|
310
|
+
(@own_imports ||= []).clear
|
311
|
+
(@dependency_matches ||= []).clear
|
312
|
+
(@errors ||= []).clear
|
261
313
|
(@content ||= +'').clear
|
262
314
|
(@own_content ||= +'').clear
|
263
315
|
(@integrity ||= {}).clear
|
264
316
|
end
|
265
317
|
|
266
318
|
##
|
267
|
-
# Reads the asset file
|
319
|
+
# Reads the asset file into memory.
|
268
320
|
#
|
269
321
|
def read
|
270
|
-
|
271
|
-
|
272
|
-
return
|
273
|
-
end
|
322
|
+
@own_content = File.read(@file)
|
323
|
+
end
|
274
324
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
325
|
+
##
|
326
|
+
# Builds reference info.
|
327
|
+
#
|
328
|
+
def build_references
|
329
|
+
return unless @delegate.reference_regex
|
330
|
+
|
331
|
+
@own_content.scan(@delegate.reference_regex) do
|
332
|
+
match = Regexp.last_match
|
333
|
+
path = match[:path]
|
334
|
+
format = match[:format]
|
335
|
+
format = REFERENCE_FORMATS[match[:entity]].first if format.nil? || format == ''
|
336
|
+
|
337
|
+
if (asset = @darkroom.manifest(path))
|
338
|
+
if !REFERENCE_FORMATS[match[:entity]].include?(format)
|
339
|
+
@errors << AssetError.new("Invalid reference format '#{format}' (must be one of "\
|
340
|
+
"'#{REFERENCE_FORMATS[match[:entity]].join("', '")}')", match[0], @path, line_num(match))
|
341
|
+
elsif match[:entity] == 'content' && format != 'base64' && asset.binary?
|
342
|
+
@errors << AssetError.new('Base64 encoding is required for binary assets', match[0], @path,
|
343
|
+
line_num(match))
|
344
|
+
elsif (error = @delegate.validate_reference&.(asset, match, format))
|
345
|
+
@errors << AssetError.new(error, match[0], @path, line_num(match))
|
279
346
|
else
|
280
|
-
@
|
347
|
+
@own_dependencies << asset
|
348
|
+
@dependency_matches << [:reference, asset, match, format]
|
281
349
|
end
|
282
350
|
else
|
283
|
-
@
|
351
|
+
@errors << not_found_error(path, match)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
##
|
357
|
+
# Builds import info.
|
358
|
+
#
|
359
|
+
def build_imports
|
360
|
+
return unless @delegate.import_regex
|
361
|
+
|
362
|
+
@own_content.scan(@delegate.import_regex) do
|
363
|
+
match = Regexp.last_match
|
364
|
+
path = match[:path]
|
365
|
+
|
366
|
+
if (asset = @darkroom.manifest(path))
|
367
|
+
@own_dependencies << asset
|
368
|
+
@own_imports << asset
|
369
|
+
@dependency_matches << [:import, asset, match]
|
370
|
+
else
|
371
|
+
@errors << not_found_error(path, match)
|
284
372
|
end
|
285
373
|
end
|
374
|
+
end
|
286
375
|
|
287
|
-
|
376
|
+
##
|
377
|
+
# Processes imports and references.
|
378
|
+
#
|
379
|
+
def process_dependencies
|
380
|
+
@dependency_matches.sort_by! { |_, __, match| -match.begin(0) }.each do |kind, asset, match, format|
|
381
|
+
if kind == :import
|
382
|
+
@own_content[match.begin(0)...match.end(0)] = ''
|
383
|
+
elsif asset.dependencies.include?(self)
|
384
|
+
@errors << CircularReferenceError.new(match[0], @path, line_num(match))
|
385
|
+
else
|
386
|
+
value, start, finish = @delegate.reference_content&.(asset, match, format)
|
387
|
+
min_start, max_finish = match.offset(0)
|
388
|
+
start ||= format == 'displace' ? min_start : match.begin(:quoted)
|
389
|
+
finish ||= format == 'displace' ? max_finish : match.end(:quoted)
|
390
|
+
start = [[start, min_start].max, max_finish].min
|
391
|
+
finish = [[finish, max_finish].min, min_start].max
|
392
|
+
|
393
|
+
@own_content[start...finish] =
|
394
|
+
case "#{match[:entity]}-#{format}"
|
395
|
+
when 'path-versioned'
|
396
|
+
value || asset.path_versioned
|
397
|
+
when 'path-unversioned'
|
398
|
+
value || asset.path_unversioned
|
399
|
+
when 'content-base64'
|
400
|
+
quote = DEFAULT_QUOTE if match[:quote] == ''
|
401
|
+
"#{quote}data:#{asset.content_type};base64,#{Base64.strict_encode64(value || asset.content)}#{quote}"
|
402
|
+
when 'content-utf8'
|
403
|
+
quote = DEFAULT_QUOTE if match[:quote] == ''
|
404
|
+
"#{quote}data:#{asset.content_type};utf8,#{value || asset.content}#{quote}"
|
405
|
+
when 'content-displace'
|
406
|
+
value || asset.content
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
@content << imports.map { |d| d.own_content }.join(IMPORT_JOINER)
|
288
412
|
end
|
289
413
|
|
290
414
|
##
|
291
|
-
# Compiles the asset if compilation is supported for the asset's type
|
415
|
+
# Compiles the asset if compilation is supported for the asset's type and appends the asset's own
|
416
|
+
# content to the overall content string.
|
292
417
|
#
|
293
418
|
def compile
|
294
|
-
if @
|
419
|
+
if @delegate.compile
|
295
420
|
begin
|
296
|
-
@own_content = @
|
421
|
+
@own_content = @delegate.compile.(@path, @own_content)
|
297
422
|
rescue => e
|
298
423
|
@errors << e
|
299
424
|
end
|
@@ -307,9 +432,9 @@ class Darkroom
|
|
307
432
|
# (i.e. it's not already minified), and the asset is not marked as internal-only.
|
308
433
|
#
|
309
434
|
def minify
|
310
|
-
if @
|
435
|
+
if @delegate.minify && @minify && !@internal
|
311
436
|
begin
|
312
|
-
@content = @
|
437
|
+
@content = @delegate.minify.(@content)
|
313
438
|
rescue => e
|
314
439
|
@errors << e
|
315
440
|
end
|
@@ -329,19 +454,59 @@ class Darkroom
|
|
329
454
|
#
|
330
455
|
def require_libs
|
331
456
|
begin
|
332
|
-
require(@
|
457
|
+
require(@delegate.compile_lib) if @delegate.compile_lib
|
333
458
|
rescue LoadError
|
334
459
|
compile_load_error = true
|
335
460
|
end
|
336
461
|
|
337
462
|
begin
|
338
|
-
require(@
|
463
|
+
require(@delegate.minify_lib) if @delegate.minify_lib && @minify
|
339
464
|
rescue LoadError
|
340
465
|
minify_load_error = true
|
341
466
|
end
|
342
467
|
|
343
|
-
raise(MissingLibraryError.new(@
|
344
|
-
raise(MissingLibraryError.new(@
|
468
|
+
raise(MissingLibraryError.new(@delegate.compile_lib, 'compile', @extension)) if compile_load_error
|
469
|
+
raise(MissingLibraryError.new(@delegate.minify_lib, 'minify', @extension)) if minify_load_error
|
470
|
+
end
|
471
|
+
|
472
|
+
##
|
473
|
+
# Utility method used by #dependencies and #imports to recursively build arrays.
|
474
|
+
#
|
475
|
+
# * +name+ - Name of the array to accumulate (:dependencies or :imports).
|
476
|
+
# * +ignore+ - Set of assets already accumulated which can be ignored (used to avoid infinite loops when
|
477
|
+
# circular references are encountered).
|
478
|
+
#
|
479
|
+
def accumulate(name, ignore)
|
480
|
+
ignore << self
|
481
|
+
process
|
482
|
+
|
483
|
+
instance_variable_get(:"@own_#{name}").each_with_object([]) do |asset, assets|
|
484
|
+
next if ignore.include?(asset)
|
485
|
+
|
486
|
+
assets.push(*asset.send(name, ignore), asset)
|
487
|
+
assets.uniq!
|
488
|
+
assets.delete(self)
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
##
|
493
|
+
# Utility method that returns the appropriate error for a dependency that doesn't exist.
|
494
|
+
#
|
495
|
+
# * +path+ - Path of the asset which cannot be found.
|
496
|
+
# * +match+ - MatchData object of the regex for the asset that cannot be found.
|
497
|
+
#
|
498
|
+
def not_found_error(path, match)
|
499
|
+
klass = Darkroom.delegate(File.extname(path)) ? AssetNotFoundError : UnrecognizedExtensionError
|
500
|
+
klass.new(path, @path, line_num(match))
|
501
|
+
end
|
502
|
+
|
503
|
+
##
|
504
|
+
# Utility method that returns the line number where a regex match was found.
|
505
|
+
#
|
506
|
+
# * +match+ - MatchData object of the regex.
|
507
|
+
#
|
508
|
+
def line_num(match)
|
509
|
+
@own_content[0..match.begin(:path)].count("\n") + 1
|
345
510
|
end
|
346
511
|
end
|
347
512
|
end
|