var fs = require('fs'); var path = require('path'); var applySourceMaps = require('./apply-source-maps'); var extractImportUrlAndMedia = require('./extract-import-url-and-media'); var isAllowedResource = require('./is-allowed-resource'); var loadOriginalSources = require('./load-original-sources'); var normalizePath = require('./normalize-path'); var rebase = require('./rebase'); var rebaseLocalMap = require('./rebase-local-map'); var rebaseRemoteMap = require('./rebase-remote-map'); var restoreImport = require('./restore-import'); var tokenize = require('../tokenizer/tokenize'); var Token = require('../tokenizer/token'); var Marker = require('../tokenizer/marker'); var hasProtocol = require('../utils/has-protocol'); var isImport = require('../utils/is-import'); var isRemoteResource = require('../utils/is-remote-resource'); var UNKNOWN_URI = 'uri:unknown'; function readSources(input, context, callback) { return doReadSources(input, context, function (tokens) { return applySourceMaps(tokens, context, function () { return loadOriginalSources(context, function () { return callback(tokens); }); }); }); } function doReadSources(input, context, callback) { if (typeof input == 'string') { return fromString(input, context, callback); } else if (Buffer.isBuffer(input)) { return fromString(input.toString(), context, callback); } else if (Array.isArray(input)) { return fromArray(input, context, callback); } else if (typeof input == 'object') { return fromHash(input, context, callback); } } function fromString(input, context, callback) { context.source = undefined; context.sourcesContent[undefined] = input; context.stats.originalSize += input.length; return fromStyles(input, context, { inline: context.options.inline }, callback); } function fromArray(input, context, callback) { var inputAsImports = input.reduce(function (accumulator, uriOrHash) { if (typeof uriOrHash === 'string') { return addStringSource(uriOrHash, accumulator); } else { return addHashSource(uriOrHash, context, accumulator); } }, []); return fromStyles(inputAsImports.join(''), context, { inline: ['all'] }, callback); } function fromHash(input, context, callback) { var inputAsImports = addHashSource(input, context, []); return fromStyles(inputAsImports.join(''), context, { inline: ['all'] }, callback); } function addStringSource(input, imports) { imports.push(restoreAsImport(normalizeUri(input))); return imports; } function addHashSource(input, context, imports) { var uri; var normalizedUri; var source; for (uri in input) { source = input[uri]; normalizedUri = normalizeUri(uri); imports.push(restoreAsImport(normalizedUri)); context.sourcesContent[normalizedUri] = source.styles; if (source.sourceMap) { trackSourceMap(source.sourceMap, normalizedUri, context); } } return imports; } function normalizeUri(uri) { var currentPath = path.resolve(''); var absoluteUri; var relativeToCurrentPath; var normalizedUri; if (isRemoteResource(uri)) { return uri; } absoluteUri = path.isAbsolute(uri) ? uri : path.resolve(uri); relativeToCurrentPath = path.relative(currentPath, absoluteUri); normalizedUri = normalizePath(relativeToCurrentPath); return normalizedUri; } function trackSourceMap(sourceMap, uri, context) { var parsedMap = typeof sourceMap == 'string' ? JSON.parse(sourceMap) : sourceMap; var rebasedMap = isRemoteResource(uri) ? rebaseRemoteMap(parsedMap, uri) : rebaseLocalMap(parsedMap, uri || UNKNOWN_URI, context.options.rebaseTo); context.inputSourceMapTracker.track(uri, rebasedMap); } function restoreAsImport(uri) { return restoreImport('url(' + uri + ')', '') + Marker.SEMICOLON; } function fromStyles(styles, context, parentInlinerContext, callback) { var tokens; var rebaseConfig = {}; if (!context.source) { rebaseConfig.fromBase = path.resolve(''); rebaseConfig.toBase = context.options.rebaseTo; } else if (isRemoteResource(context.source)) { rebaseConfig.fromBase = context.source; rebaseConfig.toBase = context.source; } else if (path.isAbsolute(context.source)) { rebaseConfig.fromBase = path.dirname(context.source); rebaseConfig.toBase = context.options.rebaseTo; } else { rebaseConfig.fromBase = path.dirname(path.resolve(context.source)); rebaseConfig.toBase = context.options.rebaseTo; } tokens = tokenize(styles, context); tokens = rebase(tokens, context.options.rebase, context.validator, rebaseConfig); return allowsAnyImports(parentInlinerContext.inline) ? inline(tokens, context, parentInlinerContext, callback) : callback(tokens); } function allowsAnyImports(inline) { return !(inline.length == 1 && inline[0] == 'none'); } function inline(tokens, externalContext, parentInlinerContext, callback) { var inlinerContext = { afterContent: false, callback: callback, errors: externalContext.errors, externalContext: externalContext, fetch: externalContext.options.fetch, inlinedStylesheets: parentInlinerContext.inlinedStylesheets || externalContext.inlinedStylesheets, inline: parentInlinerContext.inline, inlineRequest: externalContext.options.inlineRequest, inlineTimeout: externalContext.options.inlineTimeout, isRemote: parentInlinerContext.isRemote || false, localOnly: externalContext.localOnly, outputTokens: [], rebaseTo: externalContext.options.rebaseTo, sourceTokens: tokens, warnings: externalContext.warnings }; return doInlineImports(inlinerContext); } function doInlineImports(inlinerContext) { var token; var i, l; for (i = 0, l = inlinerContext.sourceTokens.length; i < l; i++) { token = inlinerContext.sourceTokens[i]; if (token[0] == Token.AT_RULE && isImport(token[1])) { inlinerContext.sourceTokens.splice(0, i); return inlineStylesheet(token, inlinerContext); } else if (token[0] == Token.AT_RULE || token[0] == Token.COMMENT) { inlinerContext.outputTokens.push(token); } else { inlinerContext.outputTokens.push(token); inlinerContext.afterContent = true; } } inlinerContext.sourceTokens = []; return inlinerContext.callback(inlinerContext.outputTokens); } function inlineStylesheet(token, inlinerContext) { var uriAndMediaQuery = extractImportUrlAndMedia(token[1]); var uri = uriAndMediaQuery[0]; var mediaQuery = uriAndMediaQuery[1]; var metadata = token[2]; return isRemoteResource(uri) ? inlineRemoteStylesheet(uri, mediaQuery, metadata, inlinerContext) : inlineLocalStylesheet(uri, mediaQuery, metadata, inlinerContext); } function inlineRemoteStylesheet(uri, mediaQuery, metadata, inlinerContext) { var isAllowed = isAllowedResource(uri, true, inlinerContext.inline); var originalUri = uri; var isLoaded = uri in inlinerContext.externalContext.sourcesContent; var isRuntimeResource = !hasProtocol(uri); if (inlinerContext.inlinedStylesheets.indexOf(uri) > -1) { inlinerContext.warnings.push('Ignoring remote @import of "' + uri + '" as it has already been imported.'); inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); return doInlineImports(inlinerContext); } else if (inlinerContext.localOnly && inlinerContext.afterContent) { inlinerContext.warnings.push('Ignoring remote @import of "' + uri + '" as no callback given and after other content.'); inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); return doInlineImports(inlinerContext); } else if (isRuntimeResource) { inlinerContext.warnings.push('Skipping remote @import of "' + uri + '" as no protocol given.'); inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1)); inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); return doInlineImports(inlinerContext); } else if (inlinerContext.localOnly && !isLoaded) { inlinerContext.warnings.push('Skipping remote @import of "' + uri + '" as no callback given.'); inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1)); inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); return doInlineImports(inlinerContext); } else if (!isAllowed && inlinerContext.afterContent) { inlinerContext.warnings.push('Ignoring remote @import of "' + uri + '" as resource is not allowed and after other content.'); inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); return doInlineImports(inlinerContext); } else if (!isAllowed) { inlinerContext.warnings.push('Skipping remote @import of "' + uri + '" as resource is not allowed.'); inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1)); inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); return doInlineImports(inlinerContext); } inlinerContext.inlinedStylesheets.push(uri); function whenLoaded(error, importedStyles) { if (error) { inlinerContext.errors.push('Broken @import declaration of "' + uri + '" - ' + error); return process.nextTick(function () { inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1)); inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); doInlineImports(inlinerContext); }); } inlinerContext.inline = inlinerContext.externalContext.options.inline; inlinerContext.isRemote = true; inlinerContext.externalContext.source = originalUri; inlinerContext.externalContext.sourcesContent[uri] = importedStyles; inlinerContext.externalContext.stats.originalSize += importedStyles.length; return fromStyles(importedStyles, inlinerContext.externalContext, inlinerContext, function (importedTokens) { importedTokens = wrapInMedia(importedTokens, mediaQuery, metadata); inlinerContext.outputTokens = inlinerContext.outputTokens.concat(importedTokens); inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); return doInlineImports(inlinerContext); }); } return isLoaded ? whenLoaded(null, inlinerContext.externalContext.sourcesContent[uri]) : inlinerContext.fetch(uri, inlinerContext.inlineRequest, inlinerContext.inlineTimeout, whenLoaded); } function inlineLocalStylesheet(uri, mediaQuery, metadata, inlinerContext) { var currentPath = path.resolve(''); var absoluteUri = path.isAbsolute(uri) ? path.resolve(currentPath, uri[0] == '/' ? uri.substring(1) : uri) : path.resolve(inlinerContext.rebaseTo, uri); var relativeToCurrentPath = path.relative(currentPath, absoluteUri); var importedStyles; var isAllowed = isAllowedResource(uri, false, inlinerContext.inline); var normalizedPath = normalizePath(relativeToCurrentPath); var isLoaded = normalizedPath in inlinerContext.externalContext.sourcesContent; if (inlinerContext.inlinedStylesheets.indexOf(absoluteUri) > -1) { inlinerContext.warnings.push('Ignoring local @import of "' + uri + '" as it has already been imported.'); } else if (!isLoaded && (!fs.existsSync(absoluteUri) || !fs.statSync(absoluteUri).isFile())) { inlinerContext.errors.push('Ignoring local @import of "' + uri + '" as resource is missing.'); } else if (!isAllowed && inlinerContext.afterContent) { inlinerContext.warnings.push('Ignoring local @import of "' + uri + '" as resource is not allowed and after other content.'); } else if (inlinerContext.afterContent) { inlinerContext.warnings.push('Ignoring local @import of "' + uri + '" as after other content.'); } else if (!isAllowed) { inlinerContext.warnings.push('Skipping local @import of "' + uri + '" as resource is not allowed.'); inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1)); } else { importedStyles = isLoaded ? inlinerContext.externalContext.sourcesContent[normalizedPath] : fs.readFileSync(absoluteUri, 'utf-8'); inlinerContext.inlinedStylesheets.push(absoluteUri); inlinerContext.inline = inlinerContext.externalContext.options.inline; inlinerContext.externalContext.source = normalizedPath; inlinerContext.externalContext.sourcesContent[normalizedPath] = importedStyles; inlinerContext.externalContext.stats.originalSize += importedStyles.length; return fromStyles(importedStyles, inlinerContext.externalContext, inlinerContext, function (importedTokens) { importedTokens = wrapInMedia(importedTokens, mediaQuery, metadata); inlinerContext.outputTokens = inlinerContext.outputTokens.concat(importedTokens); inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); return doInlineImports(inlinerContext); }); } inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1); return doInlineImports(inlinerContext); } function wrapInMedia(tokens, mediaQuery, metadata) { if (mediaQuery) { return [[Token.NESTED_BLOCK, [[Token.NESTED_BLOCK_SCOPE, '@media ' + mediaQuery, metadata]], tokens]]; } else { return tokens; } } module.exports = readSources;