module Sprockets::SourceMapUtils
def bsearch_mappings(mappings, offset, from = 0, to = mappings.size - 1)
offset - Array [line, column]
mappings - Array of mapping Hash objects
Public: Search Array of mappings for closest offset.
def bsearch_mappings(mappings, offset, from = 0, to = mappings.size - 1) mid = (from + to) / 2 if from > to return from < 1 ? nil : mappings[from-1] end case compare_source_offsets(offset, mappings[mid][:generated]) when 0 mappings[mid] when -1 bsearch_mappings(mappings, offset, from, mid - 1) when 1 bsearch_mappings(mappings, offset, mid + 1, to) end end
def combine_source_maps(original_map, second_map)
# => [{:source=>"index.coffee", :generated => [1, 1], :original => [1, 0]}]
combine_source_maps(original_map, second_map)
second_map = [{ :source => "index.js", :generated => [1, 1], :original => [2, 0] }]
original_map = [{ :source => "index.coffee", :generated => [2, 0], :original => [1, 0] }]
minified output can be traced back to the original CoffeeScript file.
map can be combined with the Uglifier map so the source lines of the
Uglifier processes the result and produces another map. The CoffeeScript
For an example, CoffeeScript may transform a file producing a map. Then
the source.
source maps. These steps can be combined into a single mapping back to
Source transformations may happen in discrete steps producing separate
mapping.
Public: Combine two seperate source map transformations into a single
def combine_source_maps(original_map, second_map) original_map ||= [] return second_map.dup if original_map.empty? new_map = [] second_map.each do |m| original_line = bsearch_mappings(original_map, m[:original]) next unless original_line new_map << original_line.merge(generated: m[:generated]) end new_map end
def compare_source_offsets(a, b)
b - Array [line, column]
a - Array [line, column]
Compatible with Array#sort.
Public: Compare two source map offsets.
def compare_source_offsets(a, b) diff = a[0] - b[0] diff = a[1] - b[1] if diff == 0 if diff < 0 -1 elsif diff > 0 1 else 0 end end
def concat_source_maps(a, b)
b - Array of source mapping Hashes
a - Array of source mapping Hashes
map3 = concat_source_maps(map1, map2)
script3 = "#{script1}#{script2}"
Examples
maps for those files can be concatenated to map back to the originals.
For an example, if two js scripts are concatenated, the individual source
Public: Concatenate two source maps.
def concat_source_maps(a, b) a ||= [] b ||= [] mappings = a.dup if a.any? offset = a.last[:generated][0] else offset = 0 end b.each do |m| mappings << m.merge(generated: [m[:generated][0] + offset, m[:generated][1]]) end mappings end
def decode_json_source_map(json)
json - String source map JSON
Public: Decode Source Map JSON into Ruby objects.
def decode_json_source_map(json) map = JSON.parse(json) map['mappings'] = decode_vlq_mappings(map['mappings'], sources: map['sources'], names: map['names']) map end
def decode_vlq_mappings(str, sources: [], names: [])
names - Array of Strings from 'names' attribute
sources - Array of Strings from 'sources' attribute
str - VLQ string from 'mappings' attribute
Public: Decode VLQ mappings and match up sources and symbol names.
def decode_vlq_mappings(str, sources: [], names: []) mappings = [] source_id = 0 original_line = 1 original_column = 0 name_id = 0 vlq_decode_mappings(str).each_with_index do |group, index| generated_column = 0 generated_line = index + 1 group.each do |segment| generated_column += segment[0] generated = [generated_line, generated_column] if segment.size >= 4 source_id += segment[1] original_line += segment[2] original_column += segment[3] source = sources[source_id] original = [original_line, original_column] else # TODO: Research this case next end if segment[4] name_id += segment[4] name = names[name_id] end mapping = {source: source, generated: generated, original: original} mapping[:name] = name if name mappings << mapping end end mappings end
def encode_json_source_map(mappings, sources: nil, names: nil, filename: nil)
filename - String filename
names - Array of String names
sources - Array of String sources
mappings - Array of Hash or String VLQ encoded mappings
Public: Encode mappings to Source Map JSON.
def encode_json_source_map(mappings, sources: nil, names: nil, filename: nil) case mappings when String when Array sources ||= mappings.map { |m| m[:source] }.uniq.compact names ||= mappings.map { |m| m[:name] }.uniq.compact mappings = encode_vlq_mappings(mappings, sources: sources, names: names) else raise TypeError, "could not encode mappings: #{mappings}" end JSON.generate({ "version" => 3, "file" => filename, "mappings" => mappings, "sources" => sources, "names" => names }) end
def encode_vlq_mappings(mappings, sources: nil, names: nil)
names - Array of String names (default: mappings name order)
sources - Array of String sources (default: mappings source order)
mappings - Array of Hash mapping objects
Public: Encode mappings Hash into a VLQ encoded String.
def encode_vlq_mappings(mappings, sources: nil, names: nil) sources ||= mappings.map { |m| m[:source] }.uniq.compact names ||= mappings.map { |m| m[:name] }.uniq.compact sources_index = Hash[sources.each_with_index.to_a] names_index = Hash[names.each_with_index.to_a] source_id = 0 source_line = 1 source_column = 0 name_id = 0 by_lines = mappings.group_by { |m| m[:generated][0] } ary = (1..(by_lines.keys.max || 1)).map do |line| generated_column = 0 (by_lines[line] || []).map do |mapping| group = [] group << mapping[:generated][1] - generated_column group << sources_index[mapping[:source]] - source_id group << mapping[:original][0] - source_line group << mapping[:original][1] - source_column group << names_index[mapping[:name]] - name_id if mapping[:name] generated_column = mapping[:generated][1] source_id = sources_index[mapping[:source]] source_line = mapping[:original][0] source_column = mapping[:original][1] name_id = names_index[mapping[:name]] if mapping[:name] group end end vlq_encode_mappings(ary) end
def vlq_decode(str)
str - VLQ encoded String
Public: Decode a VLQ string.
def vlq_decode(str) result = [] chars = str.split('') while chars.any? vlq = 0 shift = 0 continuation = true while continuation char = chars.shift raise ArgumentError unless char digit = BASE64_VALUES[char] continuation = false if (digit & VLQ_CONTINUATION_BIT) == 0 digit &= VLQ_BASE_MASK vlq += digit << shift shift += VLQ_BASE_SHIFT end result << (vlq & 1 == 1 ? -(vlq >> 1) : vlq >> 1) end result end
def vlq_decode_mappings(str)
str - VLQ encoded String
Public: Decode a VLQ string into mapping numbers.
def vlq_decode_mappings(str) mappings = [] str.split(';').each_with_index do |group, index| mappings[index] = [] group.split(',').each do |segment| mappings[index] << vlq_decode(segment) end end mappings end
def vlq_encode(ary)
ary - An Array of Integers
Public: Encode a list of numbers into a compact VLQ string.
def vlq_encode(ary) result = [] ary.each do |n| vlq = n < 0 ? ((-n) << 1) + 1 : n << 1 loop do digit = vlq & VLQ_BASE_MASK vlq >>= VLQ_BASE_SHIFT digit |= VLQ_CONTINUATION_BIT if vlq > 0 result << BASE64_DIGITS[digit] break unless vlq > 0 end end result.join end
def vlq_encode_mappings(ary)
ary - Two dimensional Array of Integers.
Public: Encode a mapping array into a compact VLQ string.
def vlq_encode_mappings(ary) ary.map { |group| group.map { |segment| vlq_encode(segment) }.join(',') }.join(';') end