clear
This commit is contained in:
399
node_modules/clean-css/lib/imports/inliner.js
generated
vendored
Normal file
399
node_modules/clean-css/lib/imports/inliner.js
generated
vendored
Normal file
@@ -0,0 +1,399 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user