File: generator.rb

package info (click to toggle)
ruby-source-map 3.0.1%2Bgit.20120229.bda06a3f-1~bpo70%2B1
  • links: PTS, VCS
  • area: main
  • in suites: wheezy-backports
  • size: 160 kB
  • sloc: ruby: 715; makefile: 2
file content (247 lines) | stat: -rw-r--r-- 9,655 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
class SourceMap
  module Generator

    # An object (responding to <<) that will be written to whenever
    # {add_generated} is called.
    #
    # @example
    #
    #   File.open("/var/www/a.js.min"){ |f|
    #     map = SourceMap.new(:generated_output => f)
    #     map.add_generated('function(a,b,c){minified=1}\n', :source => 'a.js')
    #     map.save('/var/www/a.js.map')
    #   }
    #   File.read('/var/www/a.js.min') == 'function(a,b,c){minified=1}\n'
    #
    attr_accessor :generated_output

    # Add the mapping for generated code to this source map.
    #
    # The first parameter is the generated text that you're going to add to the output, if
    # it contains multiple lines of code then it will be added to the source map as
    # several mappings.
    #
    # If present, the second parameter represents the original source of the generated
    # fragment, and may contain:
    #
    # :source => String,           # The filename of the source fille that contains this fragment.
    # :source_line => Integer,     # The line in that file that contains this fragment
    # :source_col => Integer,      # The column in that line at which this fragment starts
    # :name => String              # The original name for this variable.
    # :exact_position => Bool      # Whether all lines in the generated fragment came from
    #                                the same position in the source.
    #
    # The :source key is required to set :source_line, :source_col or :name.
    #
    # If unset :source_line and :source_col default to 1,0 for the first line of the
    # generated fragment.
    #
    # Normally :source_line is incremented and :source_col reset at every line break in
    # the generated code (because we assume that you're copying a verbatim fragment from
    # the source into the generated code). If that is not the case, you can set
    # :exact_position => true, and then all lines in the generated output will be given
    # the same :source_line and :source_col.
    #
    # The :name property is used if the fragment you are adding contains only a name that
    # you have renamed in the source transformation.
    #
    # If you'd like to ensure that the source map stays in sync with the generated
    # source, consider calling {source_map.generated_output = StringIO.new} and then
    # accessing your generated javascript with {source_map.generated_output.string},
    # otherwise be careful to always write to both.
    #
    # NOTE: By long-standing convention, the first line of a file is numbered 1, not 0.
    #
    # NOTE: when generating a source map, you should either use this method always, or use
    # the {#add_mapping} method always.
    #
    def add_generated(text, opts={})
      if !opts[:source] && (opts[:name] || opts[:source_line] || opts[:source_col])
        raise "mapping must have :source to have :source_line, :source_col or :name"
      elsif opts[:source_line] && opts[:source_line] < 1
        raise "files start on line 1 (got :source_line => #{opts[:source_line]})"
      elsif !(remain = opts.keys - [:source, :source_line, :source_col, :name, :exact_position]).empty?
        raise "mapping had unexpected keys: #{remain.inspect}"
      end

      source_line = opts[:source_line] || 1
      source_col = opts[:source_col] || 0
      self.generated_line ||= 1
      self.generated_col ||= 0

      text.split(/(\n)/).each do |line|
        if line == "\n"
          self.generated_line += 1
          self.generated_col = 0
          unless opts[:exact_position]
            source_line += 1
            source_col = 0
          end
        elsif line != ""
          mapping = {
            :generated_line => generated_line,
            :generated_col => generated_col,
          }
          if opts[:source]
            mapping[:source] = opts[:source]
            mapping[:source_line] = source_line
            mapping[:source_col] = source_col
            mapping[:name] = opts[:name] if opts[:name]
          end

          mappings << mapping

          self.generated_col += line.size
          source_col += line.size unless opts[:exact_position]
        end
      end

      generated_output << text if generated_output
    end

    # Add a mapping to the list for this object.
    #
    # A mapping identifies a fragment of code that has been moved around during
    # transformation from the source file to the generated file. The fragment should
    # be contiguous and not contain any line breaks.
    #
    # Mappings are Hashes with a valid subset of the following 6 keys:
    #
    # :generated_line => Integer,  # The line in the generated file that contains this fragment.
    # :generated_col  => Integer,  # The column in the generated_line that this mapping starts on
    # :source => String,           # The filename of the source fille that contains this fragment.
    # :source_line => Integer,     # The line in that file that contains this fragment.
    # :source_col => Integer,      # The column in that line at which this fragment starts.
    # :name => String              # The original name for this variable (if applicable).
    #
    #
    # The only 3 valid subsets of keys are:
    #   [:generated_line, :generated_col] To indicate that this is a fragment in the
    #   output file that you don't have the source for.
    #
    #   [:generated_line, :generated_col, :source, :source_line, :source_col] To indicate
    #   that this is a fragment in the output file that you do have the source for.
    #
    #   [:generated_line, :generated_col, :source, :source_line, :source_col, :name] To
    #   indicate that this is a particular identifier at a particular location in the original.
    #
    # Any other combination of keys would produce an invalid source map.
    #
    # NOTE: By long-standing convention, the first line of a file is numbered 1, not 0.
    #
    # NOTE: when generating a source map, you should either use this method always,
    # or use the {#add_generated} method always.
    #
    def add_mapping(map)
      if !map[:generated_line] || !map[:generated_col]
        raise "mapping must have :generated_line and :generated_col"
      elsif map[:source] && !(map[:source_line] && map[:source_col])
        raise "mapping must have :source_line and :source_col if it has :source"
      elsif !map[:source] && (map[:source_line] || map[:source_col])
        raise "mapping may not have a :source_line or :source_col without a :source"
      elsif map[:name] && !map[:source]
        raise "mapping may not have a :name without a :source"
      elsif map[:source_line] && map[:source_line] < 1
        raise "files start on line 1 (got :source_line => #{map[:source_line]})"
      elsif map[:generated_line] < 1
        raise "files start on line 1 (got :generated_line => #{map[:generated_line]})"
      elsif !(remain = map.keys - [:generated_line, :generated_col, :source, :source_line, :source_col, :name]).empty?
        raise "mapping had unexpected keys: #{remain.inspect}"
      end

      mappings << map
    end

    # Convert the map into an object suitable for direct serialisation.
    def as_json
      serialized_mappings = serialize_mappings!

      {
        'version' => version,
        'file' => file,
        'sourceRoot' => source_root,
        'sources' => sources,
        'names' => names,
        'mappings' => serialized_mappings
      }
    end

    # Convert the map to a string.
    def to_s
      as_json.to_json
    end

    # Write this map to a file.
    def save(file)
      File.open(file, "w"){ |f| f << to_s }
    end

    protected

    attr_reader :source_ids, :name_ids
    attr_accessor :generated_line, :generated_col

    # Get the id for the given file. If we've not
    # seen this file before, add it to the list.
    def source_id(file)
      source_ids[file] ||= (
        sources << file
        sources.size - 1
      )
    end

    # Get the id for the given name. If we've not
    # seen this name before, add it to the list.
    def name_id(name)
      name_ids[name] ||= (
        names << name
        names.size - 1
      )
    end

    # Encode a vlq. As each field in the output should be relative to the
    # previous occurance of that field, we keep track of each one.
    def vlq(num, type)
      ret = num - @previous_vlq[type]
      @previous_vlq[type] = num
      VLQ.encode(ret)
    end

    # Serialize the list of mappings into the string of base64 variable length
    # quanities. As a side-effect, regenerate the sources and names arrays.
    def serialize_mappings!
      # clear all internals as we're about to re-generate them.
      @sources  = []
      @source_ids = {}
      @names    = []
      @name_ids = {}
      @previous_vlq = Hash.new{ 0 }

      return "" if mappings.empty?

      by_lines = mappings.group_by{ |x| x[:generated_line] }

      (1..by_lines.keys.max).map do |line|
        # reset the generated_col on each line as indicated by the VLQ spec.
        # (the other values continue to be relative)
        @previous_vlq[:generated_col] = 0

        fragments = (by_lines[line] || []).sort_by{ |x| x[:generated_col] }
        fragments.map do |map|
          serialize_mapping(map)
        end.join(",")
      end.join(";")
    end

    def serialize_mapping(map)
      item = vlq(map[:generated_col], :generated_col)
      if map[:source]
        item << vlq(source_id(map[:source]), :source)
        item << vlq(map[:source_line] - 1, :source_line)
        item << vlq(map[:source_col], :source_col)
        item << vlq(name_id(map[:name]), :name) if map[:name]
      end
      item
    end
  end
end