1var fs = require('fs');
2var path = require('path');
3
4var isAllowedResource = require('./is-allowed-resource');
5var matchDataUri = require('./match-data-uri');
6var rebaseLocalMap = require('./rebase-local-map');
7var rebaseRemoteMap = require('./rebase-remote-map');
8
9var Token = require('../tokenizer/token');
10var hasProtocol = require('../utils/has-protocol');
11var isDataUriResource = require('../utils/is-data-uri-resource');
12var isRemoteResource = require('../utils/is-remote-resource');
13
14var MAP_MARKER_PATTERN = /^\/\*# sourceMappingURL=(\S+) \*\/$/;
15
16function applySourceMaps(tokens, context, callback) {
17  var applyContext = {
18    callback: callback,
19    fetch: context.options.fetch,
20    index: 0,
21    inline: context.options.inline,
22    inlineRequest: context.options.inlineRequest,
23    inlineTimeout: context.options.inlineTimeout,
24    inputSourceMapTracker: context.inputSourceMapTracker,
25    localOnly: context.localOnly,
26    processedTokens: [],
27    rebaseTo: context.options.rebaseTo,
28    sourceTokens: tokens,
29    warnings: context.warnings
30  };
31
32  return context.options.sourceMap && tokens.length > 0 ?
33    doApplySourceMaps(applyContext) :
34    callback(tokens);
35}
36
37function doApplySourceMaps(applyContext) {
38  var singleSourceTokens = [];
39  var lastSource = findTokenSource(applyContext.sourceTokens[0]);
40  var source;
41  var token;
42  var l;
43
44  for (l = applyContext.sourceTokens.length; applyContext.index < l; applyContext.index++) {
45    token = applyContext.sourceTokens[applyContext.index];
46    source = findTokenSource(token);
47
48    if (source != lastSource) {
49      singleSourceTokens = [];
50      lastSource = source;
51    }
52
53    singleSourceTokens.push(token);
54    applyContext.processedTokens.push(token);
55
56    if (token[0] == Token.COMMENT && MAP_MARKER_PATTERN.test(token[1])) {
57      return fetchAndApplySourceMap(token[1], source, singleSourceTokens, applyContext);
58    }
59  }
60
61  return applyContext.callback(applyContext.processedTokens);
62}
63
64function findTokenSource(token) {
65  var scope;
66  var metadata;
67
68  if (token[0] == Token.AT_RULE || token[0] == Token.COMMENT) {
69    metadata = token[2][0];
70  } else {
71    scope = token[1][0];
72    metadata = scope[2][0];
73  }
74
75  return metadata[2];
76}
77
78function fetchAndApplySourceMap(sourceMapComment, source, singleSourceTokens, applyContext) {
79  return extractInputSourceMapFrom(sourceMapComment, applyContext, function (inputSourceMap) {
80    if (inputSourceMap) {
81      applyContext.inputSourceMapTracker.track(source, inputSourceMap);
82      applySourceMapRecursively(singleSourceTokens, applyContext.inputSourceMapTracker);
83    }
84
85    applyContext.index++;
86    return doApplySourceMaps(applyContext);
87  });
88}
89
90function extractInputSourceMapFrom(sourceMapComment, applyContext, whenSourceMapReady) {
91  var uri = MAP_MARKER_PATTERN.exec(sourceMapComment)[1];
92  var absoluteUri;
93  var sourceMap;
94  var rebasedMap;
95
96  if (isDataUriResource(uri)) {
97    sourceMap = extractInputSourceMapFromDataUri(uri);
98    return whenSourceMapReady(sourceMap);
99  } else if (isRemoteResource(uri)) {
100    return loadInputSourceMapFromRemoteUri(uri, applyContext, function (sourceMap) {
101      var parsedMap;
102
103      if (sourceMap) {
104        parsedMap = JSON.parse(sourceMap);
105        rebasedMap = rebaseRemoteMap(parsedMap, uri);
106        whenSourceMapReady(rebasedMap);
107      } else {
108        whenSourceMapReady(null);
109      }
110    });
111  } else {
112    // at this point `uri` is already rebased, see lib/reader/rebase.js#rebaseSourceMapComment
113    // it is rebased to be consistent with rebasing other URIs
114    // however here we need to resolve it back to read it from disk
115    absoluteUri = path.resolve(applyContext.rebaseTo, uri);
116    sourceMap = loadInputSourceMapFromLocalUri(absoluteUri, applyContext);
117
118    if (sourceMap) {
119      rebasedMap = rebaseLocalMap(sourceMap, absoluteUri, applyContext.rebaseTo);
120      return whenSourceMapReady(rebasedMap);
121    } else {
122      return whenSourceMapReady(null);
123    }
124  }
125}
126
127function extractInputSourceMapFromDataUri(uri) {
128  var dataUriMatch = matchDataUri(uri);
129  var charset = dataUriMatch[2] ? dataUriMatch[2].split(/[=;]/)[2] : 'us-ascii';
130  var encoding = dataUriMatch[3] ? dataUriMatch[3].split(';')[1] : 'utf8';
131  var data = encoding == 'utf8' ? global.unescape(dataUriMatch[4]) : dataUriMatch[4];
132
133  var buffer = new Buffer(data, encoding);
134  buffer.charset = charset;
135
136  return JSON.parse(buffer.toString());
137}
138
139function loadInputSourceMapFromRemoteUri(uri, applyContext, whenLoaded) {
140  var isAllowed = isAllowedResource(uri, true, applyContext.inline);
141  var isRuntimeResource = !hasProtocol(uri);
142
143  if (applyContext.localOnly) {
144    applyContext.warnings.push('Cannot fetch remote resource from "' + uri + '" as no callback given.');
145    return whenLoaded(null);
146  } else if (isRuntimeResource) {
147    applyContext.warnings.push('Cannot fetch "' + uri + '" as no protocol given.');
148    return whenLoaded(null);
149  } else if (!isAllowed) {
150    applyContext.warnings.push('Cannot fetch "' + uri + '" as resource is not allowed.');
151    return whenLoaded(null);
152  }
153
154  applyContext.fetch(uri, applyContext.inlineRequest, applyContext.inlineTimeout, function (error, body) {
155    if (error) {
156      applyContext.warnings.push('Missing source map at "' + uri + '" - ' + error);
157      return whenLoaded(null);
158    }
159
160    whenLoaded(body);
161  });
162}
163
164function loadInputSourceMapFromLocalUri(uri, applyContext) {
165  var isAllowed = isAllowedResource(uri, false, applyContext.inline);
166  var sourceMap;
167
168  if (!fs.existsSync(uri) || !fs.statSync(uri).isFile()) {
169    applyContext.warnings.push('Ignoring local source map at "' + uri + '" as resource is missing.');
170    return null;
171  } else if (!isAllowed) {
172    applyContext.warnings.push('Cannot fetch "' + uri + '" as resource is not allowed.');
173    return null;
174  }
175
176  sourceMap = fs.readFileSync(uri, 'utf-8');
177  return JSON.parse(sourceMap);
178}
179
180function applySourceMapRecursively(tokens, inputSourceMapTracker) {
181  var token;
182  var i, l;
183
184  for (i = 0, l = tokens.length; i < l; i++) {
185    token = tokens[i];
186
187    switch (token[0]) {
188      case Token.AT_RULE:
189        applySourceMapTo(token, inputSourceMapTracker);
190        break;
191      case Token.AT_RULE_BLOCK:
192        applySourceMapRecursively(token[1], inputSourceMapTracker);
193        applySourceMapRecursively(token[2], inputSourceMapTracker);
194        break;
195      case Token.AT_RULE_BLOCK_SCOPE:
196        applySourceMapTo(token, inputSourceMapTracker);
197        break;
198      case Token.NESTED_BLOCK:
199        applySourceMapRecursively(token[1], inputSourceMapTracker);
200        applySourceMapRecursively(token[2], inputSourceMapTracker);
201        break;
202      case Token.NESTED_BLOCK_SCOPE:
203        applySourceMapTo(token, inputSourceMapTracker);
204        break;
205      case Token.COMMENT:
206        applySourceMapTo(token, inputSourceMapTracker);
207        break;
208      case Token.PROPERTY:
209        applySourceMapRecursively(token, inputSourceMapTracker);
210        break;
211      case Token.PROPERTY_BLOCK:
212        applySourceMapRecursively(token[1], inputSourceMapTracker);
213        break;
214      case Token.PROPERTY_NAME:
215        applySourceMapTo(token, inputSourceMapTracker);
216        break;
217      case Token.PROPERTY_VALUE:
218        applySourceMapTo(token, inputSourceMapTracker);
219        break;
220      case Token.RULE:
221        applySourceMapRecursively(token[1], inputSourceMapTracker);
222        applySourceMapRecursively(token[2], inputSourceMapTracker);
223        break;
224      case Token.RULE_SCOPE:
225        applySourceMapTo(token, inputSourceMapTracker);
226    }
227  }
228
229  return tokens;
230}
231
232function applySourceMapTo(token, inputSourceMapTracker) {
233  var value = token[1];
234  var metadata = token[2];
235  var newMetadata = [];
236  var i, l;
237
238  for (i = 0, l = metadata.length; i < l; i++) {
239    newMetadata.push(inputSourceMapTracker.originalPositionFor(metadata[i], value.length));
240  }
241
242  token[2] = newMetadata;
243}
244
245module.exports = applySourceMaps;
246