123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- var fs = require('fs');
- var path = require('path');
- var http = require('http');
- var https = require('https');
- var url = require('url');
-
- var UrlRewriter = require('../images/url-rewriter');
- var Splitter = require('../text/splitter.js');
-
- 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;
- };
-
- module.exports = function Inliner(context, options) {
- var defaultOptions = {
- timeout: 5000,
- request: {}
- };
- var inlinerOptions = merge(defaultOptions, options || {});
-
- var process = function(data, options) {
- if (options.shallow) {
- options.shallow = false;
- options._shared.done.push(data);
- return processNext(options);
- }
-
- 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.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);
- }
-
- // 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);
-
- if (nextLowerCase > -1 && nextUpperCase == -1)
- return nextLowerCase;
- else if (nextLowerCase == -1 && nextUpperCase > -1)
- return nextUpperCase;
- else
- return Math.min(nextLowerCase, nextUpperCase);
- };
-
- 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;
-
- // 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;
- }
-
- // 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;
-
- // re-run scan if comment ended before the idx
- if (globalEndIndex < idx)
- return scanner(idx);
-
- return globalEndIndex > idx && idx > globalStartIndex;
- };
-
- return scanner;
- };
-
- 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;
- };
- };
-
- var inline = function(data, nextStart, nextEnd, options) {
- options.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) :
- new Splitter(' ').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 = options.isRemote ||
- /^(http|https):\/\//.test(importedFile) ||
- /^\/\//.test(importedFile);
-
- if (options.localOnly && isRemote) {
- context.warnings.push('Ignoring remote @import declaration of "' + importedFile + '" as no callback given.');
- restoreImport(importedFile, mediaQuery, options);
-
- return processNext(options);
- }
-
- var method = isRemote ? inlineRemoteResource : inlineLocalResource;
- return method(importedFile, mediaQuery, options);
- };
-
- var inlineRemoteResource = function(importedFile, mediaQuery, options) {
- var importedUrl = /^https?:\/\//.test(importedFile) ?
- importedFile :
- url.resolve(options.relativeTo, importedFile);
-
- if (importedUrl.indexOf('//') === 0)
- importedUrl = 'http:' + importedUrl;
-
- if (options.visited.indexOf(importedUrl) > -1)
- return processNext(options);
-
-
- if (context.debug)
- console.error('Inlining remote stylesheet: ' + importedUrl);
-
- options.visited.push(importedUrl);
-
- var get = importedUrl.indexOf('http://') === 0 ?
- http.get :
- https.get;
-
- var timedOut = false;
- var handleError = function(message) {
- context.errors.push('Broken @import declaration of "' + importedUrl + '" - ' + message);
- restoreImport(importedUrl, mediaQuery, options);
-
- processNext(options);
- };
- var requestOptions = merge(url.parse(importedUrl), inlinerOptions.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, options);
- }
-
- var chunks = [];
- var parsedUrl = url.parse(importedUrl);
- res.on('data', function(chunk) {
- chunks.push(chunk.toString());
- });
- res.on('end', function() {
- var importedData = chunks.join('');
- importedData = UrlRewriter.process(importedData, { toBase: 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
- });
- });
- })
- .on('error', function(res) {
- handleError(res.message);
- })
- .on('timeout', function() {
- // FIX: node 0.8 fires this event twice
- if (timedOut)
- return;
-
- handleError('timeout');
- timedOut = true;
- })
- .setTimeout(inlinerOptions.timeout);
- };
-
- var inlineLocalResource = function(importedFile, mediaQuery, options) {
- var relativeTo = importedFile[0] == '/' ?
- options.root :
- options.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(options);
- }
-
- if (options.visited.indexOf(fullPath) > -1)
- return processNext(options);
-
-
- if (context.debug)
- console.error('Inlining local stylesheet: ' + fullPath);
-
- options.visited.push(fullPath);
-
- var importedData = fs.readFileSync(fullPath, 'utf8');
- var importRelativeTo = path.dirname(fullPath);
- importedData = UrlRewriter.process(importedData, {
- relative: true,
- fromBase: importRelativeTo,
- toBase: options._baseRelativeTo
- });
-
- 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
- });
- };
-
- var restoreImport = function(importedUrl, mediaQuery, options) {
- var restoredImport = '@import url(' + importedUrl + ')' + (mediaQuery.length > 0 ? ' ' + mediaQuery : '') + ';';
- options._shared.done.push(restoredImport);
- };
-
- // Inlines all imports taking care of repetitions, unknown files, and circular dependencies
- return { process: process };
- };
|