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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a77051a57dc9a5b32fe1ea1e96c831884183729058f1336139e15142d55a43f
4
- data.tar.gz: 11ee7f486da94bf7c5dadf9af8e14fd9c1ab41a9620cc72c604885d9474d9150
3
+ metadata.gz: 5c415007207739d2b2309bad3372784ac664278c9e4eb02e7b3568f4b41ac5ce
4
+ data.tar.gz: 9faf193286ae14cca9e406cf1f4c9639ef7f5eddf0cd919bd3d5b2f9a87175ef
5
5
  SHA512:
6
- metadata.gz: c4a4bb0e7cb9845154951aa703197be875b7777bba1f824d0bbba6ccfed609cebc9460a3c32db60ca846e819a0ac4b4a9c4024ba2b882a498108ef83230757d8
7
- data.tar.gz: 83a14e41099925ae3ea44c23a18a044c1db1b1ca8fc10c15dabecf725270c32ec559165ef6e596852f5c7b22f2354ab92a0cdfadd654c1c8d22a6d9d2f5ae9f1
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 | Exension(s) |
13
- | ---------- |----------------------- |-------------|
14
- | CSS | text/css | .css |
15
- | JavaScript | application/javascript | .js |
16
- | HTML | text/html | .htm, .html |
17
- | HTX | application/javascript | .htx |
18
- | ICO | image/x-icon | .ico |
19
- | JPEG | image/jpeg | .jpg, .jpeg |
20
- | PNG | image/png | .png |
21
- | SVG | image/svg+xml | .svg |
22
- | Text | text/plain | .txt |
23
- | WOFF | font/woff | .woff |
24
- | WOFF2 | font/woff2 | .woff2 |
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: ['https://cdn1.com', '...'] # Hosts to prepend to asset paths (useful in production)
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 (e.g. /favicon.ico)
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 that should not be minified
52
- internal_pattern: /^\/components\//, # Files that cannot be accessed directly
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
- # Refresh any assets that have been modified (in development, this should be called at the
57
- # beginning of each web request).
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
- # Dump assets to disk. Useful when deploying to a production environment where assets will be
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
- # Get the external path that will be used by HTTP requests.
75
- path = darkroom.asset_path('/js/app.js') # => '/static/js/app-<fingerprint>.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
- # Getting paths directly from an Asset object will not include any host or prefix.
81
- assest.path # => '/js/app.js'
82
- assest.path_versioned # => '/js/app-<fingerprint>.js'
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 # => 'application/javascript'
85
- asset.content # Content of processed /js/app.js file
112
+ asset.content_type # => 'text/javascript'
113
+ asset.content # Content of processed /js/app.js file
86
114
 
87
- asset.headers # => {'Content-Type' => 'application/javascript',
115
+ asset.headers # => {'Content-Type' => 'text/javascript',
88
116
  # 'Cache-Control' => 'public, max-age=31536000'}
89
- asset.headers(versioned: false) # => {'Content-Type' => 'application/javascript',
90
- # 'ETag' => '<fingerprint>'}
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
- Darkroom::Asset.add_spec('.extension1', 'extension2', '...', 'content/type',
156
- dependency_regex: /import (?<path>.*)/, # Regex for identifying dependencies for bundling;
157
- # must include `path` named capture group
158
- compile_lib: 'some-compile-lib', # Name of library required for compilation
159
- compile: -> (path, content) { '...' }, # Proc that returns compiled content
160
- minify_lib: 'some-minify-lib', # Name of library required for minification
161
- minify: -> (content) { '...' }, # Proc that returns minified content
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.3
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/spec_not_defined_error')
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::Asset.add_spec('.htm', '.html', 'text/html')
33
- Darkroom::Asset.add_spec('.ico', 'image/x-icon')
34
- Darkroom::Asset.add_spec('.jpg', '.jpeg', 'image/jpeg')
35
- Darkroom::Asset.add_spec('.png', 'image/png')
36
- Darkroom::Asset.add_spec('.svg', 'image/svg+xml')
37
- Darkroom::Asset.add_spec('.txt', 'text/plain')
38
- Darkroom::Asset.add_spec('.woff', 'font/woff')
39
- Darkroom::Asset.add_spec('.woff2', 'font/woff2')
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')
@@ -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
- OPEN_QUOTE = '(?<quote>[\'"])'
15
- BODY = '((?!\k<quote>).|(?<!\\\\)\\\\(\\\\\\\\)*\k<quote>)*'
16
- BODY_END = '(?<!\\\\)(\\\\\\\\)*'
17
- CLOSE_QUOTE = '\k<quote>'
16
+ IMPORT_JOINER = "\n"
17
+ DEFAULT_QUOTE = '\''
18
18
 
19
- IMPORT_PATH_REGEX = /#{OPEN_QUOTE}(?<path>#{BODY}#{BODY_END})#{CLOSE_QUOTE}/.freeze
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
- @@specs = {}
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
- # * +dependency_regex+ - Regex to match lines of the file against to find dependencies. Must contain a
30
- # named component called 'path' (e.g. +/^import (?<path>.*)/+).
31
- # * +compile+ - Proc to call that will produce the compiled version of the asset's content.
32
- # * +compile_lib+ - Name of a library to +require+ that is needed by the +compile+ proc.
33
- # * +minify+ - Proc to call that will produce the minified version of the asset's content.
34
- # * +minify_lib+ - Name of a library to +require+ that is needed by the +minify+ proc.
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
- Spec = Struct.new(:content_type, :dependency_regex, :compile, :compile_lib, :minify, :minify_lib)
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
- spec = Spec.new(
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
- spec
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
- @@specs[extension]
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+ - The path to the file on disk.
82
- # * +path+ - The path this asset will be referenced by (e.g. /js/app.js).
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 a
87
- # dependency).
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
- @spec = self.class.spec(@extension) or raise(SpecNotDefinedError.new(@extension, @file))
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, any dependencies are merged into its content (if spec for the asset type allows for it), the
108
- # content is compiled (if the asset type requires compilation), and minified (if specified for this
109
- # Asset). Returns true if asset was modified since it was last processed and false otherwise.
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
- @spec.content_type
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+ - The hash algorithm to use to generate the integrity string (one of sha256, sha384, or
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
- # * +ancestors+ - Ancestor chain followed to get to this asset as a dependency.
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(ancestors = Set.new)
225
- @dependencies ||= @own_dependencies.inject([]) do |dependencies, own_dependency|
226
- next dependencies if ancestors.include?(self)
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
- dependencies
238
- end
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, building dependency array if dependencies are supported for the asset's type.
319
+ # Reads the asset file into memory.
268
320
  #
269
321
  def read
270
- unless @spec.dependency_regex
271
- @own_content = File.read(@file)
272
- return
273
- end
322
+ @own_content = File.read(@file)
323
+ end
274
324
 
275
- File.new(@file).each.with_index do |line, line_num|
276
- if (path = line[@spec.dependency_regex, :path])
277
- if (dependency = @darkroom.manifest(path))
278
- @own_dependencies << dependency
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
- @errors << AssetNotFoundError.new(path, @path, line_num + 1)
347
+ @own_dependencies << asset
348
+ @dependency_matches << [:reference, asset, match, format]
281
349
  end
282
350
  else
283
- @own_content << line
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
- @content << dependencies.map { |d| d.own_content }.join(DEPENDENCY_JOINER)
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 @spec.compile
419
+ if @delegate.compile
295
420
  begin
296
- @own_content = @spec.compile.call(@path, @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 @spec.minify && @minify && !@internal
435
+ if @delegate.minify && @minify && !@internal
311
436
  begin
312
- @content = @spec.minify.call(@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(@spec.compile_lib) if @spec.compile_lib
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(@spec.minify_lib) if @spec.minify_lib && @minify
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(@spec.compile_lib, 'compile', @extension)) if compile_load_error
344
- raise(MissingLibraryError.new(@spec.minify_lib, 'minify', @extension)) if minify_load_error
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