sprockets 3.7.3 → 4.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +2 -299
  3. data/README.md +21 -35
  4. data/bin/sprockets +11 -8
  5. data/lib/rake/sprocketstask.rb +2 -2
  6. data/lib/sprockets/asset.rb +8 -21
  7. data/lib/sprockets/autoload/babel.rb +7 -0
  8. data/lib/sprockets/autoload/jsminc.rb +7 -0
  9. data/lib/sprockets/autoload/sassc.rb +7 -0
  10. data/lib/sprockets/autoload.rb +3 -0
  11. data/lib/sprockets/babel_processor.rb +58 -0
  12. data/lib/sprockets/base.rb +8 -8
  13. data/lib/sprockets/bower.rb +4 -2
  14. data/lib/sprockets/bundle.rb +1 -1
  15. data/lib/sprockets/cache.rb +2 -4
  16. data/lib/sprockets/closure_compressor.rb +1 -2
  17. data/lib/sprockets/coffee_script_processor.rb +9 -3
  18. data/lib/sprockets/compressing.rb +2 -2
  19. data/lib/sprockets/configuration.rb +1 -7
  20. data/lib/sprockets/context.rb +10 -18
  21. data/lib/sprockets/digest_utils.rb +40 -52
  22. data/lib/sprockets/directive_processor.rb +10 -15
  23. data/lib/sprockets/erb_processor.rb +1 -13
  24. data/lib/sprockets/http_utils.rb +19 -4
  25. data/lib/sprockets/jsminc_compressor.rb +31 -0
  26. data/lib/sprockets/jst_processor.rb +10 -10
  27. data/lib/sprockets/loader.rb +34 -28
  28. data/lib/sprockets/manifest.rb +3 -35
  29. data/lib/sprockets/manifest_utils.rb +0 -2
  30. data/lib/sprockets/mime.rb +7 -62
  31. data/lib/sprockets/path_dependency_utils.rb +2 -11
  32. data/lib/sprockets/path_digest_utils.rb +1 -1
  33. data/lib/sprockets/path_utils.rb +43 -18
  34. data/lib/sprockets/preprocessors/default_source_map.rb +24 -0
  35. data/lib/sprockets/processing.rb +30 -61
  36. data/lib/sprockets/processor_utils.rb +27 -28
  37. data/lib/sprockets/resolve.rb +172 -92
  38. data/lib/sprockets/sass_cache_store.rb +1 -6
  39. data/lib/sprockets/sass_compressor.rb +14 -1
  40. data/lib/sprockets/sass_processor.rb +18 -8
  41. data/lib/sprockets/sassc_compressor.rb +30 -0
  42. data/lib/sprockets/sassc_processor.rb +68 -0
  43. data/lib/sprockets/server.rb +11 -22
  44. data/lib/sprockets/source_map_comment_processor.rb +29 -0
  45. data/lib/sprockets/source_map_processor.rb +40 -0
  46. data/lib/sprockets/source_map_utils.rb +345 -0
  47. data/lib/sprockets/transformers.rb +62 -35
  48. data/lib/sprockets/uglifier_compressor.rb +12 -5
  49. data/lib/sprockets/unloaded_asset.rb +12 -11
  50. data/lib/sprockets/uri_tar.rb +4 -2
  51. data/lib/sprockets/uri_utils.rb +5 -8
  52. data/lib/sprockets/utils.rb +30 -79
  53. data/lib/sprockets/version.rb +1 -1
  54. data/lib/sprockets.rb +80 -35
  55. metadata +70 -41
  56. data/LICENSE +0 -21
  57. data/lib/sprockets/coffee_script_template.rb +0 -17
  58. data/lib/sprockets/deprecation.rb +0 -90
  59. data/lib/sprockets/eco_template.rb +0 -17
  60. data/lib/sprockets/ejs_template.rb +0 -17
  61. data/lib/sprockets/engines.rb +0 -92
  62. data/lib/sprockets/erb_template.rb +0 -11
  63. data/lib/sprockets/legacy.rb +0 -330
  64. data/lib/sprockets/legacy_proc_processor.rb +0 -35
  65. data/lib/sprockets/legacy_tilt_processor.rb +0 -29
  66. data/lib/sprockets/sass_template.rb +0 -19
@@ -1,5 +1,6 @@
1
1
  require 'sprockets/autoload'
2
2
  require 'sprockets/digest_utils'
3
+ require 'sprockets/source_map_utils'
3
4
 
4
5
  module Sprockets
5
6
  # Public: Sass CSS minifier.
@@ -45,7 +46,19 @@ module Sprockets
45
46
  end
46
47
 
47
48
  def call(input)
48
- Autoload::Sass::Engine.new(input[:data], @options).render
49
+ css, map = Autoload::Sass::Engine.new(
50
+ input[:data],
51
+ @options.merge(filename: 'filename')
52
+ ).render_with_sourcemap('')
53
+
54
+ css = css.sub("/*# sourceMappingURL= */\n", '')
55
+
56
+ map = SourceMapUtils.combine_source_maps(
57
+ input[:metadata][:map],
58
+ SourceMapUtils.decode_json_source_map(map.to_json(css_uri: 'uri'))["mappings"]
59
+ )
60
+
61
+ { data: css, map: map }
49
62
  end
50
63
  end
51
64
  end
@@ -1,5 +1,6 @@
1
1
  require 'rack/utils'
2
2
  require 'sprockets/autoload'
3
+ require 'sprockets/source_map_utils'
3
4
  require 'uri'
4
5
 
5
6
  module Sprockets
@@ -45,7 +46,8 @@ module Sprockets
45
46
  def initialize(options = {}, &block)
46
47
  @cache_version = options[:cache_version]
47
48
  @cache_key = "#{self.class.name}:#{VERSION}:#{Autoload::Sass::VERSION}:#{@cache_version}".freeze
48
-
49
+ @importer_class = options[:importer] || Sass::Importers::Filesystem
50
+ @sass_config = options[:sass_config] || {}
49
51
  @functions = Module.new do
50
52
  include Functions
51
53
  include options[:functions] if options[:functions]
@@ -56,24 +58,32 @@ module Sprockets
56
58
  def call(input)
57
59
  context = input[:environment].context_class.new(input)
58
60
 
59
- options = {
61
+ engine_options = {
60
62
  filename: input[:filename],
61
63
  syntax: self.class.syntax,
62
64
  cache_store: build_cache_store(input, @cache_version),
63
- load_paths: input[:environment].paths,
65
+ load_paths: context.environment.paths.map { |p| @importer_class.new(p.to_s) },
66
+ importer: @importer_class.new(Pathname.new(context.filename).to_s),
64
67
  sprockets: {
65
68
  context: context,
66
69
  environment: input[:environment],
67
70
  dependencies: context.metadata[:dependencies]
68
71
  }
69
- }
72
+ }.merge!(@sass_config)
70
73
 
71
- engine = Autoload::Sass::Engine.new(input[:data], options)
74
+ engine = Autoload::Sass::Engine.new(input[:data], engine_options)
72
75
 
73
- css = Utils.module_include(Autoload::Sass::Script::Functions, @functions) do
74
- engine.render
76
+ css, map = Utils.module_include(Autoload::Sass::Script::Functions, @functions) do
77
+ engine.render_with_sourcemap('')
75
78
  end
76
79
 
80
+ css = css.sub("\n/*# sourceMappingURL= */\n", '')
81
+
82
+ map = SourceMapUtils.combine_source_maps(
83
+ input[:metadata][:map],
84
+ SourceMapUtils.decode_json_source_map(map.to_json(css_uri: '', type: :inline))["mappings"]
85
+ )
86
+
77
87
  # Track all imported files
78
88
  sass_dependencies = Set.new([input[:filename]])
79
89
  engine.dependencies.map do |dependency|
@@ -81,7 +91,7 @@ module Sprockets
81
91
  context.metadata[:dependencies] << URIUtils.build_file_digest_uri(dependency.options[:filename])
82
92
  end
83
93
 
84
- context.metadata.merge(data: css, sass_dependencies: sass_dependencies)
94
+ context.metadata.merge(data: css, sass_dependencies: sass_dependencies, map: map)
85
95
  end
86
96
 
87
97
  # Public: Build the cache store to be used by the Sass engine.
@@ -0,0 +1,30 @@
1
+ require 'sprockets/autoload'
2
+ require 'sprockets/sass_compressor'
3
+ require 'base64'
4
+
5
+ module Sprockets
6
+ class SasscCompressor < SassCompressor
7
+ def initialize(options = {})
8
+ @options = {
9
+ syntax: :scss,
10
+ style: :compressed,
11
+ source_map_embed: true,
12
+ source_map_file: '.'
13
+ }.merge(options).freeze
14
+ end
15
+
16
+ def call(input)
17
+ data = Autoload::SassC::Engine.new(input[:data], @options.merge(filename: 'filename')).render
18
+
19
+ match_data = data.match(/(.*)\n\/\*# sourceMappingURL=data:application\/json;base64,(.+) \*\//m)
20
+ css, map = match_data[1], Base64.decode64(match_data[2])
21
+
22
+ map = SourceMapUtils.combine_source_maps(
23
+ input[:metadata][:map],
24
+ SourceMapUtils.decode_json_source_map(map)["mappings"]
25
+ )
26
+
27
+ { data: css, map: map }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,68 @@
1
+ require 'sprockets/sass_processor'
2
+ require 'base64'
3
+
4
+ module Sprockets
5
+ class SasscProcessor < SassProcessor
6
+ def initialize(options = {}, &block)
7
+ @cache_version = options[:cache_version]
8
+ @cache_key = "#{self.class.name}:#{VERSION}:#{Autoload::SassC::VERSION}:#{@cache_version}".freeze
9
+ @importer_class = options[:importer]
10
+ @sass_config = options[:sass_config] || {}
11
+ @functions = Module.new do
12
+ include SassProcessor::Functions
13
+ include options[:functions] if options[:functions]
14
+ class_eval(&block) if block_given?
15
+ end
16
+ end
17
+
18
+ def call(input)
19
+ context = input[:environment].context_class.new(input)
20
+
21
+ options = engine_options(input, context)
22
+ engine = Autoload::SassC::Engine.new(input[:data], options)
23
+
24
+ data = Utils.module_include(Autoload::SassC::Script::Functions, @functions) do
25
+ engine.render
26
+ end
27
+
28
+ match_data = data.match(/(.*)\n\/\*# sourceMappingURL=data:application\/json;base64,(.+) \*\//m)
29
+ css, map = match_data[1], Base64.decode64(match_data[2])
30
+
31
+ map = SourceMapUtils.combine_source_maps(
32
+ input[:metadata][:map],
33
+ change_source(SourceMapUtils.decode_json_source_map(map)["mappings"], input[:source_path])
34
+ )
35
+
36
+ context.metadata.merge(data: css, map: map)
37
+ end
38
+
39
+ private
40
+
41
+ def change_source(mappings, source)
42
+ mappings.each { |m| m[:source] = source }
43
+ end
44
+
45
+ def engine_options(input, context)
46
+ {
47
+ filename: input[:filename],
48
+ syntax: self.class.syntax,
49
+ load_paths: input[:environment].paths,
50
+ importer: @importer_class,
51
+ source_map_embed: true,
52
+ source_map_file: '.',
53
+ sprockets: {
54
+ context: context,
55
+ environment: input[:environment],
56
+ dependencies: context.metadata[:dependencies]
57
+ }
58
+ }.merge!(@sass_config)
59
+ end
60
+ end
61
+
62
+
63
+ class ScsscProcessor < SasscProcessor
64
+ def self.syntax
65
+ :scss
66
+ end
67
+ end
68
+ end
@@ -1,3 +1,4 @@
1
+ require 'set'
1
2
  require 'time'
2
3
  require 'rack/utils'
3
4
 
@@ -6,6 +7,9 @@ module Sprockets
6
7
  # `CachedEnvironment` that provides a Rack compatible `call`
7
8
  # interface and url generation helpers.
8
9
  module Server
10
+ # Supported HTTP request methods.
11
+ ALLOWED_REQUEST_METHODS = ['GET', 'HEAD'].to_set.freeze
12
+
9
13
  # `call` implements the Rack 1.x specification which accepts an
10
14
  # `env` Hash and returns a three item tuple with the status code,
11
15
  # headers, and body.
@@ -23,7 +27,7 @@ module Sprockets
23
27
  start_time = Time.now.to_f
24
28
  time_elapsed = lambda { ((Time.now.to_f - start_time) * 1000).to_i }
25
29
 
26
- if !['GET', 'HEAD'].include?(env['REQUEST_METHOD'])
30
+ unless ALLOWED_REQUEST_METHODS.include? env['REQUEST_METHOD']
27
31
  return method_not_allowed_response
28
32
  end
29
33
 
@@ -42,19 +46,6 @@ module Sprockets
42
46
  return forbidden_response(env)
43
47
  end
44
48
 
45
- # Look up the asset.
46
- options = {}
47
- options[:pipeline] = :self if body_only?(env)
48
-
49
- asset = find_asset(path, options)
50
-
51
- # 2.x/3.x compatibility hack. Just ignore fingerprints on ?body=1 requests.
52
- # 3.x/4.x prefers strong validation of fingerprint to body contents, but
53
- # 2.x just ignored it.
54
- if asset && parse_asset_uri(asset.uri)[1][:pipeline] == "self"
55
- fingerprint = nil
56
- end
57
-
58
49
  if fingerprint
59
50
  if_match = fingerprint
60
51
  elsif env['HTTP_IF_MATCH']
@@ -65,6 +56,9 @@ module Sprockets
65
56
  if_none_match = env['HTTP_IF_NONE_MATCH'][/^"(\w+)"$/, 1]
66
57
  end
67
58
 
59
+ # Look up the asset.
60
+ asset = find_asset(path)
61
+
68
62
  if asset.nil?
69
63
  status = :not_found
70
64
  elsif fingerprint && asset.etag != fingerprint
@@ -115,7 +109,7 @@ module Sprockets
115
109
  #
116
110
  # http://example.org/assets/../../../etc/passwd
117
111
  #
118
- path.include?("..") || absolute_path?(path) || path.include?("://")
112
+ path.include?("..") || absolute_path?(path)
119
113
  end
120
114
 
121
115
  def head_request?(env)
@@ -236,11 +230,6 @@ module Sprockets
236
230
  gsub('/', '\\\\002f ')
237
231
  end
238
232
 
239
- # Test if `?body=1` or `body=true` query param is set
240
- def body_only?(env)
241
- env["QUERY_STRING"].to_s =~ /body=(1|t)/
242
- end
243
-
244
233
  def cache_headers(env, etag)
245
234
  headers = {}
246
235
 
@@ -251,11 +240,11 @@ module Sprockets
251
240
  # If the request url contains a fingerprint, set a long
252
241
  # expires on the response
253
242
  if path_fingerprint(env["PATH_INFO"])
254
- headers["Cache-Control"] += ", max-age=31536000"
243
+ headers["Cache-Control"] << ", max-age=31536000"
255
244
 
256
245
  # Otherwise set `must-revalidate` since the asset could be modified.
257
246
  else
258
- headers["Cache-Control"] += ", must-revalidate"
247
+ headers["Cache-Control"] << ", must-revalidate"
259
248
  headers["Vary"] = "Accept-Encoding"
260
249
  end
261
250
 
@@ -0,0 +1,29 @@
1
+ module Sprockets
2
+ class SourceMapCommentProcessor
3
+ def self.call(input)
4
+ case input[:content_type]
5
+ when "application/javascript"
6
+ comment = "\n//# sourceMappingURL=%s"
7
+ map_type = "application/js-sourcemap+json"
8
+ when "text/css"
9
+ comment = "\n/*# sourceMappingURL=%s */"
10
+ map_type = "application/css-sourcemap+json"
11
+ else
12
+ fail input[:content_type]
13
+ end
14
+
15
+ env = input[:environment]
16
+
17
+ uri, _ = env.resolve!(input[:filename], accept: input[:content_type])
18
+ asset = env.load(uri)
19
+
20
+ uri, _ = env.resolve!(input[:filename], accept: map_type)
21
+ map = env.load(uri)
22
+
23
+ asset.metadata.merge(
24
+ data: asset.source + (comment % map.digest_path),
25
+ links: asset.links + [asset.uri, map.uri]
26
+ )
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,40 @@
1
+ require 'set'
2
+
3
+ module Sprockets
4
+ class SourceMapProcessor
5
+ def self.call(input)
6
+ case input[:content_type]
7
+ when "application/js-sourcemap+json"
8
+ accept = "application/javascript"
9
+ when "application/css-sourcemap+json"
10
+ accept = "text/css"
11
+ else
12
+ fail input[:content_type]
13
+ end
14
+
15
+ links = Set.new(input[:metadata][:links])
16
+
17
+ env = input[:environment]
18
+
19
+ uri, _ = env.resolve!(input[:filename], accept: accept)
20
+ asset = env.load(uri)
21
+ map = asset.metadata[:map] || []
22
+
23
+ map.map { |m| m[:source] }.uniq.compact.each do |source|
24
+ # TODO: Resolve should expect fingerprints
25
+ fingerprint = source[/-([0-9a-f]{7,128})\.[^.]+\z/, 1]
26
+ if fingerprint
27
+ path = source.sub("-#{fingerprint}", "")
28
+ else
29
+ path = source
30
+ end
31
+ uri, _ = env.resolve!(path)
32
+ links << env.load(uri).uri
33
+ end
34
+
35
+ json = env.encode_json_source_map(map, filename: asset.logical_path)
36
+
37
+ { data: json, links: links }
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,345 @@
1
+ require 'json'
2
+
3
+ module Sprockets
4
+ module SourceMapUtils
5
+ extend self
6
+
7
+ # Public: Concatenate two source maps.
8
+ #
9
+ # For an example, if two js scripts are concatenated, the individual source
10
+ # maps for those files can be concatenated to map back to the originals.
11
+ #
12
+ # Examples
13
+ #
14
+ # script3 = "#{script1}#{script2}"
15
+ # map3 = concat_source_maps(map1, map2)
16
+ #
17
+ # a - Array of source mapping Hashes
18
+ # b - Array of source mapping Hashes
19
+ #
20
+ # Returns a new Array of source mapping Hashes.
21
+ def concat_source_maps(a, b)
22
+ a ||= []
23
+ b ||= []
24
+ mappings = a.dup
25
+
26
+ if a.any?
27
+ offset = a.last[:generated][0]
28
+ else
29
+ offset = 0
30
+ end
31
+
32
+ b.each do |m|
33
+ mappings << m.merge(generated: [m[:generated][0] + offset, m[:generated][1]])
34
+ end
35
+ mappings
36
+ end
37
+
38
+ # Public: Combine two seperate source map transformations into a single
39
+ # mapping.
40
+ #
41
+ # Source transformations may happen in discrete steps producing separate
42
+ # source maps. These steps can be combined into a single mapping back to
43
+ # the source.
44
+ #
45
+ # For an example, CoffeeScript may transform a file producing a map. Then
46
+ # Uglifier processes the result and produces another map. The CoffeeScript
47
+ # map can be combined with the Uglifier map so the source lines of the
48
+ # minified output can be traced back to the original CoffeeScript file.
49
+ #
50
+ # original_map = [{ :source => "index.coffee", :generated => [2, 0], :original => [1, 0] }]
51
+ # second_map = [{ :source => "index.js", :generated => [1, 1], :original => [2, 0] }]
52
+ # combine_source_maps(original_map, second_map)
53
+ # # => [{:source=>"index.coffee", :generated => [1, 1], :original => [1, 0]}]
54
+ #
55
+ # Returns a new Array of source mapping Hashes.
56
+ def combine_source_maps(original_map, second_map)
57
+ original_map ||= []
58
+ return second_map.dup if original_map.empty?
59
+
60
+ new_map = []
61
+
62
+ second_map.each do |m|
63
+ original_line = bsearch_mappings(original_map, m[:original])
64
+ next unless original_line
65
+ new_map << original_line.merge(generated: m[:generated])
66
+ end
67
+
68
+ new_map
69
+ end
70
+
71
+ # Public: Compare two source map offsets.
72
+ #
73
+ # Compatible with Array#sort.
74
+ #
75
+ # a - Array [line, column]
76
+ # b - Array [line, column]
77
+ #
78
+ # Returns -1 if a < b, 0 if a == b and 1 if a > b.
79
+ def compare_source_offsets(a, b)
80
+ diff = a[0] - b[0]
81
+ diff = a[1] - b[1] if diff == 0
82
+
83
+ if diff < 0
84
+ -1
85
+ elsif diff > 0
86
+ 1
87
+ else
88
+ 0
89
+ end
90
+ end
91
+
92
+ # Public: Search Array of mappings for closest offset.
93
+ #
94
+ # mappings - Array of mapping Hash objects
95
+ # offset - Array [line, column]
96
+ #
97
+ # Returns mapping Hash object.
98
+ def bsearch_mappings(mappings, offset, from = 0, to = mappings.size - 1)
99
+ mid = (from + to) / 2
100
+
101
+ if from > to
102
+ return from < 1 ? nil : mappings[from-1]
103
+ end
104
+
105
+ case compare_source_offsets(offset, mappings[mid][:generated])
106
+ when 0
107
+ mappings[mid]
108
+ when -1
109
+ bsearch_mappings(mappings, offset, from, mid - 1)
110
+ when 1
111
+ bsearch_mappings(mappings, offset, mid + 1, to)
112
+ end
113
+ end
114
+
115
+ # Public: Decode Source Map JSON into Ruby objects.
116
+ #
117
+ # json - String source map JSON
118
+ #
119
+ # Returns Hash.
120
+ def decode_json_source_map(json)
121
+ map = JSON.parse(json)
122
+ map['mappings'] = decode_vlq_mappings(map['mappings'], sources: map['sources'], names: map['names'])
123
+ map
124
+ end
125
+
126
+ # Public: Encode mappings to Source Map JSON.
127
+ #
128
+ # mappings - Array of Hash or String VLQ encoded mappings
129
+ # sources - Array of String sources
130
+ # names - Array of String names
131
+ # filename - String filename
132
+ #
133
+ # Returns JSON String.
134
+ def encode_json_source_map(mappings, sources: nil, names: nil, filename: nil)
135
+ case mappings
136
+ when String
137
+ when Array
138
+ sources ||= mappings.map { |m| m[:source] }.uniq.compact
139
+ names ||= mappings.map { |m| m[:name] }.uniq.compact
140
+ mappings = encode_vlq_mappings(mappings, sources: sources, names: names)
141
+ else
142
+ raise TypeError, "could not encode mappings: #{mappings}"
143
+ end
144
+
145
+ JSON.generate({
146
+ "version" => 3,
147
+ "file" => filename,
148
+ "mappings" => mappings,
149
+ "sources" => sources,
150
+ "names" => names
151
+ })
152
+ end
153
+
154
+ # Public: Decode VLQ mappings and match up sources and symbol names.
155
+ #
156
+ # str - VLQ string from 'mappings' attribute
157
+ # sources - Array of Strings from 'sources' attribute
158
+ # names - Array of Strings from 'names' attribute
159
+ #
160
+ # Returns an Array of Mappings.
161
+ def decode_vlq_mappings(str, sources: [], names: [])
162
+ mappings = []
163
+
164
+ source_id = 0
165
+ original_line = 1
166
+ original_column = 0
167
+ name_id = 0
168
+
169
+ vlq_decode_mappings(str).each_with_index do |group, index|
170
+ generated_column = 0
171
+ generated_line = index + 1
172
+
173
+ group.each do |segment|
174
+ generated_column += segment[0]
175
+ generated = [generated_line, generated_column]
176
+
177
+ if segment.size >= 4
178
+ source_id += segment[1]
179
+ original_line += segment[2]
180
+ original_column += segment[3]
181
+
182
+ source = sources[source_id]
183
+ original = [original_line, original_column]
184
+ else
185
+ # TODO: Research this case
186
+ next
187
+ end
188
+
189
+ if segment[4]
190
+ name_id += segment[4]
191
+ name = names[name_id]
192
+ end
193
+
194
+ mapping = {source: source, generated: generated, original: original}
195
+ mapping[:name] = name if name
196
+ mappings << mapping
197
+ end
198
+ end
199
+
200
+ mappings
201
+ end
202
+
203
+ # Public: Encode mappings Hash into a VLQ encoded String.
204
+ #
205
+ # mappings - Array of Hash mapping objects
206
+ # sources - Array of String sources (default: mappings source order)
207
+ # names - Array of String names (default: mappings name order)
208
+ #
209
+ # Returns a VLQ encoded String.
210
+ def encode_vlq_mappings(mappings, sources: nil, names: nil)
211
+ sources ||= mappings.map { |m| m[:source] }.uniq.compact
212
+ names ||= mappings.map { |m| m[:name] }.uniq.compact
213
+
214
+ sources_index = Hash[sources.each_with_index.to_a]
215
+ names_index = Hash[names.each_with_index.to_a]
216
+
217
+ source_id = 0
218
+ source_line = 1
219
+ source_column = 0
220
+ name_id = 0
221
+
222
+ by_lines = mappings.group_by { |m| m[:generated][0] }
223
+
224
+ ary = (1..(by_lines.keys.max || 1)).map do |line|
225
+ generated_column = 0
226
+
227
+ (by_lines[line] || []).map do |mapping|
228
+ group = []
229
+ group << mapping[:generated][1] - generated_column
230
+ group << sources_index[mapping[:source]] - source_id
231
+ group << mapping[:original][0] - source_line
232
+ group << mapping[:original][1] - source_column
233
+ group << names_index[mapping[:name]] - name_id if mapping[:name]
234
+
235
+ generated_column = mapping[:generated][1]
236
+ source_id = sources_index[mapping[:source]]
237
+ source_line = mapping[:original][0]
238
+ source_column = mapping[:original][1]
239
+ name_id = names_index[mapping[:name]] if mapping[:name]
240
+
241
+ group
242
+ end
243
+ end
244
+
245
+ vlq_encode_mappings(ary)
246
+ end
247
+
248
+ # Public: Base64 VLQ encoding
249
+ #
250
+ # Adopted from ConradIrwin/ruby-source_map
251
+ # https://github.com/ConradIrwin/ruby-source_map/blob/master/lib/source_map/vlq.rb
252
+ #
253
+ # Resources
254
+ #
255
+ # http://en.wikipedia.org/wiki/Variable-length_quantity
256
+ # https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit
257
+ # https://github.com/mozilla/source-map/blob/master/lib/source-map/base64-vlq.js
258
+ #
259
+ VLQ_BASE_SHIFT = 5
260
+ VLQ_BASE = 1 << VLQ_BASE_SHIFT
261
+ VLQ_BASE_MASK = VLQ_BASE - 1
262
+ VLQ_CONTINUATION_BIT = VLQ_BASE
263
+
264
+ BASE64_DIGITS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')
265
+ BASE64_VALUES = (0...64).inject({}) { |h, i| h[BASE64_DIGITS[i]] = i; h }
266
+
267
+ # Public: Encode a list of numbers into a compact VLQ string.
268
+ #
269
+ # ary - An Array of Integers
270
+ #
271
+ # Returns a VLQ String.
272
+ def vlq_encode(ary)
273
+ result = []
274
+ ary.each do |n|
275
+ vlq = n < 0 ? ((-n) << 1) + 1 : n << 1
276
+ loop do
277
+ digit = vlq & VLQ_BASE_MASK
278
+ vlq >>= VLQ_BASE_SHIFT
279
+ digit |= VLQ_CONTINUATION_BIT if vlq > 0
280
+ result << BASE64_DIGITS[digit]
281
+
282
+ break unless vlq > 0
283
+ end
284
+ end
285
+ result.join
286
+ end
287
+
288
+ # Public: Decode a VLQ string.
289
+ #
290
+ # str - VLQ encoded String
291
+ #
292
+ # Returns an Array of Integers.
293
+ def vlq_decode(str)
294
+ result = []
295
+ chars = str.split('')
296
+ while chars.any?
297
+ vlq = 0
298
+ shift = 0
299
+ continuation = true
300
+ while continuation
301
+ char = chars.shift
302
+ raise ArgumentError unless char
303
+ digit = BASE64_VALUES[char]
304
+ continuation = false if (digit & VLQ_CONTINUATION_BIT) == 0
305
+ digit &= VLQ_BASE_MASK
306
+ vlq += digit << shift
307
+ shift += VLQ_BASE_SHIFT
308
+ end
309
+ result << (vlq & 1 == 1 ? -(vlq >> 1) : vlq >> 1)
310
+ end
311
+ result
312
+ end
313
+
314
+ # Public: Encode a mapping array into a compact VLQ string.
315
+ #
316
+ # ary - Two dimensional Array of Integers.
317
+ #
318
+ # Returns a VLQ encoded String seperated by , and ;.
319
+ def vlq_encode_mappings(ary)
320
+ ary.map { |group|
321
+ group.map { |segment|
322
+ vlq_encode(segment)
323
+ }.join(',')
324
+ }.join(';')
325
+ end
326
+
327
+ # Public: Decode a VLQ string into mapping numbers.
328
+ #
329
+ # str - VLQ encoded String
330
+ #
331
+ # Returns an two dimensional Array of Integers.
332
+ def vlq_decode_mappings(str)
333
+ mappings = []
334
+
335
+ str.split(';').each_with_index do |group, index|
336
+ mappings[index] = []
337
+ group.split(',').each do |segment|
338
+ mappings[index] << vlq_decode(segment)
339
+ end
340
+ end
341
+
342
+ mappings
343
+ end
344
+ end
345
+ end