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