var UrlRewriter = require('../images/url-rewriter');
var Splitter = require('../utils/splitter.js');
+var override = require('../utils/object.js').override;
var MAP_MARKER = /\/\*# sourceMappingURL=(\S+) \*\//;
var REMOTE_RESOURCE = /^(https?:)?\/\//;
-var merge = function(source1, source2) {
- var target = {};
- for (var key1 in source1)
- target[key1] = source1[key1];
- for (var key2 in source2)
- target[key2] = source2[key2];
-
- return target;
-};
+function ImportInliner (context) {
+ this.outerContext = 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));
+ImportInliner.prototype.process = function (data, context) {
+ var root = this.outerContext.options.root || process.cwd();
+
+ 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,
+ sourceTracker: this.outerContext.sourceTracker,
+ warnings: this.outerContext.warnings,
+ visited: []
});
-}
-module.exports = function Inliner(context, options, rebase) {
- var defaultOptions = {
- timeout: 5000,
- request: {}
- };
- var inlinerOptions = merge(defaultOptions, options || {});
+ return importFrom(data, context);
+};
- var process = function(data, options) {
- if (options.shallow) {
- options.shallow = false;
- options._shared.done.push(data);
- return processNext(options);
+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);
+ var afterContent = contentScanner(data);
+
+ for (; nextEnd < data.length;) {
+ nextStart = nextImportAt(data, cursor);
+ if (nextStart == -1)
+ break;
+
+ if (isComment(nextStart)) {
+ cursor = nextStart + 1;
+ continue;
}
- options._shared = options._shared || {
- done: [],
- left: []
- };
- var shared = options._shared;
-
- var nextStart = 0;
- var nextEnd = 0;
- var cursor = 0;
- var isComment = commentScanner(data);
- var afterContent = contentScanner(data);
-
- options.rebase = rebase;
- options.relativeTo = options.relativeTo || options.root;
- options._baseRelativeTo = options._baseRelativeTo || options.relativeTo;
- options.visited = options.visited || [];
-
- 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;
- }
-
- shared.done.push(data.substring(0, nextStart));
- shared.left.unshift([data.substring(nextEnd + 1), options]);
-
- return afterContent(nextStart) ?
- processNext(options) :
- inline(data, nextStart, nextEnd, options);
+ nextEnd = data.indexOf(';', nextStart);
+ if (nextEnd == -1) {
+ cursor = data.length;
+ data = '';
+ break;
}
- // no @import matched in current data
- shared.done.push(data);
- return processNext(options);
- };
-
- var nextImportAt = function (data, cursor) {
- var nextLowerCase = data.indexOf('@import', cursor);
- var nextUpperCase = data.indexOf('@IMPORT', cursor);
+ context.done.push(data.substring(0, nextStart));
+ context.left.unshift([data.substring(nextEnd + 1), context]);
- if (nextLowerCase > -1 && nextUpperCase == -1)
- return nextLowerCase;
- else if (nextLowerCase == -1 && nextUpperCase > -1)
- return nextUpperCase;
- else
- return Math.min(nextLowerCase, nextUpperCase);
- };
+ return afterContent(nextStart) ?
+ processNext(context) :
+ inline(data, nextStart, nextEnd, context);
+ }
- var processNext = function(options) {
- if (options._shared.left.length > 0)
- return process.apply(null, options._shared.left.shift());
- else
- return options.whenDone(options._shared.done.join(''));
- };
-
- var commentScanner = function(data) {
- var commentRegex = /(\/\*(?!\*\/)[\s\S]*?\*\/)/;
- var lastStartIndex = 0;
- var lastEndIndex = 0;
- var noComments = false;
-
- // test whether an index is located within a comment
- var scanner = function(idx) {
- var comment;
- var localStartIndex = 0;
- var localEndIndex = 0;
- var globalStartIndex = 0;
- var globalEndIndex = 0;
+ // no @import matched in current data
+ context.done.push(data);
+ return processNext(context);
+}
- // return if we know there are no more comments
- if (noComments)
- return false;
+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));
+ });
+}
- // idx can be still within last matched comment (many @import statements inside one comment)
- if (idx > lastStartIndex && idx < lastEndIndex)
- return true;
+function nextImportAt(data, cursor) {
+ var nextLowerCase = data.indexOf('@import', cursor);
+ var nextUpperCase = data.indexOf('@IMPORT', cursor);
- comment = data.match(commentRegex);
+ if (nextLowerCase > -1 && nextUpperCase == -1)
+ return nextLowerCase;
+ else if (nextLowerCase == -1 && nextUpperCase > -1)
+ return nextUpperCase;
+ else
+ return Math.min(nextLowerCase, nextUpperCase);
+}
- if (!comment) {
- noComments = true;
- return false;
- }
+function processNext(context) {
+ return context.left.length > 0 ?
+ importFrom.apply(null, context.left.shift()) :
+ context.whenDone(context.done.join(''));
+}
- // get the indexes relative to the current data chunk
- lastStartIndex = localStartIndex = comment.index;
- localEndIndex = localStartIndex + comment[0].length;
+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;
+
+ // 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;
+ }
- // calculate the indexes relative to the full original data
- globalEndIndex = localEndIndex + lastEndIndex;
- globalStartIndex = globalEndIndex - comment[0].length;
+ // get the indexes relative to the current data chunk
+ lastStartIndex = localStartIndex = comment.index;
+ localEndIndex = localStartIndex + comment[0].length;
- // chop off data up to and including current comment block
- data = data.substring(localEndIndex);
- lastEndIndex = globalEndIndex;
+ // calculate the indexes relative to the full original data
+ globalEndIndex = localEndIndex + lastEndIndex;
+ globalStartIndex = globalEndIndex - comment[0].length;
- // re-run scan if comment ended before the idx
- if (globalEndIndex < idx)
- return scanner(idx);
+ // chop off data up to and including current comment block
+ data = data.substring(localEndIndex);
+ lastEndIndex = globalEndIndex;
- return globalEndIndex > idx && idx > globalStartIndex;
- };
+ // re-run scan if comment ended before the idx
+ if (globalEndIndex < idx)
+ return scanner(idx);
- return scanner;
+ return globalEndIndex > idx && idx > globalStartIndex;
};
+}
- var contentScanner = function(data) {
- var isComment = commentScanner(data);
- var firstContentIdx = -1;
- while (true) {
- firstContentIdx = data.indexOf('{', firstContentIdx + 1);
- if (firstContentIdx == -1 || !isComment(firstContentIdx))
- break;
- }
-
- return function(idx) {
- return firstContentIdx > -1 ?
- idx > firstContentIdx :
- false;
- };
+function contentScanner(data) {
+ var isComment = commentScanner(data);
+ var firstContentIdx = -1;
+ while (true) {
+ firstContentIdx = data.indexOf('{', firstContentIdx + 1);
+ if (firstContentIdx == -1 || !isComment(firstContentIdx))
+ break;
+ }
+
+ return function(idx) {
+ return firstContentIdx > -1 ?
+ idx > firstContentIdx :
+ false;
};
+}
- var inline = function(data, nextStart, nextEnd, options) {
- options.shallow = data.indexOf('@shallow') > 0;
+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 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) :
- new Splitter(' ').split(importDeclaration)[0].length - (viaUrl ? 1 : 0);
+ 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) :
+ new Splitter(' ').split(importDeclaration)[0].length - (viaUrl ? 1 : 0);
- var importedFile = importDeclaration
- .substring(urlStartsAt, urlEndsAt)
- .replace(/['"]/g, '')
- .replace(/\)$/, '')
- .trim();
+ var importedFile = importDeclaration
+ .substring(urlStartsAt, urlEndsAt)
+ .replace(/['"]/g, '')
+ .replace(/\)$/, '')
+ .trim();
- var mediaQuery = importDeclaration
- .substring(urlEndsAt + 1)
- .replace(/^\)/, '')
- .trim();
+ var mediaQuery = importDeclaration
+ .substring(urlEndsAt + 1)
+ .replace(/^\)/, '')
+ .trim();
- var isRemote = options.isRemote || REMOTE_RESOURCE.test(importedFile);
+ var isRemote = context.isRemote || REMOTE_RESOURCE.test(importedFile);
- if (options.localOnly && isRemote) {
- context.warnings.push('Ignoring remote @import declaration of "' + importedFile + '" as no callback given.');
- restoreImport(importedFile, mediaQuery, options);
+ if (context.localOnly && isRemote) {
+ context.warnings.push('Ignoring remote @import declaration of "' + importedFile + '" as no callback given.');
+ restoreImport(importedFile, mediaQuery, context);
- return processNext(options);
- }
+ return processNext(context);
+ }
- var method = isRemote ? inlineRemoteResource : inlineLocalResource;
- return method(importedFile, mediaQuery, options);
- };
+ var method = isRemote ? inlineRemoteResource : inlineLocalResource;
+ return method(importedFile, mediaQuery, context);
+}
- var inlineRemoteResource = function(importedFile, mediaQuery, options) {
- var importedUrl = REMOTE_RESOURCE.test(importedFile) ?
- importedFile :
- url.resolve(options.relativeTo, importedFile);
+function inlineRemoteResource(importedFile, mediaQuery, context) {
+ var importedUrl = REMOTE_RESOURCE.test(importedFile) ?
+ importedFile :
+ url.resolve(context.relativeTo, importedFile);
- if (importedUrl.indexOf('//') === 0)
- importedUrl = 'http:' + importedUrl;
+ if (importedUrl.indexOf('//') === 0)
+ importedUrl = 'http:' + importedUrl;
- if (options.visited.indexOf(importedUrl) > -1)
- return processNext(options);
+ if (context.visited.indexOf(importedUrl) > -1)
+ return processNext(context);
- if (context.debug)
- console.error('Inlining remote stylesheet: ' + importedUrl);
+ if (context.debug)
+ console.error('Inlining remote stylesheet: ' + importedUrl);
- options.visited.push(importedUrl);
+ context.visited.push(importedUrl);
- var get = importedUrl.indexOf('http://') === 0 ?
- http.get :
- https.get;
+ var get = importedUrl.indexOf('http://') === 0 ?
+ http.get :
+ https.get;
- var handleError = function(message) {
- context.errors.push('Broken @import declaration of "' + importedUrl + '" - ' + message);
- restoreImport(importedUrl, mediaQuery, options);
+ function handleError(message) {
+ context.errors.push('Broken @import declaration of "' + importedUrl + '" - ' + message);
+ restoreImport(importedUrl, mediaQuery, context);
- processNext(options);
- };
- var requestOptions = merge(url.parse(importedUrl), inlinerOptions.request);
+ processNext(context);
+ }
- 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, options);
- }
+ var requestOptions = override(url.parse(importedUrl), context.inliner.request);
+ 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 (options.rebase)
- importedData = UrlRewriter.process(importedData, { toBase: importedUrl });
- importedData = context.sourceTracker.store(importedUrl, importedData);
- importedData = rebaseMap(importedData, importedUrl);
-
- if (mediaQuery.length > 0)
- importedData = '@media ' + mediaQuery + '{' + importedData + '}';
-
- process(importedData, {
- isRemote: true,
- relativeTo: parsedUrl.protocol + '//' + parsedUrl.host,
- _shared: options._shared,
- whenDone: options.whenDone,
- visited: options.visited,
- shallow: options.shallow
- });
+ 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 = UrlRewriter.process(importedData, { toBase: importedUrl });
+ importedData = context.sourceTracker.store(importedUrl, importedData);
+ importedData = rebaseMap(importedData, importedUrl);
+
+ if (mediaQuery.length > 0)
+ importedData = '@media ' + mediaQuery + '{' + importedData + '}';
+
+ var newContext = override(context, {
+ isRemote: true,
+ relativeTo: parsedUrl.protocol + '//' + parsedUrl.host
});
- })
- .on('error', function(res) {
- handleError(res.message);
- })
- .on('timeout', function() {
- handleError('timeout');
- })
- .setTimeout(inlinerOptions.timeout);
- };
- var inlineLocalResource = function(importedFile, mediaQuery, options) {
- var relativeTo = importedFile[0] == '/' ?
- options.root :
- options.relativeTo;
+ importFrom(importedData, newContext);
+ });
+ })
+ .on('error', function(res) {
+ handleError(res.message);
+ })
+ .on('timeout', function() {
+ handleError('timeout');
+ })
+ .setTimeout(context.inliner.timeout);
+}
- var fullPath = path.resolve(path.join(relativeTo, importedFile));
+function inlineLocalResource(importedFile, mediaQuery, context) {
+ var relativeTo = importedFile[0] == '/' ?
+ context.root :
+ context.relativeTo;
- if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) {
- context.errors.push('Broken @import declaration of "' + importedFile + '"');
- return processNext(options);
- }
+ var fullPath = path.resolve(path.join(relativeTo, importedFile));
- if (options.visited.indexOf(fullPath) > -1)
- return processNext(options);
+ 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);
- options.visited.push(fullPath);
+ if (context.debug)
+ console.error('Inlining local stylesheet: ' + fullPath);
- var importRelativeTo = path.dirname(fullPath);
- var importedData = fs.readFileSync(fullPath, 'utf8');
- if (options.rebase) {
- importedData = UrlRewriter.process(importedData, {
- relative: true,
- fromBase: importRelativeTo,
- toBase: options._baseRelativeTo
- });
- }
- importedData = context.sourceTracker.store(path.resolve(options.relativeTo, fullPath), importedData);
-
- if (mediaQuery.length > 0)
- importedData = '@media ' + mediaQuery + '{' + importedData + '}';
-
- return process(importedData, {
- root: options.root,
- relativeTo: importRelativeTo,
- _baseRelativeTo: options._baseRelativeTo,
- _shared: options._shared,
- visited: options.visited,
- whenDone: options.whenDone,
- localOnly: options.localOnly,
- shallow: options.shallow
+ context.visited.push(fullPath);
+
+ var importRelativeTo = path.dirname(fullPath);
+ var importedData = fs.readFileSync(fullPath, 'utf8');
+ if (context.rebase) {
+ importedData = UrlRewriter.process(importedData, {
+ relative: true,
+ fromBase: importRelativeTo,
+ toBase: context.baseRelativeTo
});
- };
+ }
+ importedData = context.sourceTracker.store(path.resolve(context.relativeTo, fullPath), importedData);
- var restoreImport = function(importedUrl, mediaQuery, options) {
- var restoredImport = '@import url(' + importedUrl + ')' + (mediaQuery.length > 0 ? ' ' + mediaQuery : '') + ';';
- options._shared.done.push(restoredImport);
- };
+ if (mediaQuery.length > 0)
+ importedData = '@media ' + mediaQuery + '{' + importedData + '}';
- // Inlines all imports taking care of repetitions, unknown files, and circular dependencies
- return { process: process };
-};
+ 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;