sprockets 3.7.3 → 4.0.0.beta1

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