├── lib ├── sourcemap.rb ├── source_map │ ├── version.rb │ ├── mapping.rb │ ├── offset.rb │ ├── vlq.rb │ └── map.rb └── source_map.rb ├── .gitignore ├── Gemfile ├── .travis.yml ├── examples ├── foobar.html ├── bar.coffee ├── bar.map ├── foo.coffee ├── foo.min.js ├── foo.map ├── bar.js ├── foo.min.map ├── foobar.map ├── foo.js └── foobar.js ├── Rakefile ├── sourcemap.gemspec ├── LICENSE ├── test ├── test_mapping.rb ├── test_offset.rb ├── test_vlq.rb └── test_map.rb └── README.md /lib/sourcemap.rb: -------------------------------------------------------------------------------- 1 | require 'source_map' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | .DS_Store 3 | pkg 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /lib/source_map/version.rb: -------------------------------------------------------------------------------- 1 | module SourceMap 2 | VERSION = "0.1.1" 3 | end 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 1.9.3 5 | - 2.0.0 6 | - 2.1 7 | 8 | notifications: 9 | email: false 10 | -------------------------------------------------------------------------------- /examples/foobar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /lib/source_map.rb: -------------------------------------------------------------------------------- 1 | require 'source_map/map' 2 | require 'source_map/mapping' 3 | require 'source_map/offset' 4 | require 'source_map/version' 5 | require 'source_map/vlq' 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | 4 | task :default => :test 5 | 6 | Rake::TestTask.new do |t| 7 | t.libs << 'test' 8 | t.warning = true 9 | end -------------------------------------------------------------------------------- /examples/bar.coffee: -------------------------------------------------------------------------------- 1 | # Eat lunch. 2 | eat food for food in ['toast', 'cheese', 'wine'] 3 | 4 | # Fine five course dining. 5 | courses = ['greens', 'caviar', 'truffles', 'roast', 'cake'] 6 | menu i + 1, dish for dish, i in courses 7 | 8 | # Health conscious meal. 9 | foods = ['broccoli', 'spinach', 'chocolate'] 10 | eat food for food in foods when food isnt 'chocolate' 11 | -------------------------------------------------------------------------------- /examples/bar.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "file": "bar.js", 4 | "sourceRoot": "", 5 | "sources": [ 6 | "bar.coffee" 7 | ], 8 | "names": [], 9 | "mappings": ";AACA;CAAA,KAAA,6DAAA;;CAAA;CAAA,MAAA,oCAAA;qBAAA;CAAA,EAAA,CAAA;CAAA,EAAA;;CAAA,CAGA,CAAU,GAAA,CAAV,CAAU,EAAA;;AACV,CAAA,MAAA,iDAAA;uBAAA;CAAA,CAAY,CAAH,CAAT;CAAA,EAJA;;CAAA,CAOA,CAAQ,EAAR,IAAQ,CAAA,CAAA;;AACR,CAAA,MAAA,uCAAA;sBAAA;IAAgC,CAAU;CAA1C,EAAA,CAAA,EAAA;MAAA;CAAA,EARA;CAAA" 10 | } -------------------------------------------------------------------------------- /examples/foo.coffee: -------------------------------------------------------------------------------- 1 | # Assignment: 2 | number = 42 3 | opposite = true 4 | 5 | # Conditions: 6 | number = -42 if opposite 7 | 8 | # Functions: 9 | square = (x) -> x * x 10 | 11 | # Arrays: 12 | list = [1, 2, 3, 4, 5] 13 | 14 | # Objects: 15 | math = 16 | root: Math.sqrt 17 | square: square 18 | cube: (x) -> x * square x 19 | 20 | # Splats: 21 | race = (winner, runners...) -> 22 | print winner, runners 23 | 24 | # Existence: 25 | alert "I knew it!" if elvis? 26 | 27 | # Array comprehensions: 28 | cubes = (math.cube num for num in list) 29 | -------------------------------------------------------------------------------- /examples/foo.min.js: -------------------------------------------------------------------------------- 1 | !function(){var cubes,list,math,num,number,opposite,race,square,__slice=[].slice;number=42;opposite=true;if(opposite){number=-42}square=function(x){return x*x};list=[1,2,3,4,5];math={root:Math.sqrt,square:square,cube:function(x){return x*square(x)}};race=function(){var runners,winner;winner=arguments[0],runners=2<=arguments.length?__slice.call(arguments,1):[];return print(winner,runners)};if(typeof elvis!=="undefined"&&elvis!==null){alert("I knew it!")}cubes=function(){var _i,_len,_results;_results=[];for(_i=0,_len=list.length;_i<_len;_i++){num=list[_i];_results.push(math.cube(num))}return _results}()}.call(this); 2 | //@ sourceMappingURL=foo.min.map 3 | -------------------------------------------------------------------------------- /examples/foo.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "file": "foo.js", 4 | "sourceRoot": "", 5 | "sources": [ 6 | "foo.coffee" 7 | ], 8 | "names": [], 9 | "mappings": ";AACA;CAAA,KAAA,gDAAA;KAAA,aAAA;;CAAA,CAAA,CAAW,GAAX;;CAAA,CACA,CAAW,CADX,IACA;;CAGA,CAAA,EAAgB,IAAhB;AAAU,CAAV,CAAA,CAAS,CAAT,EAAA;IAJA;;CAAA,CAOA,CAAS,GAAT,GAAU;CAAM,EAAI,QAAJ;CAPhB,EAOS;;CAPT,CAUA,CAAO,CAAP;;CAVA,CAaA,CACE,CADF;CACE,CAAQ,EAAR;CAAA,CACQ,EAAR,EAAA;CADA,CAEQ,CAAA,CAAR,KAAS;CAAM,EAAI,GAAA,OAAJ;CAFf,IAEQ;CAhBV,GAAA;;CAAA,CAmBA,CAAO,CAAP,KAAO;CACL,OAAA,OAAA;CAAA,CADc,EAAR,mDACN;CAAM,CAAQ,GAAd,CAAA,CAAA,IAAA;CApBF,EAmBO;;CAIP,CAAA,EAAsB,0CAAtB;CAAA,GAAA,CAAA,OAAA;IAvBA;;CAAA,CA0BA,GAAA;;AAAS,CAAA;UAAA,iCAAA;sBAAA;CAAA,EAAA,CAAI;CAAJ;;CA1BT;CAAA" 10 | } -------------------------------------------------------------------------------- /examples/bar.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | (function() { 3 | var courses, dish, food, foods, i, _i, _j, _k, _len, _len1, _len2, _ref; 4 | 5 | _ref = ['toast', 'cheese', 'wine']; 6 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 7 | food = _ref[_i]; 8 | eat(food); 9 | } 10 | 11 | courses = ['greens', 'caviar', 'truffles', 'roast', 'cake']; 12 | 13 | for (i = _j = 0, _len1 = courses.length; _j < _len1; i = ++_j) { 14 | dish = courses[i]; 15 | menu(i + 1, dish); 16 | } 17 | 18 | foods = ['broccoli', 'spinach', 'chocolate']; 19 | 20 | for (_k = 0, _len2 = foods.length; _k < _len2; _k++) { 21 | food = foods[_k]; 22 | if (food !== 'chocolate') { 23 | eat(food); 24 | } 25 | } 26 | 27 | }).call(this); 28 | -------------------------------------------------------------------------------- /lib/source_map/mapping.rb: -------------------------------------------------------------------------------- 1 | require 'source_map/offset' 2 | 3 | module SourceMap 4 | class Mapping < Struct.new(:source, :generated, :original, :name) 5 | # Public: Get a simple string representation of the mapping. 6 | # 7 | # Returns a String. 8 | def to_s 9 | str = "#{generated.line}:#{generated.column}" 10 | str << "->#{source}@#{original.line}:#{original.column}" 11 | str << "##{name}" if name 12 | str 13 | end 14 | 15 | # Public: Get a pretty inspect output for debugging purposes. 16 | # 17 | # Returns a String. 18 | def inspect 19 | str = "#<#{self.class} source=#{source.inspect}" 20 | str << " generated=#{generated}, original=#{original}" 21 | str << " name=#{name.inspect}" if name 22 | str << ">" 23 | str 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /examples/foo.min.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["foo.js"],"names":["cubes","list","math","num","number","opposite","race","square","__slice","slice","x","root","Math","sqrt","cube","runners","winner","arguments","length","call","print","elvis","alert","_i","_len","_results","push","this"],"mappings":"CACA,WACE,GAAIA,OAAOC,KAAMC,KAAMC,IAAKC,OAAQC,SAAUC,KAAMC,OAClDC,WAAaC,KAEfL,QAAS,EAETC,UAAW,IAEX,IAAIA,SAAU,CACZD,QAAU,GAGZG,OAAS,SAASG,GAChB,MAAOA,GAAIA,EAGbT,OAAQ,EAAG,EAAG,EAAG,EAAG,EAEpBC,OACES,KAAMC,KAAKC,KACXN,OAAQA,OACRO,KAAM,SAASJ,GACb,MAAOA,GAAIH,OAAOG,IAItBJ,MAAO,WACL,GAAIS,SAASC,MACbA,QAASC,UAAU,GAAIF,QAAU,GAAKE,UAAUC,OAASV,QAAQW,KAAKF,UAAW,KACjF,OAAOG,OAAMJ,OAAQD,SAGvB,UAAWM,SAAU,aAAeA,QAAU,KAAM,CAClDC,MAAM,cAGRtB,MAAQ,WACN,GAAIuB,IAAIC,KAAMC,QACdA,YACA,KAAKF,GAAK,EAAGC,KAAOvB,KAAKiB,OAAQK,GAAKC,KAAMD,KAAM,CAChDpB,IAAMF,KAAKsB,GACXE,UAASC,KAAKxB,KAAKY,KAAKX,MAE1B,MAAOsB,cAGRN,KAAKQ"} 2 | -------------------------------------------------------------------------------- /examples/foobar.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":null,"mappings":";AACA;CAAA,KAAA,gDAAA;KAAA,aAAA;;CAAA,CAAA,CAAW,GAAX;;CAAA,CACA,CAAW,CADX,IACA;;CAGA,CAAA,EAAgB,IAAhB;AAAU,CAAV,CAAA,CAAS,CAAT,EAAA;IAJA;;CAAA,CAOA,CAAS,GAAT,GAAU;CAAM,EAAI,QAAJ;CAPhB,EAOS;;CAPT,CAUA,CAAO,CAAP;;CAVA,CAaA,CACE,CADF;CACE,CAAQ,EAAR;CAAA,CACQ,EAAR,EAAA;CADA,CAEQ,CAAA,CAAR,KAAS;CAAM,EAAI,GAAA,OAAJ;CAFf,IAEQ;CAhBV,GAAA;;CAAA,CAmBA,CAAO,CAAP,KAAO;CACL,OAAA,OAAA;CAAA,CADc,EAAR,mDACN;CAAM,CAAQ,GAAd,CAAA,CAAA,IAAA;CApBF,EAmBO;;CAIP,CAAA,EAAsB,0CAAtB;CAAA,GAAA,CAAA,OAAA;IAvBA;;CAAA,CA0BA,GAAA;;AAAS,CAAA;UAAA,iCAAA;sBAAA;CAAA,EAAA,CAAI;CAAJ;;CA1BT;CAAA;;;ACAA;CAAA,KAAA,6DAAA;;CAAA;CAAA,MAAA,oCAAA;qBAAA;CAAA,EAAA,CAAA;CAAA,EAAA;;CAAA,CAGA,CAAU,GAAA,CAAV,CAAU,EAAA;;AACV,CAAA,MAAA,iDAAA;uBAAA;CAAA,CAAY,CAAH,CAAT;CAAA,EAJA;;CAAA,CAOA,CAAQ,EAAR,IAAQ,CAAA,CAAA;;AACR,CAAA,MAAA,uCAAA;sBAAA;IAAgC,CAAU;CAA1C,EAAA,CAAA,EAAA;MAAA;CAAA,EARA;CAAA;","sources":["foo.coffee","bar.coffee"],"names":[]} 2 | -------------------------------------------------------------------------------- /sourcemap.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'source_map/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "sourcemap" 8 | spec.version = SourceMap::VERSION 9 | spec.authors = ["Josh Peek", "Alex MacCaw"] 10 | spec.email = ["alex@alexmaccaw.com"] 11 | spec.description = %q{Ruby source maps} 12 | spec.summary = %q{Ruby source maps} 13 | spec.homepage = "http://github.com/maccman/sourcemap" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 1.3" 22 | spec.add_development_dependency "rake" 23 | spec.add_development_dependency "minitest" 24 | end 25 | -------------------------------------------------------------------------------- /examples/foo.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | (function() { 3 | var cubes, list, math, num, number, opposite, race, square, 4 | __slice = [].slice; 5 | 6 | number = 42; 7 | 8 | opposite = true; 9 | 10 | if (opposite) { 11 | number = -42; 12 | } 13 | 14 | square = function(x) { 15 | return x * x; 16 | }; 17 | 18 | list = [1, 2, 3, 4, 5]; 19 | 20 | math = { 21 | root: Math.sqrt, 22 | square: square, 23 | cube: function(x) { 24 | return x * square(x); 25 | } 26 | }; 27 | 28 | race = function() { 29 | var runners, winner; 30 | winner = arguments[0], runners = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 31 | return print(winner, runners); 32 | }; 33 | 34 | if (typeof elvis !== "undefined" && elvis !== null) { 35 | alert("I knew it!"); 36 | } 37 | 38 | cubes = (function() { 39 | var _i, _len, _results; 40 | _results = []; 41 | for (_i = 0, _len = list.length; _i < _len; _i++) { 42 | num = list[_i]; 43 | _results.push(math.cube(num)); 44 | } 45 | return _results; 46 | })(); 47 | 48 | }).call(this); 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Alex MacCaw, Joshua Peek 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/test_mapping.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'source_map/offset' 3 | 4 | class TestMapping < MiniTest::Test 5 | include SourceMap 6 | 7 | def setup 8 | @mapping = Mapping.new("a.js", Offset.new(1, 5), Offset.new(2, 0), "foo") 9 | end 10 | 11 | def test_equal 12 | assert @mapping.dup == @mapping 13 | assert Mapping.new("b.js", Offset.new(1, 5), Offset.new(2, 0), "foo") != @mapping 14 | assert Mapping.new("a.js", Offset.new(1, 5), Offset.new(2, 0), "bar") != @mapping 15 | assert Mapping.new("a.js", Offset.new(1, 6), Offset.new(2, 0), "foo") != @mapping 16 | assert Mapping.new("a.js", Offset.new(1, 5), Offset.new(3, 0), "foo") != @mapping 17 | end 18 | 19 | def test_source 20 | assert_equal "a.js", @mapping.source 21 | end 22 | 23 | def test_generated 24 | assert_equal Offset.new(1, 5), @mapping.generated 25 | end 26 | 27 | def test_original 28 | assert_equal Offset.new(2, 0), @mapping.original 29 | end 30 | 31 | def test_name 32 | assert_equal "foo", @mapping.name 33 | end 34 | 35 | def test_to_s 36 | assert_equal "1:5->a.js@2:0#foo", @mapping.to_s 37 | end 38 | 39 | def test_inspect 40 | assert_equal "#", @mapping.inspect 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/test_offset.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'source_map/offset' 3 | 4 | class TestOffset < MiniTest::Test 5 | include SourceMap 6 | 7 | def setup 8 | @offset = Offset.new(1, 5) 9 | end 10 | 11 | def test_equal 12 | assert Offset.new(1, 5) == @offset 13 | assert Offset.new(1, 6) != @offset 14 | assert Offset.new(2, 5) != @offset 15 | end 16 | 17 | def test_from_array 18 | assert Offset.new(1, 5) == Offset.new([1, 5]) 19 | end 20 | 21 | def test_from_offset 22 | assert @offset == Offset.new(@offset) 23 | end 24 | 25 | def test_line 26 | assert_equal 1, @offset.line 27 | end 28 | 29 | def test_column 30 | assert_equal 5, @offset.column 31 | end 32 | 33 | def test_to_s 34 | assert_equal "0", Offset.new(0, 0).to_s 35 | assert_equal "1", Offset.new(1, 0).to_s 36 | assert_equal "1:5", Offset.new(1, 5).to_s 37 | end 38 | 39 | def test_inspect 40 | assert_equal "#", @offset.inspect 41 | end 42 | 43 | def test_add_offset 44 | offset = @offset + Offset.new(2, 1) 45 | assert_equal 3, offset.line 46 | assert_equal 6, offset.column 47 | end 48 | 49 | def test_add_line 50 | offset = @offset + 5 51 | assert_equal 6, offset.line 52 | assert_equal 5, offset.column 53 | end 54 | 55 | def test_compare 56 | assert @offset < Offset.new(2, 0) 57 | assert @offset < Offset.new(1, 6) 58 | assert @offset > Offset.new(1, 4) 59 | assert @offset >= Offset.new(1, 5) 60 | assert @offset <= Offset.new(1, 5) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /examples/foobar.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | (function() { 3 | var cubes, list, math, num, number, opposite, race, square, 4 | __slice = [].slice; 5 | 6 | number = 42; 7 | 8 | opposite = true; 9 | 10 | if (opposite) { 11 | number = -42; 12 | } 13 | 14 | square = function(x) { 15 | return x * x; 16 | }; 17 | 18 | list = [1, 2, 3, 4, 5]; 19 | 20 | math = { 21 | root: Math.sqrt, 22 | square: square, 23 | cube: function(x) { 24 | return x * square(x); 25 | } 26 | }; 27 | 28 | race = function() { 29 | var runners, winner; 30 | winner = arguments[0], runners = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 31 | return print(winner, runners); 32 | }; 33 | 34 | if (typeof elvis !== "undefined" && elvis !== null) { 35 | alert("I knew it!"); 36 | } 37 | 38 | cubes = (function() { 39 | var _i, _len, _results; 40 | _results = []; 41 | for (_i = 0, _len = list.length; _i < _len; _i++) { 42 | num = list[_i]; 43 | _results.push(math.cube(num)); 44 | } 45 | return _results; 46 | })(); 47 | 48 | }).call(this); 49 | // Generated by CoffeeScript 1.6.3 50 | (function() { 51 | var courses, dish, food, foods, i, _i, _j, _k, _len, _len1, _len2, _ref; 52 | 53 | _ref = ['toast', 'cheese', 'wine']; 54 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 55 | food = _ref[_i]; 56 | eat(food); 57 | } 58 | 59 | courses = ['greens', 'caviar', 'truffles', 'roast', 'cake']; 60 | 61 | for (i = _j = 0, _len1 = courses.length; _j < _len1; i = ++_j) { 62 | dish = courses[i]; 63 | menu(i + 1, dish); 64 | } 65 | 66 | foods = ['broccoli', 'spinach', 'chocolate']; 67 | 68 | for (_k = 0, _len2 = foods.length; _k < _len2; _k++) { 69 | food = foods[_k]; 70 | if (food !== 'chocolate') { 71 | eat(food); 72 | } 73 | } 74 | 75 | }).call(this); 76 | 77 | //@ sourceMappingURL=foobar.map 78 | -------------------------------------------------------------------------------- /lib/source_map/offset.rb: -------------------------------------------------------------------------------- 1 | module SourceMap 2 | # Public: Offset is an immutable structure representing a position in 3 | # a source file. 4 | class Offset 5 | include Comparable 6 | 7 | # Public: Construct Offset value. 8 | # 9 | # Returns Offset instance. 10 | def self.new(*args) 11 | case args.first 12 | when Offset 13 | args.first 14 | when Array 15 | super(*args.first) 16 | else 17 | super(*args) 18 | end 19 | end 20 | 21 | # Public: Initialize an Offset. 22 | # 23 | # line - Integer line number 24 | # column - Integer column number 25 | def initialize(line, column) 26 | @line, @column = line, column 27 | end 28 | 29 | # Public: Gets Integer line of offset 30 | attr_reader :line 31 | 32 | # Public: Get Integer column of offset 33 | attr_reader :column 34 | 35 | # Public: Shift the offset by some value. 36 | # 37 | # other - An Offset to add by its line and column 38 | # Or an Integer to add by line 39 | # 40 | # Returns a new Offset instance. 41 | def +(other) 42 | case other 43 | when Offset 44 | Offset.new(self.line + other.line, self.column + other.column) 45 | when Integer 46 | Offset.new(self.line + other, self.column) 47 | else 48 | raise ArgumentError, "can't convert #{other} into #{self.class}" 49 | end 50 | end 51 | 52 | # Public: Compare Offset to another. 53 | # 54 | # Useful for determining if a position in a few is between two offsets. 55 | # 56 | # other - Another Offset 57 | # 58 | # Returns a negative number when other is smaller and a positive number 59 | # when its greater. Implements the Comparable#<=> protocol. 60 | def <=>(other) 61 | case other 62 | when Offset 63 | diff = self.line - other.line 64 | diff.zero? ? self.column - other.column : diff 65 | else 66 | raise ArgumentError, "can't convert #{other.class} into #{self.class}" 67 | end 68 | end 69 | 70 | # Public: Get a simple string representation of the offset 71 | # 72 | # Returns a String. 73 | def to_s 74 | if column == 0 75 | "#{line}" 76 | else 77 | "#{line}:#{column}" 78 | end 79 | end 80 | 81 | # Public: Get a pretty inspect output for debugging purposes. 82 | # 83 | # Returns a String. 84 | def inspect 85 | "#<#{self.class} line=#{line}, column=#{column}>" 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/source_map/vlq.rb: -------------------------------------------------------------------------------- 1 | module SourceMap 2 | # Public: Base64 VLQ encoding 3 | # 4 | # Adopted from ConradIrwin/ruby-source_map 5 | # https://github.com/ConradIrwin/ruby-source_map/blob/master/lib/source_map/vlq.rb 6 | # 7 | # Resources 8 | # 9 | # http://en.wikipedia.org/wiki/Variable-length_quantity 10 | # https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit 11 | # https://github.com/mozilla/source-map/blob/master/lib/source-map/base64-vlq.js 12 | # 13 | module VLQ 14 | VLQ_BASE_SHIFT = 5 15 | VLQ_BASE = 1 << VLQ_BASE_SHIFT 16 | VLQ_BASE_MASK = VLQ_BASE - 1 17 | VLQ_CONTINUATION_BIT = VLQ_BASE 18 | 19 | BASE64_DIGITS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('') 20 | BASE64_VALUES = (0...64).inject({}) { |h, i| h[BASE64_DIGITS[i]] = i; h } 21 | 22 | # Public: Encode a list of numbers into a compact VLQ string. 23 | # 24 | # ary - An Array of Integers 25 | # 26 | # Returns a VLQ String. 27 | def self.encode(ary) 28 | result = [] 29 | ary.each do |n| 30 | vlq = n < 0 ? ((-n) << 1) + 1 : n << 1 31 | loop do 32 | digit = vlq & VLQ_BASE_MASK 33 | vlq >>= VLQ_BASE_SHIFT 34 | digit |= VLQ_CONTINUATION_BIT if vlq > 0 35 | result << BASE64_DIGITS[digit] 36 | 37 | break unless vlq > 0 38 | end 39 | end 40 | result.join 41 | end 42 | 43 | # Public: Decode a VLQ string. 44 | # 45 | # str - VLQ encoded String 46 | # 47 | # Returns an Array of Integers. 48 | def self.decode(str) 49 | result = [] 50 | chars = str.split('') 51 | while chars.any? 52 | vlq = 0 53 | shift = 0 54 | continuation = true 55 | while continuation 56 | char = chars.shift 57 | raise ArgumentError unless char 58 | digit = BASE64_VALUES[char] 59 | continuation = false if (digit & VLQ_CONTINUATION_BIT) == 0 60 | digit &= VLQ_BASE_MASK 61 | vlq += digit << shift 62 | shift += VLQ_BASE_SHIFT 63 | end 64 | result << (vlq & 1 == 1 ? -(vlq >> 1) : vlq >> 1) 65 | end 66 | result 67 | end 68 | 69 | # Public: Encode a mapping array into a compact VLQ string. 70 | # 71 | # ary - Two dimensional Array of Integers. 72 | # 73 | # Returns a VLQ encoded String seperated by , and ;. 74 | def self.encode_mappings(ary) 75 | ary.map { |group| 76 | group.map { |segment| 77 | encode(segment) 78 | }.join(',') 79 | }.join(';') 80 | end 81 | 82 | # Public: Decode a VLQ string into mapping numbers. 83 | # 84 | # str - VLQ encoded String 85 | # 86 | # Returns an two dimensional Array of Integers. 87 | def self.decode_mappings(str) 88 | mappings = [] 89 | 90 | str.split(';').each_with_index do |group, index| 91 | mappings[index] = [] 92 | group.split(',').each do |segment| 93 | mappings[index] << decode(segment) 94 | end 95 | end 96 | 97 | mappings 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ruby Source Maps 2 | 3 | A Ruby library to read, create and manipulate Source Maps. 4 | 5 | [Source Maps](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/) allow easy debugging and development of CoffeeScript and minifed JavaScript. 6 | 7 | # Installation 8 | 9 | gem install sourcemap 10 | # or Bundler: 11 | gem 'sourcemap' 12 | 13 | # Usage 14 | 15 | ## Concatenation 16 | 17 | Join multiple source maps together. 18 | 19 | ``` ruby 20 | foo = File.read("examples/foo.js") 21 | bar = File.read("examples/bar.js") 22 | foobar = foo + bar 23 | 24 | foo_map = SourceMap::Map.from_json File.read("examples/foo.map") 25 | bar_map = SourceMap::Map.from_json File.read("examples/bar.map") 26 | foobar_map = foo_map + bar_map 27 | foobar_map.to_json 28 | ``` 29 | 30 | ## Piping 31 | 32 | Base one source map of another. 33 | 34 | ``` ruby 35 | cs_map = SourceMap::Map.from_json File.read("examples/index.map") 36 | min_map = SourceMap::Map.from_json File.read("examples/index.min.map") 37 | combined_map = cs_map | min_map 38 | combined_map.to_json 39 | ``` 40 | 41 | ## Map 42 | 43 | ### Map.from_json(json) 44 | 45 | Create a new `Map` instance from a JSON map string. 46 | 47 | SourceMap::Map.from_json(%{ 48 | { 49 | "version": 3, 50 | "file": "index.js", 51 | "sourceRoot": "", 52 | "sources": [ 53 | "index.coffee" 54 | ], 55 | "names": [], 56 | "mappings": ";AAAA;AAAA,MAAA,IAAA;;AAAA" 57 | } 58 | }) 59 | 60 | ### Map.from_hash(hash) 61 | 62 | Create a new `Map` instance from a hash. 63 | 64 | hash = { 65 | 'version' => 3, 66 | 'file' => "script.min.js", 67 | 'mappings' => "AAEAA,QAASA,MAAK,EAAG,CACfC,OAAAC,IAAA,CAAY,eAAZ,CADe", 68 | 'sources' => ["script.js"], 69 | 'names' => ["hello", "console", "log"] 70 | } 71 | map = SourceMap::Map.from_hash(hash) 72 | 73 | ### Map.new(mappings = [], filename = nil) 74 | 75 | Instantiate a `Map` instance, passing in an optional array of `Mapping`s and file name. 76 | 77 | @mappings = SourceMap::Map.new([ 78 | SourceMap::Mapping.new('a.js', SourceMap::Offset.new(0, 0), SourceMap::Offset.new(0, 0)), 79 | SourceMap::Mapping.new('b.js', SourceMap::Offset.new(1, 0), SourceMap::Offset.new(20, 0)), 80 | SourceMap::Mapping.new('c.js', SourceMap::Offset.new(2, 0), SourceMap::Offset.new(30, 0)) 81 | ]) 82 | 83 | ### Map#size 84 | 85 | Returns the amount of mappings 86 | 87 | ### Map#[] 88 | 89 | Lookup a mapping by integer 90 | 91 | map = SourceMap::Map.from_json(json) 92 | map[5] #=> 93 | 94 | ### Map#each 95 | 96 | Iterate over each mapping. 97 | 98 | ### Map#to_s 99 | 100 | Returns a VLQ representation of the source map. 101 | 102 | mapping = SourceMap::Map.from_hash(hash) 103 | mappings.to_s #=> "ACmBA;ACUA" 104 | 105 | ### Map#sources 106 | 107 | Returns an array of the original file names referenced in each mapping. 108 | 109 | ### Map#names 110 | 111 | Returns an array of 'names', which are referenced in the mappings (in case the original source file is not available). 112 | 113 | ### Map#+ 114 | 115 | Concatenates Maps together, so you can serve mappings from multiple sources as one combined map. 116 | 117 | foo_map = SourceMap::Map.from_json File.read("examples/foo.map") 118 | bar_map = SourceMap::Map.from_json File.read("examples/bar.map") 119 | foobar_map = foo_map + bar_map 120 | foobar_map.to_json 121 | 122 | ### Map#| 123 | 124 | Pipes map files together, so for example you could pipe a CoffeeScript map of `index.coffee` and an uglifier map of `index.js` together. In other words, one mapping will be based of the other. 125 | 126 | coffeescript_map = SourceMap::Map.from_json(cs_map_json) 127 | uglifier_map = SourceMap::Map.from_json(min_map_json) 128 | 129 | combined_map = coffeescript_map | uglifier_map 130 | 131 | ### Map#bsearch(offset) 132 | 133 | Find the closest generated mapping to any given offset using a binary tree search. 134 | 135 | foo_map = SourceMap::Map.from_json File.read("examples/foo.map") 136 | foo_map.bsearch(SourceMap::Offset.new(1,1)) #=> 137 | 138 | The method will return `nil` if an offset can't be found. 139 | 140 | ### Map#as_json 141 | 142 | Convert a `Map` instance back to JSON. 143 | 144 | map = Map.new([ 145 | Mapping.new('a.js', Offset.new(0, 0), Offset.new(0, 0)), 146 | Mapping.new('b.js', Offset.new(1, 0), Offset.new(20, 0)), 147 | Mapping.new('c.js', Offset.new(2, 0), Offset.new(30, 0)) 148 | ]) 149 | 150 | map.to_json #=> "{...}" 151 | 152 | ## Offset 153 | 154 | ### Offset.new(line, column) 155 | 156 | Instantiate an `Offset`, passing in a line and column integer. 157 | 158 | ### Offset#+ 159 | 160 | Add two offsets together. 161 | 162 | ### Offset#<=> 163 | 164 | Compare the position of two offsets, first the line than the column. 165 | 166 | ## Offset#to_s 167 | 168 | Get a pretty representation of an offset. 169 | -------------------------------------------------------------------------------- /test/test_vlq.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'source_map/vlq' 3 | 4 | class TestVLQ < MiniTest::Test 5 | include SourceMap 6 | 7 | TESTS = { 8 | 'A' => [0], 9 | 'C' => [1], 10 | 'D' => [-1], 11 | 'E' => [2], 12 | 'F' => [-2], 13 | 'K' => [5], 14 | 'L' => [-5], 15 | 'w+B' => [1000], 16 | 'x+B' => [-1000], 17 | 'gqjG' => [100000], 18 | 'hqjG' => [-100000], 19 | 'AAgBC' => [0, 0, 16, 1], 20 | 'AAgCgBC' => [0, 0, 32, 16, 1], 21 | 'DFLx+BhqjG' => [-1, -2, -5, -1000, -100000], 22 | 'CEKw+BgqjG' => [1, 2, 5, 1000, 100000] 23 | } 24 | 25 | MAP_TESTS = { 26 | 'AA,AA;;AACDE' => [[[0, 0], [0, 0]], [], [[0, 0, 1, -1, 2]]], 27 | ';;;;EAEE,EAAE,EAAC,CAAE;ECQY,UACC' => [[], [], [], [], [[2, 0, 2, 2], [2, 0, 0, 2], [2, 0, 0, 1], [1, 0, 0, 2]], [[2, 1, 8, 12], [10, 0, 1, 1]]], 28 | 'AAEAA,QAASA,MAAK,EAAG,CACfC,OAAAC,IAAA,CAAY,eAAZ,CADe' => [[[0, 0, 2, 0, 0], [8, 0, 0, 9, 0], [6, 0, 0, 5], [2, 0, 0, 3], [1, 0, 1, -15, 1], [7, 0, 0, 0, 1], [4, 0, 0, 0], [1, 0, 0, 12], [15, 0, 0, -12], [1, 0, -1, 15]]], 29 | ';;;;;EACAA;;EACAC;;EAGA;IAAA;;;EAGAC;IAAS;;;EAGTC;;EAGAC;IACE;IACA;IACA;MAAQ;;;;EAGVC;;;IACE;;;EAGF;IAAA;;;EAGAC;;;IAAQ;;MAAA' => [[], [], [], [], [], [[2, 0, 1, 0, 0]], [], [[2, 0, 1, 0, 1]], [], [[2, 0, 3, 0]], [[4, 0, 0, 0]], [], [], [[2, 0, 3, 0, 1]], [[4, 0, 0, 9]], [], [], [[2, 0, 3, -9, 1]], [], [[2, 0, 3, 0, 1]], [[4, 0, 1, 2]], [[4, 0, 1, 0]], [[4, 0, 1, 0]], [[6, 0, 0, 8]], [], [], [], [[2, 0, 3, -10, 1]], [], [], [[4, 0, 1, 2]], [], [], [[2, 0, 3, -2]], [[4, 0, 0, 0]], [], [], [[2, 0, 3, 0, 1]], [], [], [[4, 0, 0, 8]], [], [[6, 0, 0, 0]]], 30 | 'AACC,SAAQ,EAAG,CAAA,IACCA,CADD,CACOC,CADP,CACaC,CADb,CAC0CC,CAWpDA,EAAA,CAASA,QAAQ,CAACC,CAAD,CAAI,CACnB,MAAOA,EAAP,CAAWA,CADQ,CAIrBJ,EAAA,CAAO,CAAC,CAAD,CAAI,CAAJ,CAAO,CAAP,CAAU,CAAV,CAAa,CAAb,CAEPC,EAAA,CAAO,MACCI,IAAAC,KADD,QAEGH,CAFH,MAGCI,QAAQ,CAACH,CAAD,CAAI,CAChB,MAAOA,EAAP,CAAWD,CAAA,CAAOC,CAAP,CADK,CAHb,CAcc,YAArB,GAAI,MAAOI,MAAX,EAA8C,IAA9C,GAAoCA,KAApC,EACEC,KAAA,CAAM,YAAN,CAGO,UAAQ,EAAG,CAAA,IACdC,CADc,CACVC,CADU,CACJC,CACdA,EAAA,CAAW,EACNF,EAAA,CAAK,CAAV,KAAaC,CAAb,CAAoBX,CAAAa,OAApB,CAAiCH,CAAjC,CAAsCC,CAAtC,CAA4CD,CAAA,EAA5C,CACER,CACA,CADMF,CAAA,CAAKU,CAAL,CACN,CAAAE,CAAAE,KAAA,CAAcb,CAAAM,KAAA,CAAUL,CAAV,CAAd,CAEF,OAAOU,EAPW,CAAX,CAAA,EApCC,CAAX,CAAAG,KAAA,CA8CO,IA9CP' => [[[0, 0, 1, 1], [9, 0, 0, 8], [2, 0, 0, 3], [1, 0, 0, 0], [4, 0, 1, 1, 0], [1, 0, -1, -1], [1, 0, 1, 7, 1], [1, 0, -1, -7], [1, 0, 1, 13, 1], [1, 0, -1, -13], [1, 0, 1, 42, 1], [1, 0, 11, -52, 0], [2, 0, 0, 0], [1, 0, 0, 9, 0], [8, 0, 0, 8], [1, 0, 0, 1, 1], [1, 0, 0, -1], [1, 0, 0, 4], [1, 0, 1, -19], [6, 0, 0, 7, 0], [2, 0, 0, -7], [1, 0, 0, 11, 0], [1, 0, -1, 8], [1, 0, 4, -21, -4], [2, 0, 0, 0], [1, 0, 0, 7], [1, 0, 0, 1], [1, 0, 0, -1], [1, 0, 0, 4], [1, 0, 0, -4], [1, 0, 0, 7], [1, 0, 0, -7], [1, 0, 0, 10], [1, 0, 0, -10], [1, 0, 0, 13], [1, 0, 0, -13], [1, 0, 2, -7, 1], [2, 0, 0, 0], [1, 0, 0, 7], [6, 0, 1, 1, 4], [4, 0, 0, 0, 1], [5, 0, -1, -1], [8, 0, 2, 3, -3], [1, 0, -2, -3], [6, 0, 3, 1, 4], [8, 0, 0, 8], [1, 0, 0, 1, -3], [1, 0, 0, -1], [1, 0, 0, 4], [1, 0, 1, -16], [6, 0, 0, 7, 0], [2, 0, 0, -7], [1, 0, 0, 11, -1], [1, 0, 0, 0], [1, 0, 0, 7, 1], [1, 0, 0, -7], [1, 0, -1, 5], [1, 0, -3, -13], [1, 0, 14, 14], [12, 0, 0, -21], [3, 0, 0, 4], [6, 0, 0, 7, 4], [6, 0, 0, -11], [2, 0, 0, 46], [4, 0, 0, -46], [3, 0, 0, 36, 0], [5, 0, 0, -36], [2, 0, 1, 2, 1], [5, 0, 0, 0], [1, 0, 0, 6], [12, 0, 0, -6], [1, 0, 3, 7], [10, 0, 0, 8], [2, 0, 0, 3], [1, 0, 0, 0], [4, 0, 1, -14, 1], [1, 0, -1, 14], [1, 0, 1, -10, 1], [1, 0, -1, 10], [1, 0, 1, -4, 1], [1, 0, 1, -14, 0], [2, 0, 0, 0], [1, 0, 0, 11], [2, 0, 1, -6, -2], [2, 0, 0, 0], [1, 0, 0, 5], [1, 0, 0, -10], [5, 0, 0, 13, 1], [1, 0, 0, -13], [1, 0, 0, 20, -11], [1, 0, 0, 0, 13], [7, 0, 0, -20], [1, 0, 0, 33, -3], [1, 0, 0, -33], [1, 0, 0, 38, 1], [1, 0, 0, -38], [1, 0, 0, 44, -1], [1, 0, 0, 0], [2, 0, 0, -44], [1, 0, 1, 2, -8], [1, 0, 1, 0], [1, 0, -1, 6, -2], [1, 0, 0, 0], [1, 0, 0, 5, 10], [1, 0, 0, -5], [1, 0, 1, -6], [1, 0, 0, 0, 2], [1, 0, 0, 0, 2], [5, 0, 0, 0], [1, 0, 0, 14, -13], [1, 0, 0, 0, 6], [5, 0, 0, 0], [1, 0, 0, 10, -5], [1, 0, 0, -10], [1, 0, 0, -14], [1, 0, 2, -2], [7, 0, 0, 7, 10], [2, 0, -7, 11], [1, 0, 0, -11], [1, 0, 0, 0], [2, 0, -36, 1], [1, 0, 0, -11], [1, 0, 0, 0, 3], [5, 0, 0, 0], [1, 0, 46, 7], [4, 0, -46, -7]]] 31 | } 32 | 33 | def test_encode 34 | TESTS.each do |str, int| 35 | assert_equal str, VLQ.encode(int) 36 | end 37 | end 38 | 39 | def test_decode 40 | TESTS.each do |str, int| 41 | assert_equal int, VLQ.decode(str) 42 | end 43 | end 44 | 45 | def test_encode_decode 46 | (-255..255).each do |int| 47 | assert_equal [int], VLQ.decode(VLQ.encode([int])) 48 | end 49 | end 50 | 51 | def test_encode_mappings 52 | MAP_TESTS.each do |str, ary| 53 | assert_equal str, VLQ.encode_mappings(ary) 54 | end 55 | end 56 | 57 | def test_decode_mappings 58 | MAP_TESTS.each do |str, ary| 59 | assert_equal ary, VLQ.decode_mappings(str) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/source_map/map.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | require 'source_map/offset' 4 | require 'source_map/mapping' 5 | require 'source_map/vlq' 6 | 7 | module SourceMap 8 | class Map 9 | include Enumerable 10 | 11 | def self.from_json(json) 12 | from_hash JSON.parse(json) 13 | end 14 | 15 | def self.from_hash(hash) 16 | str = hash['mappings'] 17 | sources = hash['sources'] 18 | names = hash['names'] 19 | 20 | mappings = decode_vlq_mappings(str, sources, names) 21 | new(mappings, hash['file']) 22 | end 23 | 24 | # Internal: Decode VLQ mappings and match up sources and symbol names. 25 | # 26 | # str - VLQ string from 'mappings' attribute 27 | # sources - Array of Strings from 'sources' attribute 28 | # names - Array of Strings from 'names' attribute 29 | # 30 | # Returns an Array of Mappings. 31 | def self.decode_vlq_mappings(str, sources = [], names = []) 32 | mappings = [] 33 | 34 | source_id = 0 35 | original_line = 1 36 | original_column = 0 37 | name_id = 0 38 | 39 | VLQ.decode_mappings(str).each_with_index do |group, index| 40 | generated_column = 0 41 | generated_line = index + 1 42 | 43 | group.each do |segment| 44 | generated_column += segment[0] 45 | generated = Offset.new(generated_line, generated_column) 46 | 47 | if segment.size >= 4 48 | source_id += segment[1] 49 | original_line += segment[2] 50 | original_column += segment[3] 51 | 52 | source = sources[source_id] 53 | original = Offset.new(original_line, original_column) 54 | else 55 | # TODO: Research this case 56 | next 57 | end 58 | 59 | if segment[4] 60 | name_id += segment[4] 61 | name = names[name_id] 62 | end 63 | 64 | mappings << Mapping.new(source, generated, original, name) 65 | end 66 | end 67 | 68 | mappings 69 | end 70 | 71 | def initialize(mappings = [], filename = nil) 72 | @mappings, @filename = mappings, filename 73 | end 74 | 75 | attr_reader :filename 76 | 77 | def size 78 | @mappings.size 79 | end 80 | 81 | def [](i) 82 | @mappings[i] 83 | end 84 | 85 | def each(&block) 86 | @mappings.each(&block) 87 | end 88 | 89 | def to_s 90 | @string ||= build_vlq_string 91 | end 92 | 93 | def sources 94 | @sources ||= @mappings.map(&:source).uniq.compact 95 | end 96 | 97 | def names 98 | @names ||= @mappings.map(&:name).uniq.compact 99 | end 100 | 101 | def ==(other) 102 | eql?(other) 103 | end 104 | 105 | def eql?(other) 106 | other.is_a?(self.class) && 107 | self.mappings == other.mappings && 108 | self.filename == other.filename 109 | end 110 | 111 | def +(other) 112 | mappings = @mappings.dup 113 | offset = @mappings.any? ? @mappings.last.generated.line+1 : 0 114 | other.each do |m| 115 | mappings << Mapping.new( 116 | m.source, m.generated + offset, 117 | m.original, m.name 118 | ) 119 | end 120 | self.class.new(mappings, other.filename) 121 | end 122 | 123 | def |(other) 124 | return other.dup if self.mappings.empty? 125 | 126 | mappings = [] 127 | 128 | other.each do |m| 129 | om = bsearch(m.original) 130 | next unless om 131 | 132 | mappings << Mapping.new( 133 | om.source, m.generated, 134 | om.original, om.name 135 | ) 136 | end 137 | 138 | self.class.new(mappings, other.filename) 139 | end 140 | 141 | def bsearch(offset, from = 0, to = size - 1) 142 | mid = (from + to) / 2 143 | 144 | # We haven't found a match 145 | if from > to 146 | return from < 1 ? nil : self[from-1] 147 | end 148 | 149 | # We found an exact match 150 | if offset == self[mid].generated 151 | self[mid] 152 | 153 | # We need to filter more 154 | elsif offset < self[mid].generated 155 | bsearch(offset, from, mid - 1) 156 | elsif offset > self[mid].generated 157 | bsearch(offset, mid + 1, to) 158 | end 159 | end 160 | 161 | def as_json(*) 162 | { 163 | "version" => 3, 164 | "file" => filename, 165 | "mappings" => to_s, 166 | "sources" => sources, 167 | "names" => names 168 | } 169 | end 170 | 171 | def to_json(*a) 172 | as_json.to_json(*a) 173 | end 174 | 175 | # Public: Get a pretty inspect output for debugging purposes. 176 | # 177 | # Returns a String. 178 | def inspect 179 | str = "#<#{self.class}" 180 | str << " filename=#{filename.inspect}" if filename 181 | str << " mappings=#{mappings.map(&:to_s).inspect}" if mappings.any? 182 | str << ">" 183 | str 184 | end 185 | 186 | protected 187 | attr_reader :mappings 188 | 189 | def build_vlq_string 190 | source_id = 0 191 | source_line = 1 192 | source_column = 0 193 | name_id = 0 194 | 195 | by_lines = @mappings.group_by { |m| m.generated.line } 196 | 197 | sources_index = Hash[sources.each_with_index.to_a] 198 | names_index = Hash[names.each_with_index.to_a] 199 | 200 | ary = (1..(by_lines.keys.max || 1)).map do |line| 201 | generated_column = 0 202 | 203 | (by_lines[line] || []).map do |mapping| 204 | group = [] 205 | group << mapping.generated.column - generated_column 206 | group << sources_index[mapping.source] - source_id 207 | group << mapping.original.line - source_line 208 | group << mapping.original.column - source_column 209 | group << names_index[mapping.name] - name_id if mapping.name 210 | 211 | generated_column = mapping.generated.column 212 | source_id = sources_index[mapping.source] 213 | source_line = mapping.original.line 214 | source_column = mapping.original.column 215 | name_id = names_index[mapping.name] if mapping.name 216 | 217 | group 218 | end 219 | end 220 | 221 | VLQ.encode_mappings(ary) 222 | end 223 | end 224 | end 225 | -------------------------------------------------------------------------------- /test/test_map.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'source_map/map' 3 | 4 | class TestMap < MiniTest::Test 5 | include SourceMap 6 | 7 | def setup 8 | @mappings = Map.new([ 9 | Mapping.new('a.js', Offset.new(0, 0), Offset.new(0, 0)), 10 | Mapping.new('b.js', Offset.new(1, 0), Offset.new(20, 0)), 11 | Mapping.new('c.js', Offset.new(2, 0), Offset.new(30, 0)) 12 | ]) 13 | end 14 | 15 | def test_map 16 | hash = { 17 | 'version' => 3, 18 | 'file' => "script.min.js", 19 | 'mappings' => "AAEAA,QAASA,MAAK,EAAG,CACfC,OAAAC,IAAA,CAAY,eAAZ,CADe", 20 | 'sources' => ["script.js"], 21 | 'names' => ["hello", "console", "log"] 22 | } 23 | map = Map.from_hash(hash) 24 | 25 | assert mapping = map[0] 26 | assert_equal 1, mapping.generated.line 27 | assert_equal 0, mapping.generated.column 28 | assert_equal 3, mapping.original.line 29 | assert_equal 0, mapping.original.column 30 | assert_equal 'script.js', mapping.source 31 | assert_equal 'hello', mapping.name 32 | 33 | assert mapping = map[-1] 34 | assert_equal 1, mapping.generated.line 35 | assert_equal 45, mapping.generated.column 36 | assert_equal 3, mapping.original.line 37 | assert_equal 17, mapping.original.column 38 | assert_equal 'script.js', mapping.source 39 | assert_equal nil, mapping.name 40 | 41 | assert_equal hash['sources'], map.sources 42 | assert_equal hash['names'], map.names 43 | assert_equal hash['mappings'], map.to_s 44 | 45 | assert_equal hash, map.as_json 46 | assert_equal hash.to_json, map.to_json 47 | assert_equal hash.to_json, JSON.generate(map) 48 | end 49 | 50 | def test_map2 51 | hash = { 52 | 'version' => 3, 53 | 'file' => "example.js", 54 | 'mappings' => ";;;;;EACAA;;EACAC;;EAGA;IAAA;;;EAGAC;IAAS;;;EAGTC;;EAGAC;IACE;IACA;IACA;MAAQ;;;;EAGVC;;;IACE;;;EAGF;IAAA;;;EAGAC;;;IAAQ;;MAAA", 55 | 'sources' => ["example.coffee"], 56 | 'names' => ["number", "opposite", "square", "list", "math", "race", "cubes"] 57 | } 58 | map = Map.from_hash(hash) 59 | 60 | assert mapping = map[0] 61 | assert_equal 6, mapping.generated.line 62 | assert_equal 2, mapping.generated.column 63 | assert_equal 2, mapping.original.line 64 | assert_equal 0, mapping.original.column 65 | assert_equal 'example.coffee', mapping.source 66 | assert_equal 'number', mapping.name 67 | 68 | assert mapping = map[-1] 69 | assert_equal 43, mapping.generated.line 70 | assert_equal 6, mapping.generated.column 71 | assert_equal 28, mapping.original.line 72 | assert_equal 8, mapping.original.column 73 | assert_equal 'example.coffee', mapping.source 74 | assert_equal nil, mapping.name 75 | 76 | assert_equal hash['sources'], map.sources 77 | assert_equal hash['names'], map.names 78 | assert_equal hash['mappings'], map.to_s 79 | end 80 | 81 | def test_map3 82 | hash = { 83 | 'version' => 3, 84 | 'file' => "example.min.js", 85 | 'mappings' => "AACC,SAAQ,EAAG,CAAA,IACCA,CADD,CACOC,CADP,CACaC,CADb,CAC0CC,CAWpDA,EAAA,CAASA,QAAQ,CAACC,CAAD,CAAI,CACnB,MAAOA,EAAP,CAAWA,CADQ,CAIrBJ,EAAA,CAAO,CAAC,CAAD,CAAI,CAAJ,CAAO,CAAP,CAAU,CAAV,CAAa,CAAb,CAEPC,EAAA,CAAO,MACCI,IAAAC,KADD,QAEGH,CAFH,MAGCI,QAAQ,CAACH,CAAD,CAAI,CAChB,MAAOA,EAAP,CAAWD,CAAA,CAAOC,CAAP,CADK,CAHb,CAcc,YAArB,GAAI,MAAOI,MAAX,EAA8C,IAA9C,GAAoCA,KAApC,EACEC,KAAA,CAAM,YAAN,CAGO,UAAQ,EAAG,CAAA,IACdC,CADc,CACVC,CADU,CACJC,CACdA,EAAA,CAAW,EACNF,EAAA,CAAK,CAAV,KAAaC,CAAb,CAAoBX,CAAAa,OAApB,CAAiCH,CAAjC,CAAsCC,CAAtC,CAA4CD,CAAA,EAA5C,CACER,CACA,CADMF,CAAA,CAAKU,CAAL,CACN,CAAAE,CAAAE,KAAA,CAAcb,CAAAM,KAAA,CAAUL,CAAV,CAAd,CAEF,OAAOU,EAPW,CAAX,CAAA,EApCC,CAAX,CAAAG,KAAA,CA8CO,IA9CP", 86 | 'sources' => ["example.js"], 87 | 'names' => ["list","math","num","square","x","Math","sqrt","cube","elvis","alert","_i","_len","_results","length","push","call"] 88 | } 89 | map = Map.from_hash(hash) 90 | 91 | assert mapping = map[0] 92 | assert_equal 1, mapping.generated.line 93 | assert_equal 0, mapping.generated.column 94 | assert_equal 2, mapping.original.line 95 | assert_equal 1, mapping.original.column 96 | assert_equal 'example.js', mapping.source 97 | assert_equal nil, mapping.name 98 | 99 | assert mapping = map[-1] 100 | assert_equal 1, mapping.generated.line 101 | assert_equal 289, mapping.generated.column 102 | assert_equal 2, mapping.original.line 103 | assert_equal 1, mapping.original.column 104 | assert_equal 'example.js', mapping.source 105 | assert_equal nil, mapping.name 106 | 107 | assert_equal hash['sources'], map.sources 108 | assert_equal hash['names'], map.names 109 | assert_equal hash['mappings'], map.to_s 110 | end 111 | 112 | def test_to_s 113 | assert_equal "ACmBA;ACUA", @mappings.to_s 114 | 115 | empty_map = Map.new([]) 116 | assert_equal "", empty_map.to_s 117 | end 118 | 119 | def test_sources 120 | assert_equal ["a.js", "b.js", "c.js"], @mappings.sources 121 | end 122 | 123 | def test_names 124 | assert_equal [], @mappings.names 125 | end 126 | 127 | def test_eql 128 | map1 = @mappings 129 | map2 = @mappings.dup 130 | map3 = Map.new([ 131 | Mapping.new('a.js', Offset.new(0, 0), Offset.new(0, 0)), 132 | Mapping.new('b.js', Offset.new(1, 0), Offset.new(20, 0)), 133 | Mapping.new('c.js', Offset.new(2, 0), Offset.new(30, 0)) 134 | ]) 135 | map4 = Map.new 136 | map5 = Map.new([ 137 | Mapping.new('a.js', Offset.new(0, 0), Offset.new(0, 0)) 138 | ]) 139 | map6 = Map.new([ 140 | Mapping.new('a.js', Offset.new(0, 0), Offset.new(0, 0)), 141 | Mapping.new('b.js', Offset.new(1, 0), Offset.new(20, 0)), 142 | Mapping.new('z.js', Offset.new(2, 0), Offset.new(30, 0)) 143 | ]) 144 | map7 = Map.new([ 145 | Mapping.new('a.js', Offset.new(0, 0), Offset.new(0, 0)), 146 | Mapping.new('b.js', Offset.new(1, 0), Offset.new(20, 0)), 147 | Mapping.new('c.js', Offset.new(2, 0), Offset.new(30, 0)) 148 | ], 'bar.js') 149 | 150 | assert map1.eql?(map1) 151 | assert map1.eql?(map2) 152 | assert map1.eql?(map3) 153 | 154 | refute map1.eql?(true) 155 | refute map1.eql?(map4) 156 | refute map1.eql?(map5) 157 | refute map1.eql?(map6) 158 | refute map1.eql?(map7) 159 | end 160 | 161 | def test_add 162 | mappings2 = Map.new([ 163 | Mapping.new('d.js', Offset.new(0, 0), Offset.new(0, 0)) 164 | ]) 165 | mappings3 = @mappings + mappings2 166 | assert_equal 0, mappings3[0].generated.line 167 | assert_equal 1, mappings3[1].generated.line 168 | assert_equal 2, mappings3[2].generated.line 169 | assert_equal 3, mappings3[3].generated.line 170 | end 171 | 172 | def test_add_identity 173 | identity_map = Map.new 174 | 175 | assert_equal @mappings, identity_map + @mappings 176 | assert_equal @mappings, @mappings + identity_map 177 | end 178 | 179 | def test_pipe 180 | mappings1 = Map.from_json(%{ 181 | { 182 | "version": 3, 183 | "file": "index.js", 184 | "sourceRoot": "", 185 | "sources": [ 186 | "index.coffee" 187 | ], 188 | "names": [], 189 | "mappings": ";AAAA;AAAA,MAAA,IAAA;;AAAA,EAAA,IAAA,GAAO,SAAA,GAAA;WACL,KAAA,CAAM,aAAN,EADK;EAAA,CAAP,CAAA;;AAGA,EAAA,IAAW,IAAX;AAAA,IAAG,IAAH,CAAA,CAAA,CAAA;GAHA;AAAA" 190 | } 191 | }) 192 | 193 | mappings2 = Map.from_json(%{ 194 | { 195 | "version":3, 196 | "file":"index.min.js", 197 | "sources":["index.js"], 198 | "names":["test","alert","call","this"], 199 | "mappings":"CACA,WACE,GAAIA,KAEJA,MAAO,WACL,MAAOC,OAAM,eAGf,IAAI,KAAM,CACRD,SAGDE,KAAKC" 200 | } 201 | }) 202 | 203 | mappings3 = mappings1 | mappings2 204 | assert_equal 'CAAA,WAAA,GAAA,KAAA,MAAO,WAAA,MACL,OAAM,eAER,IAAW,KAAX,CAAG,SAHH,KAAA', mappings3.to_s 205 | end 206 | 207 | def test_pipe_identity 208 | identity_map = Map.new 209 | 210 | assert_equal @mappings, identity_map | @mappings 211 | end 212 | 213 | def test_bsearch 214 | assert_equal Offset.new(0, 0), @mappings.bsearch(Offset.new(0, 0)).original 215 | assert_equal Offset.new(0, 0), @mappings.bsearch(Offset.new(0, 5)).original 216 | assert_equal Offset.new(20, 0), @mappings.bsearch(Offset.new(1, 0)).original 217 | assert_equal Offset.new(20, 0), @mappings.bsearch(Offset.new(1, 0)).original 218 | assert_equal Offset.new(30, 0), @mappings.bsearch(Offset.new(2, 0)).original 219 | end 220 | 221 | def test_inspect 222 | assert_equal "#a.js@0:0\", \"1:0->b.js@20:0\", \"2:0->c.js@30:0\"]>", @mappings.inspect 223 | end 224 | end 225 | --------------------------------------------------------------------------------