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