Projekt

Obecné

Profil

Stáhnout (13.5 KB) Statistiky
| Větev: | Revize:
1
/* -*- Mode: js; js-indent-level: 2; -*- */
2
/*
3
 * Copyright 2011 Mozilla Foundation and contributors
4
 * Licensed under the New BSD license. See LICENSE or:
5
 * http://opensource.org/licenses/BSD-3-Clause
6
 */
7

    
8
var SourceMapGenerator = require('./source-map-generator').SourceMapGenerator;
9
var util = require('./util');
10

    
11
// Matches a Windows-style `\r\n` newline or a `\n` newline used by all other
12
// operating systems these days (capturing the result).
13
var REGEX_NEWLINE = /(\r?\n)/;
14

    
15
// Newline character code for charCodeAt() comparisons
16
var NEWLINE_CODE = 10;
17

    
18
// Private symbol for identifying `SourceNode`s when multiple versions of
19
// the source-map library are loaded. This MUST NOT CHANGE across
20
// versions!
21
var isSourceNode = "$$$isSourceNode$$$";
22

    
23
/**
24
 * SourceNodes provide a way to abstract over interpolating/concatenating
25
 * snippets of generated JavaScript source code while maintaining the line and
26
 * column information associated with the original source code.
27
 *
28
 * @param aLine The original line number.
29
 * @param aColumn The original column number.
30
 * @param aSource The original source's filename.
31
 * @param aChunks Optional. An array of strings which are snippets of
32
 *        generated JS, or other SourceNodes.
33
 * @param aName The original identifier.
34
 */
35
function SourceNode(aLine, aColumn, aSource, aChunks, aName) {
36
  this.children = [];
37
  this.sourceContents = {};
38
  this.line = aLine == null ? null : aLine;
39
  this.column = aColumn == null ? null : aColumn;
40
  this.source = aSource == null ? null : aSource;
41
  this.name = aName == null ? null : aName;
42
  this[isSourceNode] = true;
43
  if (aChunks != null) this.add(aChunks);
44
}
45

    
46
/**
47
 * Creates a SourceNode from generated code and a SourceMapConsumer.
48
 *
49
 * @param aGeneratedCode The generated code
50
 * @param aSourceMapConsumer The SourceMap for the generated code
51
 * @param aRelativePath Optional. The path that relative sources in the
52
 *        SourceMapConsumer should be relative to.
53
 */
54
SourceNode.fromStringWithSourceMap =
55
  function SourceNode_fromStringWithSourceMap(aGeneratedCode, aSourceMapConsumer, aRelativePath) {
56
    // The SourceNode we want to fill with the generated code
57
    // and the SourceMap
58
    var node = new SourceNode();
59

    
60
    // All even indices of this array are one line of the generated code,
61
    // while all odd indices are the newlines between two adjacent lines
62
    // (since `REGEX_NEWLINE` captures its match).
63
    // Processed fragments are accessed by calling `shiftNextLine`.
64
    var remainingLines = aGeneratedCode.split(REGEX_NEWLINE);
65
    var remainingLinesIndex = 0;
66
    var shiftNextLine = function() {
67
      var lineContents = getNextLine();
68
      // The last line of a file might not have a newline.
69
      var newLine = getNextLine() || "";
70
      return lineContents + newLine;
71

    
72
      function getNextLine() {
73
        return remainingLinesIndex < remainingLines.length ?
74
            remainingLines[remainingLinesIndex++] : undefined;
75
      }
76
    };
77

    
78
    // We need to remember the position of "remainingLines"
79
    var lastGeneratedLine = 1, lastGeneratedColumn = 0;
80

    
81
    // The generate SourceNodes we need a code range.
82
    // To extract it current and last mapping is used.
83
    // Here we store the last mapping.
84
    var lastMapping = null;
85

    
86
    aSourceMapConsumer.eachMapping(function (mapping) {
87
      if (lastMapping !== null) {
88
        // We add the code from "lastMapping" to "mapping":
89
        // First check if there is a new line in between.
90
        if (lastGeneratedLine < mapping.generatedLine) {
91
          // Associate first line with "lastMapping"
92
          addMappingWithCode(lastMapping, shiftNextLine());
93
          lastGeneratedLine++;
94
          lastGeneratedColumn = 0;
95
          // The remaining code is added without mapping
96
        } else {
97
          // There is no new line in between.
98
          // Associate the code between "lastGeneratedColumn" and
99
          // "mapping.generatedColumn" with "lastMapping"
100
          var nextLine = remainingLines[remainingLinesIndex];
101
          var code = nextLine.substr(0, mapping.generatedColumn -
102
                                        lastGeneratedColumn);
103
          remainingLines[remainingLinesIndex] = nextLine.substr(mapping.generatedColumn -
104
                                              lastGeneratedColumn);
105
          lastGeneratedColumn = mapping.generatedColumn;
106
          addMappingWithCode(lastMapping, code);
107
          // No more remaining code, continue
108
          lastMapping = mapping;
109
          return;
110
        }
111
      }
112
      // We add the generated code until the first mapping
113
      // to the SourceNode without any mapping.
114
      // Each line is added as separate string.
115
      while (lastGeneratedLine < mapping.generatedLine) {
116
        node.add(shiftNextLine());
117
        lastGeneratedLine++;
118
      }
119
      if (lastGeneratedColumn < mapping.generatedColumn) {
120
        var nextLine = remainingLines[remainingLinesIndex];
121
        node.add(nextLine.substr(0, mapping.generatedColumn));
122
        remainingLines[remainingLinesIndex] = nextLine.substr(mapping.generatedColumn);
123
        lastGeneratedColumn = mapping.generatedColumn;
124
      }
125
      lastMapping = mapping;
126
    }, this);
127
    // We have processed all mappings.
128
    if (remainingLinesIndex < remainingLines.length) {
129
      if (lastMapping) {
130
        // Associate the remaining code in the current line with "lastMapping"
131
        addMappingWithCode(lastMapping, shiftNextLine());
132
      }
133
      // and add the remaining lines without any mapping
134
      node.add(remainingLines.splice(remainingLinesIndex).join(""));
135
    }
136

    
137
    // Copy sourcesContent into SourceNode
138
    aSourceMapConsumer.sources.forEach(function (sourceFile) {
139
      var content = aSourceMapConsumer.sourceContentFor(sourceFile);
140
      if (content != null) {
141
        if (aRelativePath != null) {
142
          sourceFile = util.join(aRelativePath, sourceFile);
143
        }
144
        node.setSourceContent(sourceFile, content);
145
      }
146
    });
147

    
148
    return node;
149

    
150
    function addMappingWithCode(mapping, code) {
151
      if (mapping === null || mapping.source === undefined) {
152
        node.add(code);
153
      } else {
154
        var source = aRelativePath
155
          ? util.join(aRelativePath, mapping.source)
156
          : mapping.source;
157
        node.add(new SourceNode(mapping.originalLine,
158
                                mapping.originalColumn,
159
                                source,
160
                                code,
161
                                mapping.name));
162
      }
163
    }
164
  };
165

    
166
/**
167
 * Add a chunk of generated JS to this source node.
168
 *
169
 * @param aChunk A string snippet of generated JS code, another instance of
170
 *        SourceNode, or an array where each member is one of those things.
171
 */
172
SourceNode.prototype.add = function SourceNode_add(aChunk) {
173
  if (Array.isArray(aChunk)) {
174
    aChunk.forEach(function (chunk) {
175
      this.add(chunk);
176
    }, this);
177
  }
178
  else if (aChunk[isSourceNode] || typeof aChunk === "string") {
179
    if (aChunk) {
180
      this.children.push(aChunk);
181
    }
182
  }
183
  else {
184
    throw new TypeError(
185
      "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
186
    );
187
  }
188
  return this;
189
};
190

    
191
/**
192
 * Add a chunk of generated JS to the beginning of this source node.
193
 *
194
 * @param aChunk A string snippet of generated JS code, another instance of
195
 *        SourceNode, or an array where each member is one of those things.
196
 */
197
SourceNode.prototype.prepend = function SourceNode_prepend(aChunk) {
198
  if (Array.isArray(aChunk)) {
199
    for (var i = aChunk.length-1; i >= 0; i--) {
200
      this.prepend(aChunk[i]);
201
    }
202
  }
203
  else if (aChunk[isSourceNode] || typeof aChunk === "string") {
204
    this.children.unshift(aChunk);
205
  }
206
  else {
207
    throw new TypeError(
208
      "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
209
    );
210
  }
211
  return this;
212
};
213

    
214
/**
215
 * Walk over the tree of JS snippets in this node and its children. The
216
 * walking function is called once for each snippet of JS and is passed that
217
 * snippet and the its original associated source's line/column location.
218
 *
219
 * @param aFn The traversal function.
220
 */
221
SourceNode.prototype.walk = function SourceNode_walk(aFn) {
222
  var chunk;
223
  for (var i = 0, len = this.children.length; i < len; i++) {
224
    chunk = this.children[i];
225
    if (chunk[isSourceNode]) {
226
      chunk.walk(aFn);
227
    }
228
    else {
229
      if (chunk !== '') {
230
        aFn(chunk, { source: this.source,
231
                     line: this.line,
232
                     column: this.column,
233
                     name: this.name });
234
      }
235
    }
236
  }
237
};
238

    
239
/**
240
 * Like `String.prototype.join` except for SourceNodes. Inserts `aStr` between
241
 * each of `this.children`.
242
 *
243
 * @param aSep The separator.
244
 */
245
SourceNode.prototype.join = function SourceNode_join(aSep) {
246
  var newChildren;
247
  var i;
248
  var len = this.children.length;
249
  if (len > 0) {
250
    newChildren = [];
251
    for (i = 0; i < len-1; i++) {
252
      newChildren.push(this.children[i]);
253
      newChildren.push(aSep);
254
    }
255
    newChildren.push(this.children[i]);
256
    this.children = newChildren;
257
  }
258
  return this;
259
};
260

    
261
/**
262
 * Call String.prototype.replace on the very right-most source snippet. Useful
263
 * for trimming whitespace from the end of a source node, etc.
264
 *
265
 * @param aPattern The pattern to replace.
266
 * @param aReplacement The thing to replace the pattern with.
267
 */
268
SourceNode.prototype.replaceRight = function SourceNode_replaceRight(aPattern, aReplacement) {
269
  var lastChild = this.children[this.children.length - 1];
270
  if (lastChild[isSourceNode]) {
271
    lastChild.replaceRight(aPattern, aReplacement);
272
  }
273
  else if (typeof lastChild === 'string') {
274
    this.children[this.children.length - 1] = lastChild.replace(aPattern, aReplacement);
275
  }
276
  else {
277
    this.children.push(''.replace(aPattern, aReplacement));
278
  }
279
  return this;
280
};
281

    
282
/**
283
 * Set the source content for a source file. This will be added to the SourceMapGenerator
284
 * in the sourcesContent field.
285
 *
286
 * @param aSourceFile The filename of the source file
287
 * @param aSourceContent The content of the source file
288
 */
289
SourceNode.prototype.setSourceContent =
290
  function SourceNode_setSourceContent(aSourceFile, aSourceContent) {
291
    this.sourceContents[util.toSetString(aSourceFile)] = aSourceContent;
292
  };
293

    
294
/**
295
 * Walk over the tree of SourceNodes. The walking function is called for each
296
 * source file content and is passed the filename and source content.
297
 *
298
 * @param aFn The traversal function.
299
 */
300
SourceNode.prototype.walkSourceContents =
301
  function SourceNode_walkSourceContents(aFn) {
302
    for (var i = 0, len = this.children.length; i < len; i++) {
303
      if (this.children[i][isSourceNode]) {
304
        this.children[i].walkSourceContents(aFn);
305
      }
306
    }
307

    
308
    var sources = Object.keys(this.sourceContents);
309
    for (var i = 0, len = sources.length; i < len; i++) {
310
      aFn(util.fromSetString(sources[i]), this.sourceContents[sources[i]]);
311
    }
312
  };
313

    
314
/**
315
 * Return the string representation of this source node. Walks over the tree
316
 * and concatenates all the various snippets together to one string.
317
 */
318
SourceNode.prototype.toString = function SourceNode_toString() {
319
  var str = "";
320
  this.walk(function (chunk) {
321
    str += chunk;
322
  });
323
  return str;
324
};
325

    
326
/**
327
 * Returns the string representation of this source node along with a source
328
 * map.
329
 */
330
SourceNode.prototype.toStringWithSourceMap = function SourceNode_toStringWithSourceMap(aArgs) {
331
  var generated = {
332
    code: "",
333
    line: 1,
334
    column: 0
335
  };
336
  var map = new SourceMapGenerator(aArgs);
337
  var sourceMappingActive = false;
338
  var lastOriginalSource = null;
339
  var lastOriginalLine = null;
340
  var lastOriginalColumn = null;
341
  var lastOriginalName = null;
342
  this.walk(function (chunk, original) {
343
    generated.code += chunk;
344
    if (original.source !== null
345
        && original.line !== null
346
        && original.column !== null) {
347
      if(lastOriginalSource !== original.source
348
         || lastOriginalLine !== original.line
349
         || lastOriginalColumn !== original.column
350
         || lastOriginalName !== original.name) {
351
        map.addMapping({
352
          source: original.source,
353
          original: {
354
            line: original.line,
355
            column: original.column
356
          },
357
          generated: {
358
            line: generated.line,
359
            column: generated.column
360
          },
361
          name: original.name
362
        });
363
      }
364
      lastOriginalSource = original.source;
365
      lastOriginalLine = original.line;
366
      lastOriginalColumn = original.column;
367
      lastOriginalName = original.name;
368
      sourceMappingActive = true;
369
    } else if (sourceMappingActive) {
370
      map.addMapping({
371
        generated: {
372
          line: generated.line,
373
          column: generated.column
374
        }
375
      });
376
      lastOriginalSource = null;
377
      sourceMappingActive = false;
378
    }
379
    for (var idx = 0, length = chunk.length; idx < length; idx++) {
380
      if (chunk.charCodeAt(idx) === NEWLINE_CODE) {
381
        generated.line++;
382
        generated.column = 0;
383
        // Mappings end at eol
384
        if (idx + 1 === length) {
385
          lastOriginalSource = null;
386
          sourceMappingActive = false;
387
        } else if (sourceMappingActive) {
388
          map.addMapping({
389
            source: original.source,
390
            original: {
391
              line: original.line,
392
              column: original.column
393
            },
394
            generated: {
395
              line: generated.line,
396
              column: generated.column
397
            },
398
            name: original.name
399
          });
400
        }
401
      } else {
402
        generated.column++;
403
      }
404
    }
405
  });
406
  this.walkSourceContents(function (sourceFile, sourceContent) {
407
    map.setSourceContent(sourceFile, sourceContent);
408
  });
409

    
410
  return { code: generated.code, map: map };
411
};
412

    
413
exports.SourceNode = SourceNode;
(9-9/10)