Source: lib/back-end-part.js

/*------------------------------------*\
    $%BACK-END PART
\*------------------------------------*/
/* jslint node: true */

/**
 * Check if a file have been already parsed.
 * @private
 * @function alreadyParse
 * @memberOf NA#
 * @param {string}         newPath     Current file tested.
 * @param {Array.<string>} allCssFiles Files already tested.
 * @param {string}         inject      State for know if injection will be authorized.
 */
exports.cssAlreadyParse = function (newPath, allCssFiles, inject) {
    var NA = this,
        path = NA.modules.path;

    for (var i = 0; i < allCssFiles.length; i++) {
        if (path.join(NA.websitePhysicalPath, NA.webconfig.assetsRelativePath, newPath) === allCssFiles[i]) {
            inject = false;
        }
    }

    return inject;
};

/**
 * Inject Css if not already injected.
 * @private
 * @function injectCssAuth
 * @memberOf NA#
 * @param {string}         pathFile    Current file for injection.
 * @param {Array.<string>} allCssFiles Files already tested.
 * @param {string}         inject      State for know if injection will be authorized.
 */
exports.injectCssAuth = function (pathFile, allCssFiles, inject) {
    var NA = this,
        path = NA.modules.path;

    if (inject) {
        allCssFiles.push(path.join(NA.websitePhysicalPath, NA.webconfig.assetsRelativePath, pathFile));
    }
};

/**
 * Verify if common or specif file without double are ready for injection CSS.
 * @private
 * @function prepareCssInjection
 * @memberOf NA#
 * @param {Array.<string>} allCssFiles      Files already tested.
 * @param {string|Array.<string>} injection Represent the injectCss property injection to the template.
 */
exports.prepareCssInjection = function (allCssFiles, injection) {
    var NA = this,
        path = NA.modules.path,
        inject = true,

        /**
         * CSS files for specific injection of CSS.
         * @public
         * @alias injectCss
         * @type {string|Array.<string>}
         * @memberOf NA#currentRouteParameters
         */
        specificInjection = injection,

        /**
         * CSS files for common injection of CSS.
         * @public
         * @alias injectCss
         * @type {string|Array.<string>}
         * @memberOf NA#webconfig
         */
        commonInjection = NA.webconfig.injectCss;

    /* Add common injections. */
    if (typeof commonInjection === 'string') {
        allCssFiles.push(path.join(NA.websitePhysicalPath, NA.webconfig.assetsRelativePath, commonInjection));
    } else if (commonInjection) {
        for (var i = 0; i < commonInjection.length; i++) {
            /* Inject Css. */
            NA.injectCssAuth(NA.webconfig.injectCss[i], allCssFiles, inject);
        }
    }

    /* Add specific injections. */
    if (specificInjection) {
        if (typeof specificInjection === 'string') {
            /* Check if a file have been already parsed. */
            inject = NA.cssAlreadyParse(specificInjection, allCssFiles, inject);
            /* Inject Css. */
            NA.injectCssAuth(specificInjection, allCssFiles, inject);
        } else {
            for (var j = 0; j < specificInjection.length; j++) {
                /* Check if a file have been already parsed. */
                inject = NA.cssAlreadyParse(specificInjection[j], allCssFiles, inject);
                /* Inject Css. */
                NA.injectCssAuth(specificInjection[j], allCssFiles, inject);
                inject = true;
            }
        }
    }

    return allCssFiles;
};

/**
 * Inject the content of a stylesheets file into a DOM.
 * @private
 * @function injectCss
 * @memberOf NA#
 * @param {string}                 dom          The ouptput HTML.
 * @param {string|Array.<string>}  injection    Represent the injectCss property injection to the template.
 * @param {injectCss~mainCallback} mainCallback The next steps after injection.
 */
exports.injectCss = function (dom, injection, mainCallback) {
    var NA = this,
        cheerio = NA.modules.cheerio,
        cssParse = NA.modules.cssParse,
        async = NA.modules.async,
        fs = NA.modules.fs,
        allCssFiles = [],
        $ = cheerio.load(dom, { decodeEntities: false });

    /* Prepare Injection */
    allCssFiles = NA.prepareCssInjection(allCssFiles, injection);

    /* Injection */
    async.map(allCssFiles, function (sourceFile, callback) {
        /* Concatain all CSS. */
        callback(null, fs.readFileSync(sourceFile, 'utf-8'));
    }, function(error, results) {
        var output = "",
            css;

        for (var i = 0; i < results.length; i++) {
            output += results[i];
        }

        /* Parse CSS in JavaScript. */
        css = cssParse(output);

        /* Parse all rules Css. */
        function parseCssRules(callback) {
            for (var i = 0; i < css.stylesheet.rules.length; i++) {
                if (typeof css.stylesheet.rules[i].selectors !== 'undefined') {
                    callback(i);
                }
            }
        }

        /* Parse all selector Css. */
        function parseCssSelector(i, callback) {
            for (var j = 0; j < css.stylesheet.rules[i].selectors.length; j++) {
                callback(j);
            }
        }

        /* Parse all declaration Css. */
        function parseCssDeclaration(i, j) {
            for (var k = 0; k < css.stylesheet.rules[i].declarations.length; k++) {
                $(css.stylesheet.rules[i].selectors[j]).css(css.stylesheet.rules[i].declarations[k].property, css.stylesheet.rules[i].declarations[k].value);
            }
        }

        /* Apply property on the DOM. */
        parseCssRules(function (i) {
            parseCssSelector(i, function (j) {
                parseCssDeclaration(i, j);
            });
        });

        /**
         * Next steps after injection of CSS.
         * @callback injectCss~mainCallback
         * @param {string} dom DOM with modifications.
         */
        mainCallback($.html());
    });
};

/**
 * Engine for compile Preprocessor language without call any CSS files.
 * @private
 * @function cssCompilation
 * @memberOf NA#
 * @callback cssCompilation~callback callback Next step after Preprocessor compilation.
 */
exports.cssCompilation = function (callback) {
    var NA = this,
        async = NA.modules.async;

    if (NA.configuration.generate || NA.webconfig.stylesheetsBundlesBeforeResponse) {
        async.parallel([
            NA.lessCompilation.bind(NA),
            NA.stylusCompilation.bind(NA)
        ], function () {
            NA.cssMinification(callback);
        });
    } else {
        callback();
    }
};

/**
 * Engine for compile Less file without call any CSS files.
 * @private
 * @function lessCompilation
 * @memberOf NA#
 * @callback lessCompilation~callback callback Next step after Less compilation.
 */
exports.lessCompilation = function (callback) {
    var NA = this,
        enableLess = NA.webconfig.enableLess,
        async = NA.modules.async,
        path = NA.modules.path,
        less = NA.modules.less,
        fs = NA.modules.fs,
        allLessCompiled,
        data = {},
        paths = [];

    if (enableLess && enableLess.less) {

        allLessCompiled = enableLess.less;

        if (enableLess.paths && paths.length === 0) {
            for (var i = 0; i < enableLess.paths.length; i++) {
                paths[i] = path.join(NA.webconfig.assetsRelativePath, enableLess.paths[i]);
            }
        } else if (paths.length === 0) {
            paths = [
                NA.webconfig.assetsRelativePath,
                path.join(NA.webconfig.assetsRelativePath, 'stylesheets'),
                path.join(NA.webconfig.assetsRelativePath, 'styles'),
                path.join(NA.webconfig.assetsRelativePath, 'css')
            ];
        }

        async.each(allLessCompiled, function (compiledFile, next) {
            var currentFile = fs.readFileSync(path.join(NA.websitePhysicalPath, NA.webconfig.assetsRelativePath, compiledFile), 'utf-8');

            less.render(currentFile, {
                paths: paths
            }, function (e, output) {
                if (e) {
                    NA.log(e);
                }

                data.pathName = path.join(NA.websitePhysicalPath, NA.webconfig.assetsRelativePath, compiledFile.replace(/\.less$/g,'.css'));
                fs.writeFileSync(path.join(NA.websitePhysicalPath, NA.webconfig.assetsRelativePath, compiledFile.replace(/\.less$/g,'.css')), output.css);
                NA.log(NA.appLabels.lessGenerate.replace(/%([\-a-zA-Z0-9_]+)%/g, function (regex, matches) { return data[matches]; }));
                next();
            });
        }, function () {

            /**
             * Next steps after less compilation.
             * @callback lessCompilation~callback
             */
            if (callback) {
                callback();
            }
        });
    } else {
        if (callback) {
            callback();
        }
    }

};

/**
 * Engine for compile Stylus file without call any CSS files.
 * @private
 * @function stylusCompilation
 * @memberOf NA#
 * @callback stylusCompilation~callback callback Next step after Stylus compilation.
 */
exports.stylusCompilation = function (callback) {
    var NA = this,
        enableStylus = NA.webconfig.enableStylus,
        async = NA.modules.async,
        path = NA.modules.path,
        stylus = NA.modules.stylus,
        fs = NA.modules.fs,
        allStylusCompiled,
        data = {},
        paths = [];

    if (enableStylus && enableStylus.stylus) {

        allStylusCompiled = enableStylus.stylus;

        if (enableStylus.paths && paths.length === 0) {
            for (var i = 0; i < enableStylus.paths.length; i++) {
                paths[i] = path.join(NA.webconfig.assetsRelativePath, enableStylus.paths[i]);
            }
        } else if (paths.length === 0) {
            paths = [
                NA.webconfig.assetsRelativePath,
                path.join(NA.webconfig.assetsRelativePath, 'stylesheets'),
                path.join(NA.webconfig.assetsRelativePath, 'styles'),
                path.join(NA.webconfig.assetsRelativePath, 'css')
            ];
        }

        async.each(allStylusCompiled, function (compiledFile, next) {
            var currentFile = fs.readFileSync(path.join(NA.websitePhysicalPath, NA.webconfig.assetsRelativePath, compiledFile), 'utf-8');

            stylus(currentFile)
            .set('paths', paths)
            .render(function (e, output) {
                if (e) {
                    NA.log(e);
                }

                data.pathName = path.join(NA.websitePhysicalPath, NA.webconfig.assetsRelativePath, compiledFile.replace(/\.styl$/g,'.css'));
                fs.writeFileSync(path.join(NA.websitePhysicalPath, NA.webconfig.assetsRelativePath, compiledFile.replace(/\.styl$/g,'.css')), output);
                NA.log(NA.appLabels.stylusGenerate.replace(/%([\-a-zA-Z0-9_]+)%/g, function (regex, matches) { return data[matches]; }));
                next();
            });
        }, function () {

            /**
             * Next steps after stylus compilation.
             * @callback stylusCompilation~callback
             */
            if (callback) {
                callback();
            }
        });
    } else {
        if (callback) {
            callback();
        }
    }

};

/**
 * Engine for minification and concatenation of all files with a Bundle configuration.
 * @private
 * @function cssMinification
 * @memberOf NA#
 * @callback cssMinification~callback callback Next step after minification and concatenation of all CSS.
 */
exports.cssMinification = function (callback) {
    var NA = this,
        bundles = NA.webconfig.bundles,
        cleanCss = NA.modules.cleanCss,
        async = NA.modules.async,
        path = NA.modules.path,
        fs = NA.modules.fs,
        enable,
        output = "",
        data = {},
        allCssMinified = [];

    /* Verify if bundle is okay and if engine must start. */
    enable = (NA.configuration.generate ||

        /**
         * CSS minification before each HTML response.
         * @public
         * @alias stylesheetsBundlesBeforeResponse
         * @type {boolean}
         * @memberOf NA#webconfig
         * @default false
         */
        NA.webconfig.stylesheetsBundlesBeforeResponse);

    if (typeof NA.webconfig.stylesheetsBundlesEnable === 'boolean') {

        /**
         * No CSS minification if set to false.
         * @public
         * @alias stylesheetsBundlesEnable
         * @type {boolean}
         * @memberOf NA#webconfig
         * @default true
         */
        enable = NA.webconfig.stylesheetsBundlesEnable;
    }

    /* Star engine. */
    if (bundles && bundles.stylesheets && enable) {

        NA.forEach(bundles.stylesheets, function (compressedFile) {
            allCssMinified.push(compressedFile);
        });

        async.each(allCssMinified, function (compressedFile, firstCallback) {

            async.map(bundles.stylesheets[compressedFile], function (sourceFile, secondCallback) {
                try {
                    secondCallback(null, fs.readFileSync(path.join(NA.websitePhysicalPath, NA.webconfig.assetsRelativePath, sourceFile), 'utf-8'));
                } catch (e) {
                    secondCallback(e, "");
                }
            }, function(error, results) {
                for (var i = 0; i < results.length; i++) {
                    output += results[i];
                }

                output = (new cleanCss().minify(output)).styles;
                fs.writeFileSync(path.join(NA.websitePhysicalPath, NA.webconfig.assetsRelativePath, compressedFile), output);
                data.pathName = path.join(NA.websitePhysicalPath, NA.webconfig.assetsRelativePath, compressedFile);
                NA.log(NA.appLabels.cssGenerate.replace(/%([\-a-zA-Z0-9_]+)%/g, function (regex, matches) { return data[matches]; }));
                output = "";

                firstCallback();
            });

        }, function () {

            /**
             * Next steps after minification and concatenation are done.
             * @callback cssMinification~callback
             */
            if (callback) {
                callback();
            }
        });
    } else {
        if (callback) {
            callback();
        }
    }
};

/**
 * Engine for optimization of all images with a Bundle configuration.
 * @private
 * @function imgOptimization
 * @memberOf NA#
 * @callback imgOptimization~callback callback Next step after optimization of all Images.
 */
exports.imgOptimization = function (callback) {
    var NA = this,
        optimizations = NA.webconfig.optimizations,
        imagemin = NA.modules.imagemin,
        jpegtran = NA.modules.jpegtran,
        optipng = NA.modules.optipng,
        gifsicle = NA.modules.gifsicle,
        svgo = NA.modules.svgo,
        async = NA.modules.async,
        path = NA.modules.path,
        enable,
        data = {},
        allImgMinified = [];

    /* Verify if bundle is okay and if engine must start. */
    enable = (NA.configuration.generate ||

        /**
         * Images optimization before each HTML response.
         * @public
         * @alias imagesOptimizationsBeforeResponse
         * @type {boolean}
         * @memberOf NA#webconfig
         * @default false
         */
        NA.webconfig.imagesOptimizationsBeforeResponse);

    if (typeof NA.webconfig.imagesOptimizationsEnable === 'boolean') {

        /**
         * No images minification if set to false.
         * @public
         * @alias imagesOptimizationsEnable
         * @type {boolean}
         * @memberOf NA#webconfig
         * @default true
         */
        enable = NA.webconfig.imagesOptimizationsEnable;
    }

    /* Star engine. */
    if (optimizations && optimizations.images && enable) {

        NA.forEach(optimizations.images, function (compressedFile) {
            allImgMinified.push(compressedFile);
        });

        async.each(allImgMinified, function (compressedFile, firstCallback) {

            var source = path.join(NA.websitePhysicalPath, NA.webconfig.assetsRelativePath, compressedFile),
                output = path.join(NA.websitePhysicalPath, NA.webconfig.assetsRelativePath, optimizations.images[compressedFile]);

                imagemin([source], output, { use: [
                        jpegtran(NA.webconfig.optimizations && NA.webconfig.optimizations.jpg),
                        optipng(NA.webconfig.optimizations && NA.webconfig.optimizations.png),
                        gifsicle(NA.webconfig.optimizations && NA.webconfig.optimizations.gif),
                        svgo(NA.webconfig.optimizations && NA.webconfig.optimizations.svg)
                    ] }).then(function () {
                    data.pathName = path.normalize(source);
                    
                    NA.log(NA.appLabels.imgGenerate.replace(/%([\-a-zA-Z0-9_]+)%/g, function (regex, matches) { return data[matches]; }));

                    firstCallback();
                });
        }, function () {

            /**
             * Next steps after minification and concatenation are done.
             * @callback cssMinification~callback
             */
            if (callback) {
                callback();
            }
        });
    } else {
        if (callback) {
            callback();
        }
    }
};

/**
 * Engine for obfuscation and concatenation of all files with a Bundle configuration.
 * @private
 * @function jsObfuscation
 * @memberOf NA#
 * @callback jsObfuscation~callback callback Next step after obfuscation and concatenation of all CSS.
 */
exports.jsObfuscation = function (callback) {
    var NA = this,
        bundles = NA.webconfig.bundles,
        uglifyJs = NA.modules.uglifyJs,
        async = NA.modules.async,
        path = NA.modules.path,
        fs = NA.modules.fs,
        enable,
        output = "",
        data = {},
        allJsMinified = [];

    /* Verify if bundle is okay and if engine must start. */
    enable = (NA.configuration.generate ||

        /**
         * JavaScript obfuscation before each HTML response.
         * @public
         * @alias javascriptBundlesBeforeResponse
         * @type {boolean}
         * @memberOf NA#webconfig
         * @default false
         */
        NA.webconfig.javascriptBundlesBeforeResponse);

    if (typeof NA.webconfig.javascriptBundlesEnable === 'boolean') {

        /**
         * No JavaScript obfuscation if set to false.
         * @public
         * @alias javascriptBundlesEnable
         * @type {boolean}
         * @memberOf NA#webconfig
         * @default true
         */
        enable = NA.webconfig.javascriptBundlesEnable;
    }

    /* Star engine. */
    if (bundles && bundles.javascript && enable) {

        NA.forEach(bundles.javascript, function (compressedFile) {
            allJsMinified.push(compressedFile);
        });

        async.each(allJsMinified, function (compressedFile, firstCallback) {

            async.map(bundles.javascript[compressedFile], function (sourceFile, secondCallback) {
                secondCallback(null, uglifyJs.minify(path.join(NA.websitePhysicalPath, NA.webconfig.assetsRelativePath, sourceFile)).code);
            }, function(error, results) {
                for (var i = 0; i < results.length; i++) {
                    output += results[i];
                }

                fs.writeFileSync(path.join(NA.websitePhysicalPath, NA.webconfig.assetsRelativePath, compressedFile), output);
                data.pathName = path.join(NA.websitePhysicalPath, NA.webconfig.assetsRelativePath, compressedFile);
                NA.log(NA.appLabels.jsGenerate.replace(/%([\-a-zA-Z0-9_]+)%/g, function (regex, matches) { return data[matches]; }));
                output = "";

                firstCallback();
            });

        }, function () {

            /**
             * Next steps after obfuscation and concatenation are done.
             * @callback jsObfuscation~callback
             */
            if (callback) {
                callback();
            }
        });
    } else {
        if (callback) {
            callback();
        }
    }
};

/**
 * Load the modules adding by the website.
 * @private
 * @function loadListOfExternalModules
 * @memberOf NA#
 * @param {loadListOfExternalModules~callback} callback Next steps after loading of additional modules.
 */
exports.loadListOfExternalModules = function (callback) {
    var NA = this,
        path = NA.modules.path;

    NA.loadController(NA.webconfig.commonController, function () {

        /**
         * Folder which contain the `node-atlas` node_modules.
         * @public
         * @alias nodeAtlasModulesPath
         * @type {String}
         * @memberOf NA#
         * @default « path/to/node-atlas/node_modules/ »
         */
        NA.nodeAtlasModulesPath = path.join(NA.serverPhysicalPath, 'node_modules');

        /**
         * Folder which contain the current website node_modules.
         * @public
         * @alias websiteModulesPath
         * @type {String}
         * @memberOf NA#
         * @default « path/to/current/website/node_modules/ »
         */
        NA.websiteModulesPath = path.join(NA.websitePhysicalPath, 'node_modules');

        /* Use the `NA.websiteController[<commonController>].loadModules(...)` function if set... */
        if (typeof NA.websiteController[NA.webconfig.commonController] !== 'undefined' &&
            typeof NA.websiteController[NA.webconfig.commonController].loadModules !== 'undefined') {

            /**
             * Define this function for adding npm module into `NA.modules` of application. Only for `common` controller file.
             * @function loadModules
             * @memberOf NA#websiteController[]
             */
            NA.websiteController[NA.webconfig.commonController].loadModules.call(NA);
        }

        /**
         * Next step !
         * @callback loadListOfExternalModules~callback
         */
        callback();
    });
};

/**
 * Load a controller file.
 * @private
 * @function loadController
 * @memberOf NA#
 * @param {string}                  controller The name of controller file we want to load.
 * @param {loadController~callback} callback   Next steps after controller loading.
 */
exports.loadController = function (controller, callback) {
    var NA = this,
        path = NA.modules.path,
        commonControllerPath = path.join(
            NA.websitePhysicalPath,
            NA.webconfig.controllersRelativePath,
            (controller) ? controller : ''
        );

    /* If a controller is required. Loading of this controller... */
    if (typeof controller !== 'undefined') {

        /* Open controller and load it */
        try {
            NA.websiteController[controller] = require(commonControllerPath);
        /* In case of error. */
        } catch (err) {
            if (err && err.code === 'MODULE_NOT_FOUND') {
                err.controller = commonControllerPath;
                return NA.log(NA.appLabels.notFound.controller.replace(/%([\-a-zA-Z0-9_]+)%/g, function (regex, matches) { return err[matches]; }));
            } else if (err) {
                return NA.log(err);
            }
        }
    }

    /**
     * Next step !
     * @callback loadController~callback
     */
    callback();
};