value-processor.js 4.99 KB
/*
 * MIT License http://opensource.org/licenses/MIT
 * Author: Ben Holloway @bholloway
 */
'use strict';

var path        = require('path'),
    loaderUtils = require('loader-utils');

/**
 * Create a value processing function for a given file path.
 *
 * @param {function(Object):string} join The inner join function
 * @param {string} root The loader options.root value where given
 * @param {string} directory The directory of the file webpack is currently processing
 * @return {function} value processing function
 */
function valueProcessor({ join, root, directory }) {
  var URL_STATEMENT_REGEX = /(url\s*\(\s*)(?:(['"])((?:(?!\2).)*)(\2)|([^'"](?:(?!\)).)*[^'"]))(\s*\))/g,
      QUERY_REGEX         = /([?#])/g;

  /**
   * Process the given CSS declaration value.
   *
   * @param {string} value A declaration value that may or may not contain a url() statement
   * @param {function(number):Object} getPathsAtChar Given an offset in the declaration value get a
   *  list of possible absolute path strings
   */
  return function transformValue(value, getPathsAtChar) {

    // allow multiple url() values in the declaration
    //  split by url statements and process the content
    //  additional capture groups are needed to match quotations correctly
    //  escaped quotations are not considered
    return value
      .split(URL_STATEMENT_REGEX)
      .map(initialise)
      .map(eachSplitOrGroup)
      .join('');

    /**
     * Ensure all capture group tokens are a valid string.
     *
     * @param {string|void} token A capture group or uncaptured token
     * @returns {string}
     */
    function initialise(token) {
      return typeof token === 'string' ? token : '';
    }

    /**
     * An Array reduce function that accumulates string length.
     */
    function accumulateLength(accumulator, element) {
      return accumulator + element.length;
    }

    /**
     * Encode the content portion of <code>url()</code> statements.
     * There are 6 capture groups in the split making every 7th unmatched.
     *
     * @param {string} element A single split item
     * @param {number} i The index of the item in the split
     * @param {Array} arr The array of split values
     * @returns {string} Every 3 or 5 items is an encoded url everything else is as is
     */
    function eachSplitOrGroup(element, i, arr) {

      // the content of the url() statement is either in group 3 or group 5
      var mod = i % 7;

      // only one of the capture groups 3 or 5 will match the other will be falsey
      if (element && ((mod === 3) || (mod === 5))) {

        // calculate the offset of the match from the front of the string
        var position = arr.slice(0, i - mod + 1).reduce(accumulateLength, 0);

        // detect quoted url and unescape backslashes
        var before    = arr[i - 1],
            after     = arr[i + 1],
            isQuoted  = (before === after) && ((before === '\'') || (before === '"')),
            unescaped = isQuoted ? element.replace(/\\{2}/g, '\\') : element;

        // split into uri and query/hash and then determine if the uri is some type of file
        var split      = unescaped.split(QUERY_REGEX),
            uri        = split[0],
            query      = split.slice(1).join(''),
            isRelative = testIsRelative(uri),
            isAbsolute = testIsAbsolute(uri);

        // file like URIs are processed but not all URIs are files
        if (isRelative || isAbsolute) {
          var bases    = getPathsAtChar(position), // construct iterator as late as possible in case sourcemap invalid
              absolute = join({ uri, query, isAbsolute, bases });

          if (typeof absolute === 'string') {
            var relative = path.relative(directory, absolute)
              .replace(/\\/g, '/'); // #6 - backslashes are not legal in URI

            return loaderUtils.urlToRequest(relative + query);
          }
        }
      }

      // everything else, including parentheses and quotation (where present) and media statements
      return element;
    }
  };

  /**
   * The loaderUtils.isUrlRequest() doesn't support windows absolute paths on principle. We do not subscribe to that
   * dogma so we add path.isAbsolute() check to allow them.
   *
   * We also eliminate module relative (~) paths.
   *
   * @param {string|undefined} uri A uri string possibly empty or undefined
   * @return {boolean} True for relative uri
   */
  function testIsRelative(uri) {
    return !!uri && loaderUtils.isUrlRequest(uri, false) && !path.isAbsolute(uri) && (uri.indexOf('~') !== 0);
  }

  /**
   * The loaderUtils.isUrlRequest() doesn't support windows absolute paths on principle. We do not subscribe to that
   * dogma so we add path.isAbsolute() check to allow them.
   *
   * @param {string|undefined} uri A uri string possibly empty or undefined
   * @return {boolean} True for absolute uri
   */
  function testIsAbsolute(uri) {
    return !!uri && (typeof root === 'string') && loaderUtils.isUrlRequest(uri, root) &&
      (/^\//.test(uri) || path.isAbsolute(uri));
  }
}

module.exports = valueProcessor;