1var fs = require('fs');
2var path = require('path');
3
4var applySourceMaps = require('./apply-source-maps');
5var extractImportUrlAndMedia = require('./extract-import-url-and-media');
6var isAllowedResource = require('./is-allowed-resource');
7var loadOriginalSources = require('./load-original-sources');
8var normalizePath = require('./normalize-path');
9var rebase = require('./rebase');
10var rebaseLocalMap = require('./rebase-local-map');
11var rebaseRemoteMap = require('./rebase-remote-map');
12var restoreImport = require('./restore-import');
13
14var tokenize = require('../tokenizer/tokenize');
15var Token = require('../tokenizer/token');
16var Marker = require('../tokenizer/marker');
17var hasProtocol = require('../utils/has-protocol');
18var isImport = require('../utils/is-import');
19var isRemoteResource = require('../utils/is-remote-resource');
20
21var UNKNOWN_URI = 'uri:unknown';
22
23function readSources(input, context, callback) {
24  return doReadSources(input, context, function (tokens) {
25    return applySourceMaps(tokens, context, function () {
26      return loadOriginalSources(context, function () { return callback(tokens); });
27    });
28  });
29}
30
31function doReadSources(input, context, callback) {
32  if (typeof input == 'string') {
33    return fromString(input, context, callback);
34  } else if (Buffer.isBuffer(input)) {
35    return fromString(input.toString(), context, callback);
36  } else if (Array.isArray(input)) {
37    return fromArray(input, context, callback);
38  } else if (typeof input == 'object') {
39    return fromHash(input, context, callback);
40  }
41}
42
43function fromString(input, context, callback) {
44  context.source = undefined;
45  context.sourcesContent[undefined] = input;
46  context.stats.originalSize += input.length;
47
48  return fromStyles(input, context, { inline: context.options.inline }, callback);
49}
50
51function fromArray(input, context, callback) {
52  var inputAsImports = input.reduce(function (accumulator, uriOrHash) {
53    if (typeof uriOrHash === 'string') {
54      return addStringSource(uriOrHash, accumulator);
55    } else {
56      return addHashSource(uriOrHash, context, accumulator);
57    }
58
59  }, []);
60
61  return fromStyles(inputAsImports.join(''), context, { inline: ['all'] }, callback);
62}
63
64function fromHash(input, context, callback) {
65  var inputAsImports = addHashSource(input, context, []);
66  return fromStyles(inputAsImports.join(''), context, { inline: ['all'] }, callback);
67}
68
69function addStringSource(input, imports) {
70  imports.push(restoreAsImport(normalizeUri(input)));
71  return imports;
72}
73
74function addHashSource(input, context, imports) {
75  var uri;
76  var normalizedUri;
77  var source;
78
79  for (uri in input) {
80    source = input[uri];
81    normalizedUri = normalizeUri(uri);
82
83    imports.push(restoreAsImport(normalizedUri));
84
85    context.sourcesContent[normalizedUri] = source.styles;
86
87    if (source.sourceMap) {
88      trackSourceMap(source.sourceMap, normalizedUri, context);
89    }
90  }
91
92  return imports;
93}
94
95function normalizeUri(uri) {
96  var currentPath = path.resolve('');
97  var absoluteUri;
98  var relativeToCurrentPath;
99  var normalizedUri;
100
101  if (isRemoteResource(uri)) {
102    return uri;
103  }
104
105  absoluteUri = path.isAbsolute(uri) ?
106    uri :
107    path.resolve(uri);
108  relativeToCurrentPath = path.relative(currentPath, absoluteUri);
109  normalizedUri = normalizePath(relativeToCurrentPath);
110
111  return normalizedUri;
112}
113
114function trackSourceMap(sourceMap, uri, context) {
115  var parsedMap = typeof sourceMap == 'string' ?
116      JSON.parse(sourceMap) :
117      sourceMap;
118  var rebasedMap = isRemoteResource(uri) ?
119    rebaseRemoteMap(parsedMap, uri) :
120    rebaseLocalMap(parsedMap, uri || UNKNOWN_URI, context.options.rebaseTo);
121
122  context.inputSourceMapTracker.track(uri, rebasedMap);
123}
124
125function restoreAsImport(uri) {
126  return restoreImport('url(' + uri + ')', '') + Marker.SEMICOLON;
127}
128
129function fromStyles(styles, context, parentInlinerContext, callback) {
130  var tokens;
131  var rebaseConfig = {};
132
133  if (!context.source) {
134    rebaseConfig.fromBase = path.resolve('');
135    rebaseConfig.toBase = context.options.rebaseTo;
136  } else if (isRemoteResource(context.source)) {
137    rebaseConfig.fromBase = context.source;
138    rebaseConfig.toBase = context.source;
139  } else if (path.isAbsolute(context.source)) {
140    rebaseConfig.fromBase = path.dirname(context.source);
141    rebaseConfig.toBase = context.options.rebaseTo;
142  } else {
143    rebaseConfig.fromBase = path.dirname(path.resolve(context.source));
144    rebaseConfig.toBase = context.options.rebaseTo;
145  }
146
147  tokens = tokenize(styles, context);
148  tokens = rebase(tokens, context.options.rebase, context.validator, rebaseConfig);
149
150  return allowsAnyImports(parentInlinerContext.inline) ?
151    inline(tokens, context, parentInlinerContext, callback) :
152    callback(tokens);
153}
154
155function allowsAnyImports(inline) {
156  return !(inline.length == 1 && inline[0] == 'none');
157}
158
159function inline(tokens, externalContext, parentInlinerContext, callback) {
160  var inlinerContext = {
161    afterContent: false,
162    callback: callback,
163    errors: externalContext.errors,
164    externalContext: externalContext,
165    fetch: externalContext.options.fetch,
166    inlinedStylesheets: parentInlinerContext.inlinedStylesheets || externalContext.inlinedStylesheets,
167    inline: parentInlinerContext.inline,
168    inlineRequest: externalContext.options.inlineRequest,
169    inlineTimeout: externalContext.options.inlineTimeout,
170    isRemote: parentInlinerContext.isRemote || false,
171    localOnly: externalContext.localOnly,
172    outputTokens: [],
173    rebaseTo: externalContext.options.rebaseTo,
174    sourceTokens: tokens,
175    warnings: externalContext.warnings
176  };
177
178  return doInlineImports(inlinerContext);
179}
180
181function doInlineImports(inlinerContext) {
182  var token;
183  var i, l;
184
185  for (i = 0, l = inlinerContext.sourceTokens.length; i < l; i++) {
186    token = inlinerContext.sourceTokens[i];
187
188    if (token[0] == Token.AT_RULE && isImport(token[1])) {
189      inlinerContext.sourceTokens.splice(0, i);
190      return inlineStylesheet(token, inlinerContext);
191    } else if (token[0] == Token.AT_RULE || token[0] == Token.COMMENT) {
192      inlinerContext.outputTokens.push(token);
193    } else {
194      inlinerContext.outputTokens.push(token);
195      inlinerContext.afterContent = true;
196    }
197  }
198
199  inlinerContext.sourceTokens = [];
200  return inlinerContext.callback(inlinerContext.outputTokens);
201}
202
203function inlineStylesheet(token, inlinerContext) {
204  var uriAndMediaQuery = extractImportUrlAndMedia(token[1]);
205  var uri = uriAndMediaQuery[0];
206  var mediaQuery = uriAndMediaQuery[1];
207  var metadata = token[2];
208
209  return isRemoteResource(uri) ?
210    inlineRemoteStylesheet(uri, mediaQuery, metadata, inlinerContext) :
211    inlineLocalStylesheet(uri, mediaQuery, metadata, inlinerContext);
212}
213
214function inlineRemoteStylesheet(uri, mediaQuery, metadata, inlinerContext) {
215  var isAllowed = isAllowedResource(uri, true, inlinerContext.inline);
216  var originalUri = uri;
217  var isLoaded = uri in inlinerContext.externalContext.sourcesContent;
218  var isRuntimeResource = !hasProtocol(uri);
219
220  if (inlinerContext.inlinedStylesheets.indexOf(uri) > -1) {
221    inlinerContext.warnings.push('Ignoring remote @import of "' + uri + '" as it has already been imported.');
222    inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
223    return doInlineImports(inlinerContext);
224  } else if (inlinerContext.localOnly && inlinerContext.afterContent) {
225    inlinerContext.warnings.push('Ignoring remote @import of "' + uri + '" as no callback given and after other content.');
226    inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
227    return doInlineImports(inlinerContext);
228  } else if (isRuntimeResource) {
229    inlinerContext.warnings.push('Skipping remote @import of "' + uri + '" as no protocol given.');
230    inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1));
231    inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
232    return doInlineImports(inlinerContext);
233  } else if (inlinerContext.localOnly && !isLoaded) {
234    inlinerContext.warnings.push('Skipping remote @import of "' + uri + '" as no callback given.');
235    inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1));
236    inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
237    return doInlineImports(inlinerContext);
238  } else if (!isAllowed && inlinerContext.afterContent) {
239    inlinerContext.warnings.push('Ignoring remote @import of "' + uri + '" as resource is not allowed and after other content.');
240    inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
241    return doInlineImports(inlinerContext);
242  } else if (!isAllowed) {
243    inlinerContext.warnings.push('Skipping remote @import of "' + uri + '" as resource is not allowed.');
244    inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1));
245    inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
246    return doInlineImports(inlinerContext);
247  }
248
249  inlinerContext.inlinedStylesheets.push(uri);
250
251  function whenLoaded(error, importedStyles) {
252    if (error) {
253      inlinerContext.errors.push('Broken @import declaration of "' + uri + '" - ' + error);
254
255      return process.nextTick(function () {
256        inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1));
257        inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
258        doInlineImports(inlinerContext);
259      });
260    }
261
262    inlinerContext.inline = inlinerContext.externalContext.options.inline;
263    inlinerContext.isRemote = true;
264
265    inlinerContext.externalContext.source = originalUri;
266    inlinerContext.externalContext.sourcesContent[uri] = importedStyles;
267    inlinerContext.externalContext.stats.originalSize += importedStyles.length;
268
269    return fromStyles(importedStyles, inlinerContext.externalContext, inlinerContext, function (importedTokens) {
270      importedTokens = wrapInMedia(importedTokens, mediaQuery, metadata);
271
272      inlinerContext.outputTokens = inlinerContext.outputTokens.concat(importedTokens);
273      inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
274
275      return doInlineImports(inlinerContext);
276    });
277  }
278
279  return isLoaded ?
280    whenLoaded(null, inlinerContext.externalContext.sourcesContent[uri]) :
281    inlinerContext.fetch(uri, inlinerContext.inlineRequest, inlinerContext.inlineTimeout, whenLoaded);
282}
283
284function inlineLocalStylesheet(uri, mediaQuery, metadata, inlinerContext) {
285  var currentPath = path.resolve('');
286  var absoluteUri = path.isAbsolute(uri) ?
287    path.resolve(currentPath, uri[0] == '/' ? uri.substring(1) : uri) :
288    path.resolve(inlinerContext.rebaseTo, uri);
289  var relativeToCurrentPath = path.relative(currentPath, absoluteUri);
290  var importedStyles;
291  var isAllowed = isAllowedResource(uri, false, inlinerContext.inline);
292  var normalizedPath = normalizePath(relativeToCurrentPath);
293  var isLoaded = normalizedPath in inlinerContext.externalContext.sourcesContent;
294
295  if (inlinerContext.inlinedStylesheets.indexOf(absoluteUri) > -1) {
296    inlinerContext.warnings.push('Ignoring local @import of "' + uri + '" as it has already been imported.');
297  } else if (!isLoaded && (!fs.existsSync(absoluteUri) || !fs.statSync(absoluteUri).isFile())) {
298    inlinerContext.errors.push('Ignoring local @import of "' + uri + '" as resource is missing.');
299  } else if (!isAllowed && inlinerContext.afterContent) {
300    inlinerContext.warnings.push('Ignoring local @import of "' + uri + '" as resource is not allowed and after other content.');
301  } else if (inlinerContext.afterContent) {
302    inlinerContext.warnings.push('Ignoring local @import of "' + uri + '" as after other content.');
303  } else if (!isAllowed) {
304    inlinerContext.warnings.push('Skipping local @import of "' + uri + '" as resource is not allowed.');
305    inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1));
306  } else {
307    importedStyles = isLoaded ?
308      inlinerContext.externalContext.sourcesContent[normalizedPath] :
309      fs.readFileSync(absoluteUri, 'utf-8');
310
311    inlinerContext.inlinedStylesheets.push(absoluteUri);
312    inlinerContext.inline = inlinerContext.externalContext.options.inline;
313
314    inlinerContext.externalContext.source = normalizedPath;
315    inlinerContext.externalContext.sourcesContent[normalizedPath] = importedStyles;
316    inlinerContext.externalContext.stats.originalSize += importedStyles.length;
317
318    return fromStyles(importedStyles, inlinerContext.externalContext, inlinerContext, function (importedTokens) {
319      importedTokens = wrapInMedia(importedTokens, mediaQuery, metadata);
320
321      inlinerContext.outputTokens = inlinerContext.outputTokens.concat(importedTokens);
322      inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
323
324      return doInlineImports(inlinerContext);
325    });
326  }
327
328  inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
329
330  return doInlineImports(inlinerContext);
331}
332
333function wrapInMedia(tokens, mediaQuery, metadata) {
334  if (mediaQuery) {
335    return [[Token.NESTED_BLOCK, [[Token.NESTED_BLOCK_SCOPE, '@media ' + mediaQuery, metadata]], tokens]];
336  } else {
337    return tokens;
338  }
339}
340
341module.exports = readSources;
342