| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- 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;
|