Source: url-builder.js

'use strict';

// External protocols regex, supports:
// "http", "https", "//" and "www."
var REGEX_EXTERNAL_PROTOCOLS = /^https?:\/\/|\/\/|www\./;


/**
 * Creates an instance of URLBuilder class.
 *
 * @constructor
 * @param {object} - instance of {@link ConfigParser} object.
 */
function URLBuilder(configParser) {
    this._configParser = configParser;
}

URLBuilder.prototype = {
    constructor: URLBuilder,

    /**
     * Returns a list of URLs from provided list of modules.
     *
     * @param {array} modules List of modules for which URLs should be created.
     * @return {array} List of URLs.
     */
    build: function (modules) {
        var bufferAbsoluteURL = [];
        var bufferRelativeURL = [];
        var modulesAbsoluteURL = [];
        var modulesRelativeURL = [];
        var result = [];

        var config = this._configParser.getConfig();

        var basePath = config.basePath || '';
        var registeredModules = this._configParser.getModules();

        /* istanbul ignore else */
        if (basePath.length && basePath.charAt(basePath.length - 1) !== '/') {
            basePath += '/';
        }

        for (var i = 0; i < modules.length; i++) {
            var module = registeredModules[modules[i]];

            // If module has fullPath, individual URL have to be created.
            if (module.fullPath) {
                result.push({
                    modules: [module.name],
                    url: module.fullPath
                });

            } else {
                var path = this._getModulePath(module);
                var absolutePath = path.indexOf('/') === 0;

                // If the URL starts with external protocol, individual URL shall be created.
                if (REGEX_EXTERNAL_PROTOCOLS.test(path)) {
                    result.push({
                        modules: [module.name],
                        url: path
                    });

                // If combine is disabled, or the module is an anonymous one,
                // create an individual URL based on the config URL and module's path.
                // If the module's path starts with "/", do not include basePath in the URL.
                } else if (!config.combine || module.anonymous) {
                    result.push({
                        modules: [module.name],
                        url: config.url + (absolutePath ? '' : basePath) + path
                    });

                } else {
                    // If combine is true, this is not an anonymous module and the module does not have full path.
                    // The module will be collected in a buffer to be loaded among with other modules from combo loader.
                    // The path will be stored in different buffer depending on the fact if it is absolute URL or not.
                    if (absolutePath) {
                        bufferAbsoluteURL.push(path);
                        modulesAbsoluteURL.push(module.name);
                    } else {
                        bufferRelativeURL.push(path);
                        modulesRelativeURL.push(module.name);
                    }
                }
            }

            module.requested = true;
        }

        // Add to the result all modules, which have to be combined.
        if (bufferRelativeURL.length) {
            result = result.concat(
                this._generateBufferURLs(
                    modulesRelativeURL,
                    bufferRelativeURL,
                    {
                        basePath: basePath,
                        url: config.url,
                        urlMaxLength: config.urlMaxLength
                    }
                )
            );
            bufferRelativeURL.length = 0;
        }

        if (bufferAbsoluteURL.length) {
            result = result.concat(
                this._generateBufferURLs(
                    modulesAbsoluteURL,
                    bufferAbsoluteURL,
                    {
                        url: config.url,
                        urlMaxLength: config.urlMaxLength
                    }
                )
            );
            bufferAbsoluteURL.length = 0;
        }

        return result;
    },

    /**
     * Generate the appropriate set of URLs based on the list of
     * required modules and the maximum allowed URL length
     *
     * @param {Array<String>} modules Array of module names
     * @param {Array<String>} urls Array of module URLs
     * @param {Object} config Configuration object containing URL, basePath and urlMaxLength
     * @return {Array<Object>} Resulting array of {modules, url} objects
     */
    _generateBufferURLs: function(modules, urls, config) {
        var i;
        var basePath = config.basePath || '';
        var result = [];
        var urlMaxLength = config.urlMaxLength || 2000;

        var urlResult = {
            modules: [modules[0]],
            url: config.url + basePath + urls[0]
        };

        for (i = 1; i < urls.length; i++) {
            var module = modules[i];
            var path = urls[i];

            if ((urlResult.url.length + basePath.length + path.length + 1) < urlMaxLength) {
                urlResult.modules.push(module);
                urlResult.url += '&' + basePath + path;
            } else {
                result.push(urlResult);

                urlResult = {
                    modules: [module],
                    url: config.url + basePath + path
                };
            }
        }

        result.push(urlResult);

        return result;
    },

    /**
     * Returns the path for a module. If module has property path, it will be returned directly. Otherwise,
     * the name of module will be used and extension .js will be added to module name if omitted.
     *
     * @protected
     * @param {object} module The module which path should be returned.
     * @return {string} Module path.
     */
    _getModulePath: function (module) {
        var path = module.path || module.name;

        var paths = this._configParser.getConfig().paths || {};

        var found = false;
        Object.keys(paths).forEach(function(item) {
            /* istanbul ignore else */
            if (path === item || path.indexOf(item + '/') === 0) {
                path = paths[item] + path.substring(item.length);
            }
        });

        /* istanbul ignore else */
        if(!found && typeof paths['*'] === 'function') {
            path = paths['*'](path);
        }

        if (!REGEX_EXTERNAL_PROTOCOLS.test(path) && path.indexOf('.js') !== path.length - 3) {
            path += '.js';
        }

        return path;
    }
};