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