source_map 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,54 @@
1
+ Copyright (c) 2012 Conrad Irwin <conrad.irwin@gmail.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19
+
20
+ --------------------------------------------------------------------------------
21
+
22
+
23
+ Portions of this file (in particular the VLQ decoding algorithms) are based on
24
+ work from https://github.com/mozilla/source-map,
25
+
26
+ which bears the following copyright notice:
27
+
28
+ Copyright (c) 2009-2011, Mozilla Foundation and contributors
29
+ All rights reserved.
30
+
31
+ Redistribution and use in source and binary forms, with or without
32
+ modification, are permitted provided that the following conditions are met:
33
+
34
+ * Redistributions of source code must retain the above copyright notice, this
35
+ list of conditions and the following disclaimer.
36
+
37
+ * Redistributions in binary form must reproduce the above copyright notice,
38
+ this list of conditions and the following disclaimer in the documentation
39
+ and/or other materials provided with the distribution.
40
+
41
+ * Neither the names of the Mozilla Foundation nor the names of project
42
+ contributors may be used to endorse or promote products derived from this
43
+ software without specific prior written permission.
44
+
45
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
46
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
47
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
48
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
49
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
50
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
51
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
52
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
53
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
54
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,138 @@
1
+ The `source_map` gem provides an API for parsing, and an API for generating source maps in ruby.
2
+
3
+ Source maps?
4
+ ============
5
+
6
+ Source maps are Javascripts equivalent of the C `#line` functionality. They allow you to
7
+ combine multiple javascript files into one, or minify your javascript yet still debug it
8
+ as though you had done neither of these things.
9
+
10
+ To do this you attach a SourceMap to a given generated javascript file, which contains a
11
+ list of mappings between points in the generated file and points in the original files.
12
+
13
+ This gem helps you create or parse those mapping files according to the
14
+ <a href="https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit">SourceMaps version 3</a> spec.
15
+
16
+
17
+ Installing
18
+ ==========
19
+
20
+ gem install source_map
21
+
22
+
23
+ Generating a source map
24
+ =======================
25
+
26
+ Let's say you have a directory full of javascript files, but you'd prefer them to be
27
+ lumped together to avoid latency.
28
+
29
+ ```ruby
30
+
31
+ require 'source_map'
32
+
33
+ file = File.open("public/combined.js", "w")
34
+
35
+ map = SourceMap.new(:generated_output => file,
36
+ :file => "combined.js",
37
+ :source_root => "http://localhost:3000/")
38
+
39
+ Dir["js/*"].each do |filename|
40
+ map.add_generated File.read(filename), :source => filename.sub('public/', '')
41
+ end
42
+
43
+ map.save("public/combined.js.map")
44
+ ```
45
+
46
+ This snippet will create two files for you. `combined.js` which contains all your
47
+ javascripts lumped together, and `combined.js.map` which explains which bits of the file
48
+ came from where.
49
+
50
+ (Using the :generated_output feature to automatically write the combined.js file is
51
+ totally optional if you don't need that feature).
52
+
53
+ If you want more flexibility, there's an alternative API that requires you to do a bit
54
+ more manual work:
55
+
56
+ ```ruby
57
+
58
+ require 'source_map'
59
+
60
+ map = SourceMap.new(:file => 'combined.js',
61
+ :source_root => 'http://localhost:3000/')
62
+
63
+ my_crazy_process.each_fragment do |x|
64
+ map.add_mapping(
65
+ :generated_line => x.generated_line,
66
+ :generated_col => 0,
67
+ :source_line => x.source_line,
68
+ :source_col => 0
69
+ :source => "foo.js"
70
+ )
71
+ end
72
+ ```
73
+
74
+ If you use this API, you'll probably need to read
75
+ <a href="https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit">the Spec</a>.
76
+
77
+
78
+ Using a source map
79
+ ==================
80
+
81
+ You'll need Chrome version 19 or greater. Go to the developer console, and click on the
82
+ settings cog; and then click "Enable source maps".
83
+
84
+ Now, ensure that when you load `combined.js`, you also need to send an extra HTTP header:
85
+ `X-SourceMap: /combined.js.map`.
86
+
87
+ Finally ensure that eah of the source files can be reached by appending the value you
88
+ provided to `:source`, to the value you provided for `:source_root`.
89
+
90
+
91
+ NOTE: in theory you can (instead of using the `X-SourceMap` header) add a comment to the
92
+ end of your generated file (`combined.js`) which looks like:
93
+
94
+ //@ sourceMappingURL=/combined.js.map
95
+
96
+ however I haven't had much luck with this.
97
+
98
+ NOTE2: In theory you could use the Closure Inspector Firefox extension instead of Chrome
99
+ 19, but I couldn't get it to work either (even when I tried in Firefox 3.6 which is the
100
+ most recent version it supports).
101
+
102
+ Sorry this is a bit rubbish :(.
103
+
104
+
105
+ Future work
106
+ ===========
107
+
108
+ * An API to look up the position in the original source from a given position in the generated
109
+ file.
110
+
111
+ * I'd like to write a tool that given two source maps, composes them. Once that is done,
112
+ then we could pipe `combined.js` through a minifier which generates a `combined.js.min`
113
+ and a `combined.js.min.map`. And then we could combine `combined.js.map` and
114
+ `combined.js.min.map` so that we can use our concatenated and minified code with the
115
+ debugger with impunity. (The only such minifier that exists at the moment is the closure
116
+ compier, maybe that will change...)
117
+
118
+ * Supporting the index-file mode of SourceMaps (an alternative to the previous suggestion
119
+ in some circumstances)
120
+
121
+
122
+ Meta-Fu
123
+ =======
124
+
125
+ This stuff is all available under the MIT license, bug-reports and feature suggestions
126
+ welcome.
127
+
128
+
129
+ Further Reading
130
+ ===============
131
+
132
+ This stuff is quite new so there's not exactly a lot of information about it:
133
+
134
+ * <a href="https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit">the Version 3 Spec</a>.
135
+ * <a href="https://github.com/mozilla/source-map/">A javascript implementation (which helped this one)</a>
136
+ * <a href="http://peter.sh/2012/01/css-selector-profiler-source-mapping-and-software-rendering/">Announcement of feature being released into Chrome.</a>
137
+ * <a href="https://developers.google.com/closure/compiler/docs/inspector">The closure inspector was the first tool to allow reading of source maps, now seems a bit broken</a>
138
+ * <a href="https://wiki.mozilla.org/DevTools/Features/SourceMap">Implementation status at Mozilla</a>
@@ -0,0 +1,64 @@
1
+ require 'rubygems'
2
+ require 'json'
3
+
4
+ require File.expand_path("../source_map/vlq.rb", __FILE__)
5
+ require File.expand_path("../source_map/generator.rb", __FILE__)
6
+ require File.expand_path("../source_map/parser.rb", __FILE__)
7
+
8
+ class SourceMap
9
+ include SourceMap::Generator
10
+ include SourceMap::Parser
11
+
12
+ # Create a new blank SourceMap
13
+ #
14
+ # Options may include:
15
+ #
16
+ # :file => String # See {#file}
17
+ # :source_root => String # See {#source_root}
18
+ # :generated_output => IO # See {#generated_output}
19
+ #
20
+ # :sources => Array[String] # See {#sources}
21
+ # :names => Array[String] # See {#names}
22
+ #
23
+ # :version => 3 # Which version of SourceMap to use (only 3 is allowed)
24
+ #
25
+ def initialize(opts={})
26
+ unless (remain = opts.keys - [:generated_output, :file, :source_root, :sources, :names, :version]).empty?
27
+ raise ArgumentError, "Unsupported options to SourceMap.new: #{remain.inspect}"
28
+ end
29
+ self.generated_output = opts[:generated_output]
30
+ self.file = opts[:file] || ''
31
+ self.source_root = opts[:source_root] || ''
32
+ self.version = opts[:version] || 3
33
+ self.sources = opts[:sources] || []
34
+ self.names = opts[:names] || []
35
+ self.mappings = []
36
+ raise "version #{opts[:version]} not supported" if version != 3
37
+ end
38
+
39
+ # The name of the file containing the code that this SourceMap describes.
40
+ # (default "")
41
+ attr_accessor :file
42
+
43
+ # The URL/directory that contains the original source files.
44
+ #
45
+ # This is prefixed to the entries in ['sources']
46
+ # (default "")
47
+ attr_accessor :source_root
48
+
49
+ # The version of the SourceMap spec we're using.
50
+ # (default 3)
51
+ attr_accessor :version
52
+
53
+ # The list of sources (used during parsing/generating)
54
+ # These are relative to the source_root.
55
+ # (default [])
56
+ attr_accessor :sources
57
+
58
+ # A list of names (used during parsing/generating)
59
+ # (default [])
60
+ attr_accessor :names
61
+
62
+ # A list of mapping objects.
63
+ attr_accessor :mappings
64
+ end
@@ -0,0 +1,247 @@
1
+ class SourceMap
2
+ module Generator
3
+
4
+ # An object (responding to <<) that will be written to whenever
5
+ # {add_generated} is called.
6
+ #
7
+ # @example
8
+ #
9
+ # File.open("/var/www/a.js.min"){ |f|
10
+ # map = SourceMap.new(:generated_output => f)
11
+ # map.add_generated('function(a,b,c){minified=1}\n', :source => 'a.js')
12
+ # map.save('/var/www/a.js.map')
13
+ # }
14
+ # File.read('/var/www/a.js.min') == 'function(a,b,c){minified=1}\n'
15
+ #
16
+ attr_accessor :generated_output
17
+
18
+ # Add the mapping for generated code to this source map.
19
+ #
20
+ # The first parameter is the generated text that you're going to add to the output, if
21
+ # it contains multiple lines of code then it will be added to the source map as
22
+ # several mappings.
23
+ #
24
+ # If present, the second parameter represents the original source of the generated
25
+ # fragment, and may contain:
26
+ #
27
+ # :source => String, # The filename of the source fille that contains this fragment.
28
+ # :source_line => Integer, # The line in that file that contains this fragment
29
+ # :source_col => Integer, # The column in that line at which this fragment starts
30
+ # :name => String # The original name for this variable.
31
+ # :exact_position => Bool # Whether all lines in the generated fragment came from
32
+ # the same position in the source.
33
+ #
34
+ # The :source key is required to set :source_line, :source_col or :name.
35
+ #
36
+ # If unset :source_line and :source_col default to 1,0 for the first line of the
37
+ # generated fragment.
38
+ #
39
+ # Normally :source_line is incremented and :source_col reset at every line break in
40
+ # the generated code (because we assume that you're copying a verbatim fragment from
41
+ # the source into the generated code). If that is not the case, you can set
42
+ # :exact_position => true, and then all lines in the generated output will be given
43
+ # the same :source_line and :source_col.
44
+ #
45
+ # The :name property is used if the fragment you are adding contains only a name that
46
+ # you have renamed in the source transformation.
47
+ #
48
+ # If you'd like to ensure that the source map stays in sync with the generated
49
+ # source, consider calling {source_map.generated_output = StringIO.new} and then
50
+ # accessing your generated javascript with {source_map.generated_output.string},
51
+ # otherwise be careful to always write to both.
52
+ #
53
+ # NOTE: By long-standing convention, the first line of a file is numbered 1, not 0.
54
+ #
55
+ # NOTE: when generating a source map, you should either use this method always, or use
56
+ # the {#add_mapping} method always.
57
+ #
58
+ def add_generated(text, opts={})
59
+ if !opts[:source] && (opts[:name] || opts[:source_line] || opts[:source_col])
60
+ raise "mapping must have :source to have :source_line, :source_col or :name"
61
+ elsif opts[:source_line] && opts[:source_line] < 1
62
+ raise "files start on line 1 (got :source_line => #{opts[:source_line]})"
63
+ elsif !(remain = opts.keys - [:source, :source_line, :source_col, :name, :exact_position]).empty?
64
+ raise "mapping had unexpected keys: #{remain.inspect}"
65
+ end
66
+
67
+ source_line = opts[:source_line] || 1
68
+ source_col = opts[:source_col] || 0
69
+ self.generated_line ||= 1
70
+ self.generated_col ||= 0
71
+
72
+ text.split(/(\n)/).each do |line|
73
+ if line == "\n"
74
+ self.generated_line += 1
75
+ self.generated_col = 0
76
+ unless opts[:exact_position]
77
+ source_line += 1
78
+ source_col = 0
79
+ end
80
+ elsif line != ""
81
+ mapping = {
82
+ :generated_line => generated_line,
83
+ :generated_col => generated_col,
84
+ }
85
+ if opts[:source]
86
+ mapping[:source] = opts[:source]
87
+ mapping[:source_line] = source_line
88
+ mapping[:source_col] = source_col
89
+ mapping[:name] = opts[:name] if opts[:name]
90
+ end
91
+
92
+ mappings << mapping
93
+
94
+ self.generated_col += line.size
95
+ source_col += line.size unless opts[:exact_position]
96
+ end
97
+ end
98
+
99
+ generated_output << text if generated_output
100
+ end
101
+
102
+ # Add a mapping to the list for this object.
103
+ #
104
+ # A mapping identifies a fragment of code that has been moved around during
105
+ # transformation from the source file to the generated file. The fragment should
106
+ # be contiguous and not contain any line breaks.
107
+ #
108
+ # Mappings are Hashes with a valid subset of the following 6 keys:
109
+ #
110
+ # :generated_line => Integer, # The line in the generated file that contains this fragment.
111
+ # :generated_col => Integer, # The column in the generated_line that this mapping starts on
112
+ # :source => String, # The filename of the source fille that contains this fragment.
113
+ # :source_line => Integer, # The line in that file that contains this fragment.
114
+ # :source_col => Integer, # The column in that line at which this fragment starts.
115
+ # :name => String # The original name for this variable (if applicable).
116
+ #
117
+ #
118
+ # The only 3 valid subsets of keys are:
119
+ # [:generated_line, :generated_col] To indicate that this is a fragment in the
120
+ # output file that you don't have the source for.
121
+ #
122
+ # [:generated_line, :generated_col, :source, :source_line, :source_col] To indicate
123
+ # that this is a fragment in the output file that you do have the source for.
124
+ #
125
+ # [:generated_line, :generated_col, :source, :source_line, :source_col, :name] To
126
+ # indicate that this is a particular identifier at a particular location in the original.
127
+ #
128
+ # Any other combination of keys would produce an invalid source map.
129
+ #
130
+ # NOTE: By long-standing convention, the first line of a file is numbered 1, not 0.
131
+ #
132
+ # NOTE: when generating a source map, you should either use this method always,
133
+ # or use the {#add_generated} method always.
134
+ #
135
+ def add_mapping(map)
136
+ if !map[:generated_line] || !map[:generated_col]
137
+ raise "mapping must have :generated_line and :generated_col"
138
+ elsif map[:source] && !(map[:source_line] && map[:source_col])
139
+ raise "mapping must have :source_line and :source_col if it has :source"
140
+ elsif !map[:source] && (map[:source_line] || map[:source_col])
141
+ raise "mapping may not have a :source_line or :source_col without a :source"
142
+ elsif map[:name] && !map[:source]
143
+ raise "mapping may not have a :name without a :source"
144
+ elsif map[:source_line] && map[:source_line] < 1
145
+ raise "files start on line 1 (got :source_line => #{map[:source_line]})"
146
+ elsif map[:generated_line] < 1
147
+ raise "files start on line 1 (got :generated_line => #{map[:generated_line]})"
148
+ elsif !(remain = map.keys - [:generated_line, :generated_col, :source, :source_line, :source_col, :name]).empty?
149
+ raise "mapping had unexpected keys: #{remain.inspect}"
150
+ end
151
+
152
+ mappings << map
153
+ end
154
+
155
+ # Convert the map into an object suitable for direct serialisation.
156
+ def as_json
157
+ serialized_mappings = serialize_mappings!
158
+
159
+ {
160
+ 'version' => version,
161
+ 'file' => file,
162
+ 'sourceRoot' => source_root,
163
+ 'sources' => sources,
164
+ 'names' => names,
165
+ 'mappings' => serialized_mappings
166
+ }
167
+ end
168
+
169
+ # Convert the map to a string.
170
+ def to_s
171
+ as_json.to_json
172
+ end
173
+
174
+ # Write this map to a file.
175
+ def save(file)
176
+ File.open(file, "w"){ |f| f << to_s }
177
+ end
178
+
179
+ protected
180
+
181
+ attr_reader :source_ids, :name_ids
182
+ attr_accessor :generated_line, :generated_col
183
+
184
+ # Get the id for the given file. If we've not
185
+ # seen this file before, add it to the list.
186
+ def source_id(file)
187
+ source_ids[file] ||= (
188
+ sources << file
189
+ sources.size - 1
190
+ )
191
+ end
192
+
193
+ # Get the id for the given name. If we've not
194
+ # seen this name before, add it to the list.
195
+ def name_id(name)
196
+ name_ids[name] ||= (
197
+ names << name
198
+ names.size - 1
199
+ )
200
+ end
201
+
202
+ # Encode a vlq. As each field in the output should be relative to the
203
+ # previous occurance of that field, we keep track of each one.
204
+ def vlq(num, type)
205
+ ret = num - @previous_vlq[type]
206
+ @previous_vlq[type] = num
207
+ VLQ.encode(ret)
208
+ end
209
+
210
+ # Serialize the list of mappings into the string of base64 variable length
211
+ # quanities. As a side-effect, regenerate the sources and names arrays.
212
+ def serialize_mappings!
213
+ # clear all internals as we're about to re-generate them.
214
+ @sources = []
215
+ @source_ids = {}
216
+ @names = []
217
+ @name_ids = {}
218
+ @previous_vlq = Hash.new{ 0 }
219
+
220
+ return "" if mappings.empty?
221
+
222
+ by_lines = mappings.group_by{ |x| x[:generated_line] }
223
+
224
+ (1..by_lines.keys.max).map do |line|
225
+ # reset the generated_col on each line as indicated by the VLQ spec.
226
+ # (the other values continue to be relative)
227
+ @previous_vlq[:generated_col] = 0
228
+
229
+ fragments = (by_lines[line] || []).sort_by{ |x| x[:generated_col] }
230
+ fragments.map do |map|
231
+ serialize_mapping(map)
232
+ end.join(",")
233
+ end.join(";")
234
+ end
235
+
236
+ def serialize_mapping(map)
237
+ item = vlq(map[:generated_col], :generated_col)
238
+ if map[:source]
239
+ item << vlq(source_id(map[:source]), :source)
240
+ item << vlq(map[:source_line] - 1, :source_line)
241
+ item << vlq(map[:source_col], :source_col)
242
+ item << vlq(name_id(map[:name]), :name) if map[:name]
243
+ end
244
+ item
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,102 @@
1
+ class SourceMap
2
+
3
+ class ParserError < RuntimeError; end
4
+
5
+ # Load a SourceMap from a Hash such as might be returned by
6
+ # {SourceMap#as_json}.
7
+ #
8
+ def self.from_json(json)
9
+ raise ParserError, "Cannot parse version: #{json['version']} of SourceMap" unless json['version'] == 3
10
+
11
+ map = new(:file => json['file'],
12
+ :source_root => json['sourceRoot'],
13
+ :sources => json['sources'],
14
+ :names => json['names'])
15
+
16
+ map.parse_mappings(json['mappings'] || '')
17
+ map
18
+ end
19
+
20
+ # Load a SourceMap from a String.
21
+ def self.from_s(str)
22
+ from_json JSON.parse(str)
23
+ end
24
+
25
+ # Load a SourceMap from a file.
26
+ def self.load(filename)
27
+ from_s File.read(filename)
28
+ end
29
+
30
+ module Parser
31
+ # Parse the mapping string from a SourceMap.
32
+ #
33
+ # The mappings string contains one comma-separated list of segments per line
34
+ # in the output file, these lists are joined by semi-colons.
35
+ #
36
+ def parse_mappings(string)
37
+ @previous = Hash.new{ 0 }
38
+
39
+ string.split(";").each_with_index do |line, line_idx|
40
+ # The generated_col resets to 0 at the start of every line, though
41
+ # all the other differences are maintained.
42
+ @previous[:generated_col] = 0
43
+ line.split(",").each do |segment|
44
+ mappings << parse_mapping(segment, line_idx + 1)
45
+ end
46
+ end
47
+
48
+ self.mappings = self.mappings.sort_by{ |x| [x[:generated_line], x[:generated_col]] }
49
+ end
50
+
51
+ # All the numbers in SourceMaps are stored as differences from each other,
52
+ # so we need to remove the difference every time we read a number.
53
+ def undiff(int, type)
54
+ @previous[type] += int
55
+ end
56
+
57
+ # Parse an individual mapping.
58
+ #
59
+ # This is a list of variable-length-quanitity, with 1, 4 or 5 items. See the spec
60
+ # https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit
61
+ # for more details.
62
+ def parse_mapping(segment, line_num)
63
+ item = VLQ.decode_array(segment)
64
+
65
+ unless [1, 4, 5].include?(item.size)
66
+ raise ParserError, "In map for #{file}:#{line_num}: unparseable item: #{segment}"
67
+ end
68
+
69
+ map = {
70
+ :generated_line => line_num,
71
+ :generated_col => undiff(item[0], :generated_col),
72
+ }
73
+
74
+ if item.size >= 4
75
+ map[:source] = sources[undiff(item[1], :source_id)]
76
+ map[:source_line] = undiff(item[2], :source_line) + 1 # line numbers are stored starting from 0
77
+ map[:source_col] = undiff(item[3], :source_col)
78
+ map[:name] = names[undiff(item[4], :name_id)] if item[4]
79
+ end
80
+
81
+ if map[:generated_col] < 0
82
+ raise ParserError, "In map for #{file}:#{line_num}: unexpected generated_col: #{map[:generated_col]}"
83
+
84
+ elsif map.key?(:source) && (map[:source].nil? || @previous[:source_id] < 0)
85
+ raise ParserError, "In map for #{file}:#{line_num}: unknown source id: #{@previous[:source_id]}"
86
+
87
+ elsif map.key?(:source_line) && map[:source_line] < 1
88
+ raise ParserError, "In map for #{file}:#{line_num}: unexpected source_line: #{map[:source_line]}"
89
+
90
+ elsif map.key?(:source_col) && map[:source_col] < 0
91
+ raise ParserError, "In map for #{file}:#{line_num}: unexpected source_col: #{map[:source_col]}"
92
+
93
+ elsif map.key?(:name) && (map[:name].nil? || @previous[:name_id] < 0)
94
+ raise ParserError, "In map for #{file}:#{line_num}: unknown name id: #{@previous[:name_id]}"
95
+
96
+ else
97
+ map
98
+
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,122 @@
1
+ class SourceMap
2
+ # Support for encoding/decoding the variable length quantity format
3
+ # described in the spec at:
4
+ #
5
+ # https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit
6
+ #
7
+ # This implementation is heavily based on https://github.com/mozilla/source-map
8
+ # Copyright 2009-2011, Mozilla Foundation and contributors, BSD
9
+ #
10
+ module VLQ
11
+
12
+ # A single base 64 digit can contain 6 bits of data. For the base 64 variable
13
+ # length quantities we use in the source map spec, the first bit is the sign,
14
+ # the next four bits are the actual value, and the 6th bit is the
15
+ # continuation bit. The continuation bit tells us whether there are more
16
+ # digits in this value following this digit.
17
+ #
18
+ # Continuation
19
+ # | Sign
20
+ # | |
21
+ # V V
22
+ # 101011
23
+
24
+ VLQ_BASE_SHIFT = 5;
25
+
26
+ # binary: 100000
27
+ VLQ_BASE = 1 << VLQ_BASE_SHIFT;
28
+
29
+ # binary: 011111
30
+ VLQ_BASE_MASK = VLQ_BASE - 1;
31
+
32
+ # binary: 100000
33
+ VLQ_CONTINUATION_BIT = VLQ_BASE;
34
+
35
+ BASE64_DIGITS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')
36
+ BASE64_VALUES = (0..64).inject({}){ |h, i| h.update BASE64_DIGITS[i] => i }
37
+
38
+ # Returns the base 64 VLQ encoded value.
39
+ def self.encode(int)
40
+
41
+ vlq = to_vlq_signed(int)
42
+ encoded = ""
43
+
44
+ begin
45
+ digit = vlq & VLQ_BASE_MASK
46
+ vlq >>= VLQ_BASE_SHIFT
47
+ digit |= VLQ_CONTINUATION_BIT if vlq > 0
48
+ encoded << base64_encode(digit)
49
+ end while vlq > 0
50
+
51
+ encoded
52
+ end
53
+
54
+ # Decodes the next base 64 VLQ value from the given string and returns the
55
+ # value and the rest of the string.
56
+ def self.decode(str)
57
+
58
+ vlq = 0
59
+ shift = 0
60
+ continue = true
61
+ chars = str.split('')
62
+
63
+ while continue
64
+ char = chars.shift or raise "Expected more digits in base 64 VLQ value."
65
+ digit = base64_decode(char)
66
+ continue = false if (digit & VLQ_CONTINUATION_BIT) == 0
67
+ digit &= VLQ_BASE_MASK
68
+ vlq += digit << shift
69
+ shift += VLQ_BASE_SHIFT
70
+ end
71
+
72
+ [from_vlq_signed(vlq), chars.join('')]
73
+ end
74
+
75
+ # Decode an array of variable length quantities from the given string and
76
+ # return them.
77
+ def self.decode_array(str)
78
+ output = []
79
+ while str != ''
80
+ int, str = decode(str)
81
+ output << int
82
+ end
83
+ output
84
+ end
85
+
86
+ protected
87
+
88
+ def self.base64_encode(int)
89
+ BASE64_DIGITS[int] or raise ArgumentError, "#{int} is not a valid base64 digit"
90
+ end
91
+
92
+ def self.base64_decode(char)
93
+ BASE64_VALUES[char] or raise ArgumentError, "#{char} is not a valid base64 digit"
94
+ end
95
+
96
+ # Converts from a two's-complement integer to an integer where the
97
+ # sign bit is placed in the least significant bit. For example, as decimals:
98
+ # 1 becomes 2 (10 binary), -1 becomes 3 (11 binary)
99
+ # 2 becomes 4 (100 binary), -2 becomes 5 (101 binary)
100
+ def self.to_vlq_signed(int)
101
+ if int < 0
102
+ ((-int) << 1) + 1
103
+ else
104
+ int << 1
105
+ end
106
+ end
107
+
108
+ # Converts to a two's-complement value from a value where the sign bit is
109
+ # placed in the least significant bit. For example, as decimals:
110
+ #
111
+ # 2 (10 binary) becomes 1, 3 (11 binary) becomes -1
112
+ # 4 (100 binary) becomes 2, 5 (101 binary) becomes -2
113
+ def self.from_vlq_signed(vlq)
114
+ if vlq & 1 == 1
115
+ -(vlq >> 1)
116
+ else
117
+ vlq >> 1
118
+ end
119
+ end
120
+
121
+ end
122
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: source_map
3
+ version: !ruby/object:Gem::Version
4
+ hash: 5
5
+ prerelease: false
6
+ segments:
7
+ - 3
8
+ - 0
9
+ - 1
10
+ version: 3.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Conrad Irwin
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-02-28 00:00:00 -08:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: json
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: rake
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :development
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: rspec
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :development
62
+ version_requirements: *id003
63
+ description: " Ruby support for Source Maps allows you to interact with Source Maps in Ruby. This\n lets you do things like concatenate different javascript files and still debug them\n as though they were separate files.\n\n See the spec for more information:\n\
64
+ https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit\n"
65
+ email:
66
+ - conrad.irwin@gmail.com
67
+ executables: []
68
+
69
+ extensions: []
70
+
71
+ extra_rdoc_files: []
72
+
73
+ files:
74
+ - lib/source_map.rb
75
+ - lib/source_map/vlq.rb
76
+ - lib/source_map/generator.rb
77
+ - lib/source_map/parser.rb
78
+ - LICENSE
79
+ - README.md
80
+ has_rdoc: true
81
+ homepage: http://github.com/ConradIrwin/ruby-source_map
82
+ licenses:
83
+ - MIT
84
+ post_install_message:
85
+ rdoc_options: []
86
+
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ hash: 57
95
+ segments:
96
+ - 1
97
+ - 8
98
+ - 7
99
+ version: 1.8.7
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ hash: 3
106
+ segments:
107
+ - 0
108
+ version: "0"
109
+ requirements: []
110
+
111
+ rubyforge_project:
112
+ rubygems_version: 1.3.7
113
+ signing_key:
114
+ specification_version: 3
115
+ summary: Ruby support for source_maps (version 3)
116
+ test_files: []
117
+