* Requires Node.js 4.0+ to run.
* Replaces the old tokenizer with a new one which doesn't use any escaping.
+* Replaces the old `@import` inlining with one on top of the new tokenizer.
[3.4.22 / 2016-12-12](https://github.com/jakubpawlowicz/clean-css/compare/v3.4.21...v3.4.22)
==================
+++ /dev/null
-var fs = require('fs');
-var path = require('path');
-var http = require('http');
-var https = require('https');
-var url = require('url');
-
-var rewriteUrls = require('../urls/rewrite');
-var split = require('../utils/split');
-var override = require('../utils/object.js').override;
-
-var MAP_MARKER = /\/\*# sourceMappingURL=(\S+) \*\//;
-var REMOTE_RESOURCE = /^(https?:)?\/\//;
-var NO_PROTOCOL_RESOURCE = /^\/\//;
-
-function ImportInliner (context) {
- this.outerContext = context;
-}
-
-ImportInliner.prototype.process = function (data, context) {
- var root = this.outerContext.options.root;
-
- context = override(context, {
- baseRelativeTo: this.outerContext.options.relativeTo || root,
- debug: this.outerContext.options.debug,
- done: [],
- errors: this.outerContext.errors,
- left: [],
- inliner: this.outerContext.options.inliner,
- rebase: this.outerContext.options.rebase,
- relativeTo: this.outerContext.options.relativeTo || root,
- root: root,
- sourceReader: this.outerContext.sourceReader,
- sourceTracker: this.outerContext.sourceTracker,
- warnings: this.outerContext.warnings,
- visited: []
- });
-
- return importFrom(data, context);
-};
-
-function importFrom(data, context) {
- if (context.shallow) {
- context.shallow = false;
- context.done.push(data);
- return processNext(context);
- }
-
- var nextStart = 0;
- var nextEnd = 0;
- var cursor = 0;
- var isComment = commentScanner(data);
-
- for (; nextEnd < data.length;) {
- nextStart = nextImportAt(data, cursor);
- if (nextStart == -1)
- break;
-
- if (isComment(nextStart)) {
- cursor = nextStart + 1;
- continue;
- }
-
- nextEnd = data.indexOf(';', nextStart);
- if (nextEnd == -1) {
- cursor = data.length;
- data = '';
- break;
- }
-
- var noImportPart = data.substring(0, nextStart);
- context.done.push(noImportPart);
- context.left.unshift([data.substring(nextEnd + 1), override(context, { shallow: false })]);
- context.afterContent = hasContent(noImportPart);
- return inline(data, nextStart, nextEnd, context);
- }
-
- // no @import matched in current data
- context.done.push(data);
- return processNext(context);
-}
-
-function rebaseMap(data, source) {
- return data.replace(MAP_MARKER, function (match, sourceMapUrl) {
- return REMOTE_RESOURCE.test(sourceMapUrl) ?
- match :
- match.replace(sourceMapUrl, url.resolve(source, sourceMapUrl));
- });
-}
-
-function nextImportAt(data, cursor) {
- var nextLowerCase = data.indexOf('@import', cursor);
- var nextUpperCase = data.indexOf('@IMPORT', cursor);
-
- if (nextLowerCase > -1 && nextUpperCase == -1)
- return nextLowerCase;
- else if (nextLowerCase == -1 && nextUpperCase > -1)
- return nextUpperCase;
- else
- return Math.min(nextLowerCase, nextUpperCase);
-}
-
-function processNext(context) {
- return context.left.length > 0 ?
- importFrom.apply(null, context.left.shift()) :
- context.whenDone(context.done.join(''));
-}
-
-function commentScanner(data) {
- var commentRegex = /(\/\*(?!\*\/)[\s\S]*?\*\/)/;
- var lastStartIndex = 0;
- var lastEndIndex = 0;
- var noComments = false;
-
- // test whether an index is located within a comment
- return function scanner(idx) {
- var comment;
- var localStartIndex = 0;
- var localEndIndex = 0;
- var globalStartIndex = 0;
- var globalEndIndex = 0;
-
- // return if we know there are no more comments
- if (noComments)
- return false;
-
- do {
- // idx can be still within last matched comment (many @import statements inside one comment)
- if (idx > lastStartIndex && idx < lastEndIndex)
- return true;
-
- comment = data.match(commentRegex);
-
- if (!comment) {
- noComments = true;
- return false;
- }
-
- // get the indexes relative to the current data chunk
- lastStartIndex = localStartIndex = comment.index;
- localEndIndex = localStartIndex + comment[0].length;
-
- // calculate the indexes relative to the full original data
- globalEndIndex = localEndIndex + lastEndIndex;
- globalStartIndex = globalEndIndex - comment[0].length;
-
- // chop off data up to and including current comment block
- data = data.substring(localEndIndex);
- lastEndIndex = globalEndIndex;
- } while (globalEndIndex < idx);
-
- return globalEndIndex > idx && idx > globalStartIndex;
- };
-}
-
-function hasContent(data) {
- var isComment = commentScanner(data);
- var firstContentIdx = -1;
- while (true) {
- firstContentIdx = data.indexOf('{', firstContentIdx + 1);
- if (firstContentIdx == -1 || !isComment(firstContentIdx))
- break;
- }
-
- return firstContentIdx > -1;
-}
-
-function inline(data, nextStart, nextEnd, context) {
- context.shallow = data.indexOf('@shallow') > 0;
-
- var importDeclaration = data
- .substring(nextImportAt(data, nextStart) + '@import'.length + 1, nextEnd)
- .replace(/@shallow\)$/, ')')
- .trim();
-
- var viaUrl = importDeclaration.indexOf('url(') === 0;
- var urlStartsAt = viaUrl ? 4 : 0;
- var isQuoted = /^['"]/.exec(importDeclaration.substring(urlStartsAt, urlStartsAt + 2));
- var urlEndsAt = isQuoted ?
- importDeclaration.indexOf(isQuoted[0], urlStartsAt + 1) :
- split(importDeclaration, ' ')[0].length - (viaUrl ? 1 : 0);
-
- var importedFile = importDeclaration
- .substring(urlStartsAt, urlEndsAt)
- .replace(/['"]/g, '')
- .replace(/\)$/, '')
- .trim();
-
- var mediaQuery = importDeclaration
- .substring(urlEndsAt + 1)
- .replace(/^\)/, '')
- .trim();
-
- var isRemote = context.isRemote || REMOTE_RESOURCE.test(importedFile);
-
- if (isRemote && (context.localOnly || !allowedResource(importedFile, true, context.imports))) {
- if (context.afterContent || hasContent(context.done.join('')))
- context.warnings.push('Ignoring remote @import of "' + importedFile + '" as no callback given.');
- else
- restoreImport(importedFile, mediaQuery, context);
-
- return processNext(context);
- }
-
- if (!isRemote && !allowedResource(importedFile, false, context.imports)) {
- if (context.afterImport)
- context.warnings.push('Ignoring local @import of "' + importedFile + '" as after other inlined content.');
- else
- restoreImport(importedFile, mediaQuery, context);
- return processNext(context);
- }
-
- if (!isRemote && context.afterContent) {
- context.warnings.push('Ignoring local @import of "' + importedFile + '" as after other CSS content.');
- return processNext(context);
- }
-
- var method = isRemote ? inlineRemoteResource : inlineLocalResource;
- return method(importedFile, mediaQuery, context);
-}
-
-function allowedResource(importedFile, isRemote, rules) {
- if (rules.length === 0)
- return false;
-
- if (isRemote && NO_PROTOCOL_RESOURCE.test(importedFile))
- importedFile = 'http:' + importedFile;
-
- var match = isRemote ?
- url.parse(importedFile).host :
- importedFile;
- var allowed = true;
-
- for (var i = 0; i < rules.length; i++) {
- var rule = rules[i];
-
- if (rule == 'all')
- allowed = true;
- else if (isRemote && rule == 'local')
- allowed = false;
- else if (isRemote && rule == 'remote')
- allowed = true;
- else if (!isRemote && rule == 'remote')
- allowed = false;
- else if (!isRemote && rule == 'local')
- allowed = true;
- else if (rule[0] == '!' && rule.substring(1) === match)
- allowed = false;
- }
-
- return allowed;
-}
-
-function inlineRemoteResource(importedFile, mediaQuery, context) {
- var importedUrl = REMOTE_RESOURCE.test(importedFile) ?
- importedFile :
- url.resolve(context.relativeTo, importedFile);
- var originalUrl = importedUrl;
-
- if (NO_PROTOCOL_RESOURCE.test(importedUrl))
- importedUrl = 'http:' + importedUrl;
-
- if (context.visited.indexOf(importedUrl) > -1)
- return processNext(context);
-
-
- if (context.debug)
- console.error('Inlining remote stylesheet: ' + importedUrl);
-
- context.visited.push(importedUrl);
-
- var proxyProtocol = context.inliner.request.protocol || context.inliner.request.hostname;
- var get =
- ((proxyProtocol && proxyProtocol.indexOf('https://') !== 0 ) ||
- importedUrl.indexOf('http://') === 0) ?
- http.get :
- https.get;
-
- var errorHandled = false;
- function handleError(message) {
- if (errorHandled)
- return;
-
- errorHandled = true;
- context.errors.push('Broken @import declaration of "' + importedUrl + '" - ' + message);
- restoreImport(importedUrl, mediaQuery, context);
-
- process.nextTick(function () {
- processNext(context);
- });
- }
-
- var requestOptions = override(url.parse(importedUrl), context.inliner.request);
- if (context.inliner.request.hostname !== undefined) {
-
- //overwrite as we always expect a http proxy currently
- requestOptions.protocol = context.inliner.request.protocol || 'http:';
- requestOptions.path = requestOptions.href;
- }
-
-
- get(requestOptions, function (res) {
- if (res.statusCode < 200 || res.statusCode > 399) {
- return handleError('error ' + res.statusCode);
- } else if (res.statusCode > 299) {
- var movedUrl = url.resolve(importedUrl, res.headers.location);
- return inlineRemoteResource(movedUrl, mediaQuery, context);
- }
-
- var chunks = [];
- var parsedUrl = url.parse(importedUrl);
- res.on('data', function (chunk) {
- chunks.push(chunk.toString());
- });
- res.on('end', function () {
- var importedData = chunks.join('');
- if (context.rebase)
- importedData = rewriteUrls(importedData, { toBase: originalUrl }, context);
- context.sourceReader.trackSource(importedUrl, importedData);
- importedData = context.sourceTracker.store(importedUrl, importedData);
- importedData = rebaseMap(importedData, importedUrl);
-
- if (mediaQuery.length > 0)
- importedData = '@media ' + mediaQuery + '{' + importedData + '}';
-
- context.afterImport = true;
-
- var newContext = override(context, {
- isRemote: true,
- relativeTo: parsedUrl.protocol + '//' + parsedUrl.host + parsedUrl.pathname
- });
-
- process.nextTick(function () {
- importFrom(importedData, newContext);
- });
- });
- })
- .on('error', function (res) {
- handleError(res.message);
- })
- .on('timeout', function () {
- handleError('timeout');
- })
- .setTimeout(context.inliner.timeout);
-}
-
-function inlineLocalResource(importedFile, mediaQuery, context) {
- var relativeTo = importedFile[0] == '/' ?
- context.root :
- context.relativeTo;
-
- var fullPath = path.resolve(path.join(relativeTo, importedFile));
-
- if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) {
- context.errors.push('Broken @import declaration of "' + importedFile + '"');
- return processNext(context);
- }
-
- if (context.visited.indexOf(fullPath) > -1)
- return processNext(context);
-
-
- if (context.debug)
- console.error('Inlining local stylesheet: ' + fullPath);
-
- context.visited.push(fullPath);
-
- var importRelativeTo = path.dirname(fullPath);
- var importedData = fs.readFileSync(fullPath, 'utf8');
- if (context.rebase) {
- var rewriteOptions = {
- relative: true,
- fromBase: importRelativeTo,
- toBase: context.baseRelativeTo
- };
- importedData = rewriteUrls(importedData, rewriteOptions, context);
- }
-
- var relativePath = path.relative(context.root, fullPath);
- context.sourceReader.trackSource(relativePath, importedData);
- importedData = context.sourceTracker.store(relativePath, importedData);
-
- if (mediaQuery.length > 0)
- importedData = '@media ' + mediaQuery + '{' + importedData + '}';
-
- context.afterImport = true;
-
- var newContext = override(context, {
- relativeTo: importRelativeTo
- });
-
- return importFrom(importedData, newContext);
-}
-
-function restoreImport(importedUrl, mediaQuery, context) {
- var restoredImport = '@import url(' + importedUrl + ')' + (mediaQuery.length > 0 ? ' ' + mediaQuery : '') + ';';
- context.done.push(restoredImport);
-}
-
-module.exports = ImportInliner;
var WHOLE_PIXEL_VALUE = /(?:^|\s|\()(-?\d+)px/;
var TIME_VALUE = /^(\-?[\d\.]+)(m?s)$/;
+var IMPORT_PREFIX_PATTERN = /^@import/i;
var QUOTED_PATTERN = /^('.*'|".*")$/;
var QUOTED_BUT_SAFE_PATTERN = /^['"][a-zA-Z][a-zA-Z\d\-_]+['"]$/;
var URL_PREFIX_PATTERN = /^url\(/i;
return precision;
}
+function isImport(token) {
+ return IMPORT_PREFIX_PATTERN.test(token[1]);
+}
+
function basicOptimize(tokens, context) {
var options = context.options;
var ie7Hack = options.compatibility.selectors.ie7Hack;
var adjacentSpace = options.compatibility.selectors.adjacentSpace;
var spaceAfterClosingBrace = options.compatibility.properties.spaceAfterClosingBrace;
var mayHaveCharset = false;
+ var afterRules = false;
options.unitsRegexp = options.unitsRegexp || buildUnitRegexp(options);
options.precision = options.precision || buildPrecision(options);
switch (token[0]) {
case Token.AT_RULE:
- token[1] = tidyAtRule(token[1]);
+ token[1] = isImport(token) && afterRules ? '' : tidyAtRule(token[1]);
mayHaveCharset = true;
break;
case Token.AT_RULE_BLOCK:
optimizeBody(token[2], context);
+ afterRules = true;
break;
case Token.BLOCK:
token[1] = tidyBlock(token[1], spaceAfterClosingBrace);
basicOptimize(token[2], context);
+ afterRules = true;
break;
case Token.COMMENT:
optimizeComment(token, options);
case Token.RULE:
token[1] = tidyRules(token[1], !ie7Hack, adjacentSpace);
optimizeBody(token[2], context);
+ afterRules = true;
break;
}
function tidyAtRule(value) {
return value
.replace(/\s+/g, ' ')
+ .replace(/url\(\s+/g, 'url(')
+ .replace(/\s+\)/g, ')')
.trim();
}
return uri;
}
- if (isAbsolute(uri) || isSVGMarker(uri) || isInternal(uri)) {
+ if (isAbsolute(uri) && !isRemote(rebaseConfig.toBase)) {
return uri;
}
- if (isData(uri)) {
- return '\'' + uri + '\'';
- }
-
- if (isRemote(uri) && !isRemote(rebaseConfig.toBase)) {
+ if (isRemote(uri) || isSVGMarker(uri) || isInternal(uri)) {
return uri;
}
- if (isRemote(uri) && !isSameOrigin(uri, rebaseConfig.toBase)) {
- return uri;
+ if (isData(uri)) {
+ return '\'' + uri + '\'';
}
- if (!isRemote(uri) && isRemote(rebaseConfig.toBase)) {
+ if (isRemote(rebaseConfig.toBase)) {
return url.resolve(rebaseConfig.toBase, uri);
}
return /^[^:]+?:\/\//.test(uri) || uri.indexOf('//') === 0;
}
-function isSameOrigin(uri1, uri2) {
- return url.parse(uri1).protocol == url.parse(uri2).protocol &&
- url.parse(uri1).host == url.parse(uri2).host;
-}
-
function isData(uri) {
return uri.indexOf('data:') === 0;
}
+var fs = require('fs');
+var http = require('http');
+var https = require('https');
var path = require('path');
+var url = require('url');
var tokenize = require('../tokenizer/tokenize');
var Token = require('../tokenizer/token');
var rewriteUrl = require('../urls/rewrite-url');
-function readSources(input, context, callback) {
- var tokens;
+var override = require('../utils/override');
+var split = require('../utils/split');
+
+var IMPORT_PREFIX_PATTERN = /^@import/i;
+var BRACE_PREFIX = /^\(/;
+var BRACE_SUFFIX = /\)$/;
+var QUOTE_PREFIX_PATTERN = /['"]\s*/;
+var QUOTE_SUFFIX_PATTERN = /\s*['"]/;
+var URL_PREFIX_PATTERN = /^url\(\s*/i;
+var URL_SUFFIX_PATTERN = /\s*\)/i;
+var HTTP_PROTOCOL = 'http:';
+var HTTP_RESOURCE_PATTERN = /^http:\/\//;
+var HTTPS_RESOURCE_PATTERN = /^https:\/\//;
+var NO_PROTOCOL_RESOURCE_PATTERN = /^\/\//;
+var REMOTE_RESOURCE_PATTERN = /^(https?:)?\/\//;
+
+function readSources(input, context, callback) {
if (typeof input == 'string') {
- tokens = tokenize(input, context);
+ return fromString(input, context, {}, callback);
} else if (typeof input == 'object') {
- tokens = fromHash(input, context);
+ return fromHash(input, context, {}, callback);
}
+}
+
+function fromString(input, context, parentInlinerContext, callback) {
+ var tokens = tokenize(input, context);
- return callback(tokens);
+ return context.options.processImport ?
+ inlineImports(tokens, context, parentInlinerContext, callback) :
+ callback(tokens);
}
-function fromHash(input, context) {
+function fromHash(input, context, parentInlinerContext, callback) {
var tokens = [];
var sourcePath;
var source;
for (sourcePath in input) {
source = input[sourcePath];
- relativeTo = sourcePath[0] == '/' ?
- context.options.root :
- baseRelativeTo;
- fullPath = path.resolve(
- path.join(
- relativeTo,
- sourcePath
- )
- );
- config = {
- relative: true,
- fromBase: path.dirname(fullPath),
- toBase: baseRelativeTo
- };
+ if (isRemote(sourcePath)) {
+ config = {
+ relative: true,
+ fromBase: sourcePath,
+ toBase: sourcePath
+ };
+ } else {
+ relativeTo = sourcePath[0] == '/' ?
+ context.options.root :
+ baseRelativeTo;
+
+ fullPath = path.resolve(
+ path.join(
+ relativeTo,
+ sourcePath
+ )
+ );
+
+ config = {
+ relative: true,
+ fromBase: path.dirname(fullPath),
+ toBase: baseRelativeTo
+ };
+ }
tokens = tokens.concat(
rebase(
tokenize(source.styles, context),
+ context.options.rebase,
context.validator,
config
)
);
}
- return tokens;
+ return context.options.processImport ?
+ inlineImports(tokens, context, parentInlinerContext, callback) :
+ callback(tokens);
}
-function rebase(tokens, validator, config) {
+function rebase(tokens, rebaseAll, validator, config) {
+ return rebaseAll ?
+ rebaseEverything(tokens, validator, config) :
+ rebaseAtRules(tokens, validator, config);
+}
+
+function rebaseEverything(tokens, validator, config) {
var token;
var i, l;
switch (token[0]) {
case Token.AT_RULE:
- //
+ rebaseAtRule(token, validator, config);
break;
case Token.AT_RULE_BLOCK:
//
break;
case Token.BLOCK:
- rebase(token[2], validator, config);
+ rebaseEverything(token[2], validator, config);
break;
case Token.PROPERTY:
//
return tokens;
}
+function rebaseAtRules(tokens, validator, config) {
+ var token;
+ var i, l;
+
+ for (i = 0, l = tokens.length; i < l; i++) {
+ token = tokens[i];
+
+ switch (token[0]) {
+ case Token.AT_RULE:
+ rebaseAtRule(token, validator, config);
+ break;
+ }
+ }
+
+ return tokens;
+}
+
+function rebaseAtRule(token, validator, config) {
+ var uriAndMediaQuery = extractUrlAndMedia(token[1]);
+ var newUrl = rewriteUrl(uriAndMediaQuery[0], config);
+ var mediaQuery = uriAndMediaQuery[1];
+
+ token[1] = restoreImport(newUrl, mediaQuery);
+}
+
+function restoreImport(uri, mediaQuery) {
+ return ('@import ' + uri + ' ' + mediaQuery).trim();
+}
+
function rebaseProperties(properties, validator, config) {
var property;
var value;
}
}
+function inlineImports(tokens, externalContext, parentInlinerContext, callback) {
+ var inlinerContext = {
+ afterContent: false,
+ callback: callback,
+ externalContext: externalContext,
+ imported: parentInlinerContext.imported || [],
+ isRemote: parentInlinerContext.isRemote || false,
+ outputTokens: [],
+ sourceTokens: tokens
+ };
+
+ 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 && IMPORT_PREFIX_PATTERN.test(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 = extractUrlAndMedia(token[1]);
+ var uri = uriAndMediaQuery[0];
+ var mediaQuery = uriAndMediaQuery[1];
+
+ return isRemote(uri) ?
+ inlineRemoteStylesheet(uri, mediaQuery, inlinerContext) :
+ inlineLocalStylesheet(uri, mediaQuery, inlinerContext);
+}
+
+function extractUrlAndMedia(atRuleValue) {
+ var uri;
+ var mediaQuery;
+ var stripped;
+ var parts;
+
+ stripped = atRuleValue
+ .replace(IMPORT_PREFIX_PATTERN, '')
+ .trim()
+ .replace(URL_PREFIX_PATTERN, '(')
+ .replace(URL_SUFFIX_PATTERN, ')')
+ .replace(QUOTE_PREFIX_PATTERN, '')
+ .replace(QUOTE_SUFFIX_PATTERN, '');
+
+ parts = split(stripped, ' ');
+
+ uri = parts[0]
+ .replace(BRACE_PREFIX, '')
+ .replace(BRACE_SUFFIX, '');
+ mediaQuery = parts.slice(1).join(' ');
+
+ return [uri, mediaQuery];
+}
+
+function isRemote(uri) {
+ return REMOTE_RESOURCE_PATTERN.test(uri);
+}
+
+function allowedResource(uri, isRemote, rules) {
+ var match;
+ var allowed = true;
+ var rule;
+ var i;
+
+ if (rules.length === 0) {
+ return false;
+ }
+
+ if (isRemote && NO_PROTOCOL_RESOURCE_PATTERN.test(uri)) {
+ uri = 'http:' + uri;
+ }
+
+ match = isRemote ?
+ url.parse(uri).host :
+ uri;
+
+ for (i = 0; i < rules.length; i++) {
+ rule = rules[i];
+
+ if (rule == 'all') {
+ allowed = true;
+ } else if (isRemote && rule == 'local') {
+ allowed = false;
+ } else if (isRemote && rule == 'remote') {
+ allowed = true;
+ } else if (!isRemote && rule == 'remote') {
+ allowed = false;
+ } else if (!isRemote && rule == 'local') {
+ allowed = true;
+ } else if (rule[0] == '!' && rule.substring(1) === match) {
+ allowed = false;
+ }
+ }
+
+ return allowed;
+}
+
+function inlineRemoteStylesheet(uri, mediaQuery, inlinerContext) {
+ var inliner = inlinerContext.externalContext.options.inliner;
+ var errorHandled = false;
+ var fetch;
+ var isAllowed = allowedResource(uri, true, inlinerContext.externalContext.options.processImportFrom);
+ var onError;
+ var options;
+ var originalUri = uri;
+ var proxyProtocol = inliner.request.protocol || inliner.request.hostname;
+
+ if (inlinerContext.imported.indexOf(uri) > -1) {
+ inlinerContext.externalContext.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.externalContext.localOnly && inlinerContext.afterContent) {
+ inlinerContext.externalContext.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 (inlinerContext.externalContext.localOnly) {
+ inlinerContext.externalContext.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.externalContext.warnings.push('Ignoring remote @import of "' + uri + '" as resource not allowed and after other content.');
+ inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
+ return doInlineImports(inlinerContext);
+ } else if (!isAllowed) {
+ inlinerContext.externalContext.warnings.push('Skipping remote @import of "' + uri + '" as resource not allowed.');
+ inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1));
+ inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
+ return doInlineImports(inlinerContext);
+ }
+
+ if (NO_PROTOCOL_RESOURCE_PATTERN.test(uri)) {
+ uri = 'http:' + uri;
+ }
+
+ fetch = (proxyProtocol && !HTTPS_RESOURCE_PATTERN.test(proxyProtocol)) || HTTP_RESOURCE_PATTERN.test(uri) ?
+ http.get :
+ https.get;
+
+ options = override(url.parse(uri), inliner.request);
+ if (inliner.request.hostname !== undefined) {
+ // overwrite as we always expect a http proxy currently
+ options.protocol = inliner.request.protocol || HTTP_PROTOCOL;
+ options.path = options.href;
+ }
+
+ onError = function(message) {
+ if (errorHandled)
+ return;
+
+ errorHandled = true;
+ inlinerContext.externalContext.errors.push('Broken @import declaration of "' + uri + '" - ' + message);
+
+ process.nextTick(function () {
+ inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1));
+ inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
+ doInlineImports(inlinerContext);
+ });
+ };
+
+ inlinerContext.imported.push(uri);
+
+ fetch(options, function (res) {
+ var chunks = [];
+ var movedUri;
+
+ if (res.statusCode < 200 || res.statusCode > 399) {
+ return onError('error ' + res.statusCode);
+ } else if (res.statusCode > 299) {
+ movedUri = url.resolve(uri, res.headers.location);
+ return inlineRemoteStylesheet(movedUri, mediaQuery, inlinerContext);
+ }
+
+ res.on('data', function (chunk) {
+ chunks.push(chunk.toString());
+ });
+ res.on('end', function () {
+ var importedStyles;
+ var sourceHash = {};
+
+ importedStyles = chunks.join('');
+ sourceHash[originalUri] = {
+ styles: importedStyles
+ };
+
+ inlinerContext.isRemote = true;
+ fromHash(sourceHash, inlinerContext.externalContext, inlinerContext, function (importedTokens) {
+ importedTokens = wrapInMedia(importedTokens, mediaQuery);
+
+ inlinerContext.outputTokens = inlinerContext.outputTokens.concat(importedTokens);
+ inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
+
+ doInlineImports(inlinerContext);
+ });
+ });
+
+ })
+ .on('error', function (res) {
+ onError(res.message);
+ })
+ .on('timeout', function () {
+ onError('timeout');
+ })
+ .setTimeout(inliner.timeout);
+}
+
+function inlineLocalStylesheet(uri, mediaQuery, inlinerContext) {
+ var relativeTo = uri[0] == '/' ?
+ inlinerContext.externalContext.options.root :
+ inlinerContext.externalContext.options.relativeTo || inlinerContext.externalContext.options.root;
+ var absolutePath = path.join(relativeTo, uri);
+ var resolvedPath = path.resolve(absolutePath);
+ var importedStyles;
+ var importedTokens;
+ var isAllowed = allowedResource(uri, false, inlinerContext.externalContext.options.processImportFrom);
+ var sourceHash = {};
+
+ if (inlinerContext.imported.indexOf(resolvedPath) > -1) {
+ inlinerContext.externalContext.warnings.push('Ignoring local @import of "' + uri + '" as it has already beeb imported.');
+ } else if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isFile()) {
+ inlinerContext.externalContext.errors.push('Ignoring local @import of "' + uri + '" as resource is missing.');
+ } else if (!isAllowed && inlinerContext.afterContent) {
+ inlinerContext.externalContext.warnings.push('Ignoring local @import of "' + uri + '" as resource not allowed and after other content.');
+ } else if (inlinerContext.afterContent) {
+ inlinerContext.externalContext.warnings.push('Ignoring local @import of "' + uri + '" as after other content.');
+ } else if (!isAllowed) {
+ inlinerContext.externalContext.warnings.push('Skipping local @import of "' + uri + '" as resource not allowed.');
+ inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1));
+ } else {
+ importedStyles = fs.readFileSync(resolvedPath, 'utf-8');
+ inlinerContext.imported.push(resolvedPath);
+
+ sourceHash[uri] = {
+ styles: importedStyles
+ };
+ importedTokens = fromHash(sourceHash, inlinerContext.externalContext, inlinerContext, function (tokens) { return tokens; });
+ importedTokens = wrapInMedia(importedTokens, mediaQuery);
+
+ inlinerContext.outputTokens = inlinerContext.outputTokens.concat(importedTokens);
+ }
+
+ inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
+
+ return doInlineImports(inlinerContext);
+}
+
+function wrapInMedia(tokens, mediaQuery) {
+ if (mediaQuery) {
+ return [[Token.BLOCK, [['@media ' + mediaQuery]], tokens]];
+ } else {
+ return tokens;
+ }
+}
+
module.exports = readSources;
'@import url(test/fixtures/partials/comment.css);',
'a{display:block}'
],
- 'of a file (with media) with a comment': [
+ 'of a file with media having a comment': [
'@import url(test/fixtures/partials/comment.css) screen and (device-height: 600px);',
'@media screen and (device-height:600px){a{display:block}}'
],
nock.cleanAll();
}
},
+ 'of a remote resource after content and no callback': {
+ topic: function () {
+ var source = '.one{color:red}@import url(http://127.0.0.1/remote.css);';
+ this.reqMocks = nock('http://127.0.0.1')
+ .get('/remote.css')
+ .reply(200, 'div{padding:0}');
+
+ return new CleanCSS().minify(source);
+ },
+ 'should not raise errors': function (error, minified) {
+ assert.isEmpty(minified.errors);
+ },
+ 'should raise warnings': function (error, minified) {
+ assert.lengthOf(minified.warnings, 1);
+ assert.match(minified.warnings[0], /no callback given/);
+ },
+ 'should process @import': function (error, minified) {
+ assert.equal(minified.styles, '.one{color:red}');
+ },
+ teardown: function () {
+ assert.isFalse(this.reqMocks.isDone());
+ nock.cleanAll();
+ }
+ },
'of a remote resource mixed with local ones but no callback': {
topic: function () {
var source = '@import url(test/fixtures/partials/one.css);@import url(http://127.0.0.1/remote.css);';
'should not raise errors': function (error, minified) {
assert.isEmpty(minified.errors);
},
- 'should not raise warnings': function (error, minified) {
- assert.isEmpty(minified.warnings);
+ 'should raise warnings': function (error, minified) {
+ assert.lengthOf(minified.warnings, 1);
+ assert.equal(minified.warnings[0], 'Skipping remote @import of "http://127.0.0.1/skipped.css" as resource not allowed.');
},
'should keep imports': function (error, minified) {
assert.equal(minified.styles, '@import url(http://127.0.0.1/skipped.css);.one{color:red}');
'should not raise errors': function (error, minified) {
assert.isEmpty(minified.errors);
},
- 'should not raise warnings': function (error, minified) {
- assert.isEmpty(minified.warnings);
+ 'should raise warnings': function (error, minified) {
+ assert.lengthOf(minified.warnings, 2);
+ assert.equal(minified.warnings[0], 'Skipping remote @import of "http://127.0.0.1/remote.css" as resource not allowed.');
+ assert.equal(minified.warnings[1], 'Skipping remote @import of "http://assets.127.0.0.1/remote.css" as resource not allowed.');
},
'should keeps imports': function (error, minified) {
assert.equal(minified.styles, '@import url(http://127.0.0.1/remote.css);@import url(http://assets.127.0.0.1/remote.css);.one{color:red}');
assert.isEmpty(minified.errors);
},
'should raise a warning': function (error, minified) {
- assert.isEmpty(minified.warnings);
+ assert.lengthOf(minified.warnings, 3);
+ assert.equal(minified.warnings[0], 'Skipping remote @import of "http://127.0.0.1/remote.css" as resource not allowed.');
+ assert.equal(minified.warnings[1], 'Skipping remote @import of "http://assets.127.0.0.1/remote.css" as resource not allowed.');
+ assert.equal(minified.warnings[2], 'Skipping local @import of "test/fixtures/partials/one.css" as resource not allowed.');
},
'should process first imports': function (error, minified) {
assert.equal(minified.styles, '@import url(http://127.0.0.1/remote.css);@import url(http://assets.127.0.0.1/remote.css);@import url(test/fixtures/partials/one.css);');
assert.isEmpty(minified.errors);
},
'should raise a warning': function (error, minified) {
- assert.isEmpty(minified.warnings);
+ assert.lengthOf(minified.warnings, 1);
+ assert.equal(minified.warnings[0], 'Skipping remote @import of "//127.0.0.1/remote.css" as resource not allowed.');
},
'should process first imports': function (error, minified) {
assert.equal(minified.styles, '@import url(//127.0.0.1/remote.css);.one{color:red}');