source_map 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +54 -0
- data/README.md +138 -0
- data/lib/source_map.rb +64 -0
- data/lib/source_map/generator.rb +247 -0
- data/lib/source_map/parser.rb +102 -0
- data/lib/source_map/vlq.rb +122 -0
- metadata +117 -0
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.
|
data/README.md
ADDED
@@ -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>
|
data/lib/source_map.rb
ADDED
@@ -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
|
+
|