среда, 29 марта 2017 г.

SystemJS and Babel

Структура расположения папок и файлов:

/index.html

/source/css/index.css

/source/js/config/systemjs-config.js

/source/js/index.js

/source/js/app/view/view.html
/source/js/app/sum/sum.js
/source/js/app/render/render.js

/source/js/lib/react/react.js
/source/js/lib/react/react-dom.js

/source/js/lib/systemjs/system.src.js
/source/js/lib/systemjs/text.js
/source/js/lib/systemjs/css.js

/source/js/lib/systemjs/plugin-babel.js
/source/js/lib/systemjs/systemjs-babel-browser.js
/source/js/lib/systemjs/regenerator-runtime.js
/source/js/lib/systemjs/babel-helpers.js
/source/js/lib/systemjs/babel-helpers/ - папка с файлами

Файл index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <script type="text/javascript" src="source/js/lib/systemjs/system.src.js"></script>
    <script type="text/javascript" src="source/js/config/systemjs-config.js"></script>
    <title>SystemJS and Babel</title>
</head>
<body>
    <div id="root"></div>
    <div id="message"></div>
    <script type="text/javascript">SystemJS.import('js/index.js');</script>
</body>
</html>

Файл systemjs-config.js

SystemJS.config({
      baseURL: 'source'
    , map: {
          'plugin-babel': 'js/lib/systemjs/plugin-babel.js'
        , 'systemjs-babel-build': 'js/lib/systemjs/systemjs-babel-browser.js'
        , 'text': 'js/lib/systemjs/text.js'
        , 'css': 'js/lib/systemjs/css.js'
      }
    , transpiler: 'plugin-babel'
    , meta: {
          '*.js': {
              babelOptions: {
                    stage1: true
                  , react: true
              }
          }
        , '*.css': {loader: 'css'}
        , '*.html': {loader: 'text'}
      }
    , paths: {
          'react': 'js/lib/react/react.js'
        , 'react-dom': 'js/lib/react/react-dom.js'

        , indexStyle: 'style/default/index.css'

        , view: 'app/view/view.html'
        , sum: 'app/sum/sum.js'
        , render: 'app/render/render.js'
      }
});

Файл index.css

div#root {font-size: 50px;}

Файл index.js

import 'indexStyle';

import view from 'view';
import render from 'render';
import {sum} from 'sum';

document.getElementById('message').innerHTML = view;

render();

alert(sum(1, 2););

Файл view.html

<p>World</p>

Файл render.js

import React from 'react';
import ReactDOM from 'react-dom';

export default () => {
    ReactDOM.render(<h1>Hello</h1>, document.getElementById('root'));
};

Файл sum.js

export function sum (a, b) {return a + b;}

четверг, 23 марта 2017 г.

Webpack + RequireJS Plugins loader

Webpack loader polyfill for RequireJS "text" loader module.

Сборка через Webpack файлов, загружаемых с помощью плагинов для RequireJS имеющих пути вида require("plugin-name!path/to/file").

Рассмотрим сборку проекта на примере загрузки файлов с помощью плагина "text".

Папку "text-loader" с кодом плагина загрузчика для Webpack необходимо поместить в папку "node_modules".

/node_module/text-loader/index.js

Код файла "index.js"

module.exports = function (content) {
    this.cacheable && this.cacheable();
    this.value = content;
    return "module.exports = " + JSON.stringify(content);
}

В файле "webpack.config.js" необходимо добавить следующие строки для использования загрузчика "text-loader"
...
...
resolveLoader: {
    alias: {
        "text": "text-loader"
    }
},
...
...
resolve: {
    extensions: ['', '.webpack.js', '.web.js', '.js', '.ts', '.tsx'],
    alias: {
              ...
              ...
              template: path.resolve(__dirname, './source/template.html'),
              ...
              ...
          }
}
...

Пример кода javascript-файла, загружающего модуль "template.html" внутри себя

define(function (require) {
    var template = require('text!template');
    console.log(template);
});

Пример кода файла "template.html"

<h1>Hello</h1>

Пример кода плагина загрузчика JS-кода для Webpack.

/node_module/js-loader/index.js

module.exports = function (content) {
    this.cacheable && this.cacheable();
    this.value = content;
    return content;
}

В файле "webpack.config.js" необходимо добавить следующие строки для использования загрузчика "js-loader"
...
...
resolveLoader: {
    alias: {
        "js": "js-loader"
    }
},
...
...

Пример кода javascript-файла, загружающего модуль "module.js" внутри себя

define(function (require) {
    var alertOne = require('js!module');
    alertOne();
});

Пример кода файла "module.js"

define(function () {
    return function () {
        alert(1);
    };
});

среда, 22 марта 2017 г.

RequireJS Plugin Builder for ECMAScript 6 Babel

Структура расположения папок и файлов:

/index.html

/build/build.js - итоговый собранный файл с помщью Node.js

/js/app/main/main.js
/js/app/simple/simple.js
/js/app/sum/sum.js
/js/app/class/class.js
/js/app/render/render.js

/js/config/require-config.js

/js/lib/requirejs/require.js
/js/lib/loader/loader.js

/js/lib/babel-standalone/babel.js

/js/lib/react/react.js
/js/lib/react/react-dom.js

/gulp.js
/gulpfile.js

/node_modules
/node.exe
/npm.cmd

Файл index.html

<!DOCTYPE html>
<html>
  <head>
    <script src="js/lib/requirejs/require.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script src="build/build.js"></script>
    <script>require(['main']);</script>
  </body>
</html>

Файл main.js

define(function (require) {
    var simple = require('simple');
    simple();
    var sum = require('loader!sum');
    require('loader!class');
    require('loader!render');
    alert(sum(1, 2));
});

Файл simple.js

define(function () {
    return function () {alert(1);}
});

Файл sum.js

define(function (require) {

    return (a, b) => a + b;

});

Файл class.js

define(function (require) {

    var sum = require('loader!sum');

    console.log(sum(1, 2));

    class A {
        constructor (a) {
            console.log('Hello ' + a);
        }
    }

    new A('world!');

});

Файл render.js

define(function (require) {
    var React = require('react');
    var ReactDOM = require('react_dom');
    ReactDOM.render(<span>Hello World 123</span>, document.getElementById('root'));
});

Файл require-config.js

requirejs.config({
      paths: {
          loader: 'js/lib/loader/loader'
        , babel: 'js/lib/babel-standalone/babel'
        , react: 'js/lib/react/react'
        , react_dom: 'js/lib/react/react-dom'

        , 'main': 'js/app/main/main'
        , 'class': 'js/app/class/class'
        , 'sum': 'js/app/sum/sum'
        , 'render': 'js/app/render/render'
        , 'simple': 'js/app/simple/simple'
      }
});

Файл loader.js

// ECMAScript 6 Babel Loader Plugin

// For Browser

// Define paths:

// requirejs.config({
//       config: {
//           loader: {
//               resolveModuleSource: function (source) {return 'loader!' + source;}
//           }
//       }
//     , paths: {
//           loader: 'js/lib/loader/loader'
//         , babel: 'js/lib/babel-standalone/babel'
//       }
// });

// Then use as a normal RequireJS plugin:

// require(['loader!path/to/my-es6-file'], function (MyEs6File) {
//     var myfile = new MyEs6File();
//     ...
// });

// or

// define(function () {
//     var myfile = new MyEs6File();
//     ...
// });

// For Server

// To use your existing client-side AMD modules in node, use global-define and set the path to the ECMAScript 6 Babel Loader Plugin.
// This so that when RequireJS tries to resolve the module name, it maps to the correct package.

// require('global-define')({
//       basePath: __dirname
//     , paths: {
//           loader: 'js/lib/loader/loader'
//       }
// });

define(['babel'], function(Babel) {

    var buildMap = {}
        , fetchFile
        , fs;

    function isBrowser () {return typeof window !== 'undefined' && window.navigator && window.document;}
    function isNode () {return typeof process !== 'undefined' && process.versions && !!process.versions.node;}
    function isRequireBuildProcess () {return isNode() && require.nodeRequire;}

    if (isBrowser()) {
        fetchFile = function (url, callback) {
            var xhr;
            if (window.XMLHttpRequest) {
                xhr = new XMLHttpRequest();
            } else if (window.ActiveXObject) {
                xhr = new window.ActiveXObject('Microsoft.XMLHTTP');
            }
            xhr.open('GET', url, true);
            xhr.onreadystatechange = function () {
                if (xhr.readyState === 4) {callback(xhr.responseText);} // Do not explicitly handle errors, those should be visible via console output in the browser.
            };
            xhr.send(null);
        };
    } else if (isRequireBuildProcess()) {
        fs = require.nodeRequire('fs'); // nodeRequire is a method added by r.js
        fetchFile = function (path, callback) {
            callback(fs.readFileSync(path, 'utf8'));
        };
    } else if (isNode()) {
        fs = require('fs');
        fetchFile = function (path, callback) {
            callback(fs.readFileSync(path, 'utf8'));
        };
    }

    return {
          load: function (name, req, onload, config) {
            var url = req.toUrl(name + '.js');
            fetchFile(url, function (responseText) {
                var code = Babel.transform(responseText, {
                      presets: ['es2015', 'react']
                    , filename: 'embedded'
                    , sourceMaps: 'inline'
                }).code;
                if (config.isBuild) {buildMap[name] = code;}
                if (isNode() && !config.isBuild) {
                    var fileName = name + '.tmp';
                    fs.writeFileSync(fileName, code);
                    onload(req(fileName));
                    fs.unlink(fileName);
                } else {
                    onload.fromText(code);
                }
            });
          }
        , write: function (pluginName, moduleName, write) {
            if (moduleName in buildMap) {write.asModule(pluginName + '!' + moduleName, buildMap[moduleName]);}
          }
    };

});

Файл gulp.js

const task = process.argv.slice(2).join(' ')
        , command = `node ./node_modules/gulp/bin/gulp.js ${task}`
        , execProcess = require('child_process').exec(command);
execProcess.stdout.on('data', data => console.log(Buffer.isBuffer(data) ? data.toString() : data));
execProcess.stderr.on('data', data => console.log(Buffer.isBuffer(data) ? data.toString() : data));
execProcess.on('close', code => code === 0 ? console.log('Done') : console.log(`Exit code: ${code}`));

Файл gulpfile.js

const fs = require('fs')
        , del = require('del')
        , gulp = require('gulp')
        , requirejs = require('gulp-requirejs');

gulp.task('clean', () => del(['./build/**/*'], {force: true}));

gulp.task('requirejs', ['clean'], () => {
    eval('var requirejsConfig =' + fs.readFileSync('./js/config/require-config.js').toString().replace('requirejs.config(', '').replace(');', ';'));
    requirejsConfig.baseUrl = '.';
    const config = {};
    for (let key in requirejsConfig) {
        if (requirejsConfig.hasOwnProperty(key)) {config[key] = requirejsConfig[key];}
    }
    config.name = 'main';
    config.out = 'build.js';
    requirejs(config).pipe(gulp.dest('./build'));
});

вторник, 21 марта 2017 г.

RequireJS Loader Plugin for TypeScript

Структура расположения папок и файлов:

/index.html

/js/index.js

/js/app/main/main.js
/js/app/class.js
/js/app/sum.js

/js/config/require-config.js

/js/lib/requirejs/require.js
/js/lib/loader/loader.js

/'js/lib/typescript/typescriptServices.js

/js/lib/react/react.js
/js/lib/react/react-dom.js

Файл loader.js

// TypeScript Loader Plugin

// For Browser

// Define paths:

// requirejs.config({
//       config: {
//           loader: {
//               resolveModuleSource: function (source) {return 'loader!' + source;}
//           }
//       }
//     , paths: {
//           loader: 'js/lib/loader/loader'
//         , typescript: 'js/lib/typescript/typescriptServices'
//       }
// });

// Then use as a normal RequireJS plugin:

// require(['loader!path/to/my-typescript-file'], function (MyTypescriptFile) {
//     var myfile = new MyTypescriptFile();
//     ...
// });

// or

// define(function () {
//     var myfile = new MyTypescriptFile();
//     ...
// });

// For Server

// To use your existing client-side AMD modules in node, use global-define and set the path to the TypeScript Loader Plugin.
// This so that when RequireJS tries to resolve the module name, it maps to the correct package.

// require('global-define')({
//       basePath: __dirname
//     , paths: {
//           loader: 'js/lib/loader/loader'
//       }
// });

define(['typescript'], function (ts) {

    var buildMap = {}
        , fetchFile
        , fs;

    function isBrowser () {return typeof window !== 'undefined' && window.navigator && window.document;}
    function isNode () {return typeof process !== 'undefined' && process.versions && !!process.versions.node;}
    function isRequireBuildProcess () {return isNode() && require.nodeRequire;}

    if (isBrowser()) {
        fetchFile = function (url, callback) {
            var xhr;
            if (window.XMLHttpRequest) {
                xhr = new XMLHttpRequest();
            } else if (window.ActiveXObject) {
                xhr = new window.ActiveXObject('Microsoft.XMLHTTP');
            }
            xhr.open('GET', url, true);
            xhr.onreadystatechange = function () {
                if (xhr.readyState === 4) {callback(xhr.responseText);} // Do not explicitly handle errors, those should be visible via console output in the browser.
            };
            xhr.send(null);
        };
    } else if (isRequireBuildProcess()) {
        fs = require.nodeRequire('fs'); // nodeRequire is a method added by r.js
        fetchFile = function (path, callback) {
            callback(fs.readFileSync(path, 'utf8'));
        };
    } else if (isNode()) {
        fs = require('fs');
        fetchFile = function (path, callback) {
            callback(fs.readFileSync(path, 'utf8'));
        };
    }

    return {
          load: function (name, req, onload, config) {
            if (!window.exports) {window.exports = {};}
            var url = req.toUrl(name + '.js');
            fetchFile(url, function (responseText) {
                var code = ts.transpileModule(responseText, {
                      compilerOptions: {
                          module: 'none' // 'amd' or ts.ModuleKind.AMD
                        , target: 'es3' // ts.ScriptTarget.ES3

                        , noImplicitUseStrict: true

                        , noEmitHelpers: true

                        , removeComments: true

                        , sourceMap: true
                        , inlineSourceMap: true
                        , inlineSources: true

                        , jsx: 'react'

                        , importHelpers: true
                      }
                    , moduleName: ''
                }).outputText;
                if (code === undefined) {throw new Error('Output generation failed');}
                if (config.isBuild) {buildMap[name] = code;}
                if (isNode() && !config.isBuild) {
                    var fileName = name + '.tmp';
                    fs.writeFileSync(fileName, code);
                    onload(req(fileName));
                    fs.unlink(fileName);
                } else {
                    onload.fromText(code);
                }
            });
          }
        , write: function (pluginName, moduleName, write) {
            if (moduleName in buildMap) {write.asModule(pluginName + '!' + moduleName, buildMap[moduleName]);}
          }
    };

});

Файл index.html

<!DOCTYPE html>
<html>
  <head>
    <script src="js/lib/requirejs/require.js"></script>
    <script src="js/config/require-config.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script src="js/index.js"></script>
  </body>
</html>

Файл require-config.js

requirejs.config({
      config: {
          loader: {
              resolveModuleSource: function (source) {return 'loader!' + source;}
          }
      }
    , shim: {
          typescript: {exports: 'ts'}
      }
    , paths: {
          loader: 'js/lib/loader/loader'
        , typescript: 'js/lib/typescript/typescriptServices'
        , react: 'js/lib/react/react'
        , react_dom: 'js/lib/react/react-dom'

        , 'main': 'js/app/main/main'
        , 'class': 'js/app/class/class'
        , 'sum': 'js/app/sum/sum'
      }
});

Файл index.js

require(['loader!main']);

Файл main.js

define(function (require) {
    var React = require('react')
        , ReactDOM = require('react_dom');
    require('loader!class');
    ReactDOM.render(<span>Hello World 123</span>, document.getElementById('root'));
});

Файл class.js

define(function (require) {

    const sum: any = require('loader!sum');

    console.log(sum(1, 2));

    class A {
        constructor (a) {
            console.log('Hello ' + a);
        }
    }

    new A('world!');

});

Файл sum.js

define(function (require) {

    return (a, b) => a + b;

});

четверг, 16 марта 2017 г.

Компиляция файла TypeScript в браузере

<!DOCTYPE html>
<html>
  <head>
      <title>TypeScript Compilation</title>
  </head>
  <body>
    <div id="root"></div>
    <textarea id="source" style="height: 300px; width: 500px;">

        define(function (require) {
        const b: any = require('some');
        const c: any = 1;
        if (1 == '2') {console.log(12);}
        class A {
            constructor (d) {
                const a = 1;
            }
        }
        class B extends A {
            constructor () {
                super();
            }
            render () {
                return (
                    <div>a</div>;
                );
            }

        }
        // comments

});

    </textarea>
    <textarea id="result" style="height: 300px; width: 500px;"></textarea>

    <script src="js/lib/typescript/typescriptServices.js"></script>
    <script type="text/javascript">

        var outputText = ts.transpileModule(document.getElementById('source').value, {
              compilerOptions: {
                  module: 'none' // 'amd' or ts.ModuleKind.AMD
                , target: 'es3' // ts.ScriptTarget.ES3

                , noImplicitUseStrict: true

                , noEmitHelpers: true

                , removeComments: true

                , sourceMap: true
                , inlineSourceMap: true
                , inlineSources: true

                , jsx: 'react'

                , importHelpers: true
              }
            , moduleName: ''
        })
        console.log(outputText);
        outputText = outputText.outputText;
        if (outputText === undefined) {throw new Error("Output generation failed");}
        document.getElementById('result').innerHTML = outputText;

    </script>
  </body>
</html>

среда, 15 марта 2017 г.

RequireJS Loader Plugin for ECMAScript 6 Babel

Структура расположения папок и файлов:

/index.html

/js/index.js

/js/app/main/main.js
/js/app/class.js
/js/app/sum.js

/js/config/require-config.js

/js/lib/requirejs/require.js
/js/lib/loader/loader.js

/js/lib/babel-standalone/babel.js

/js/lib/react/react.js
/js/lib/react/react-dom.js

Файл loader.js

// ECMAScript 6 Babel Loader Plugin

// For Browser

// Define paths:

// requirejs.config({
//       config: {
//           loader: {
//               resolveModuleSource: function (source) {return 'loader!' + source;}
//           }
//       }
//     , paths: {
//           loader: 'js/lib/loader/loader'
//         , babel: 'js/lib/babel-standalone/babel'
//       }
// });

// Then use as a normal RequireJS plugin:

// require(['loader!path/to/my-es6-file'], function (MyEs6File) {
//     var myfile = new MyEs6File();
//     ...
// });

// or

// define(function () {
//     var myfile = new MyEs6File();
//     ...
// });

// For Server

// To use your existing client-side AMD modules in node, use global-define and set the path to the ECMAScript 6 Babel Loader Plugin.
// This so that when RequireJS tries to resolve the module name, it maps to the correct package.

// require('global-define')({
//       basePath: __dirname
//     , paths: {
//           loader: 'js/lib/loader/loader'
//       }
// });

define(['babel'], function(Babel) {

    var buildMap = {}
        , fetchFile
        , fs;

    function isBrowser () {return typeof window !== 'undefined' && window.navigator && window.document;}
    function isNode () {return typeof process !== 'undefined' && process.versions && !!process.versions.node;}
    function isRequireBuildProcess () {return isNode() && require.nodeRequire;}

    if (isBrowser()) {
        fetchFile = function (url, callback) {
            var xhr;
            if (window.XMLHttpRequest) {
                xhr = new XMLHttpRequest();
            } else if (window.ActiveXObject) {
                xhr = new window.ActiveXObject('Microsoft.XMLHTTP');
            }
            xhr.open('GET', url, true);
            xhr.onreadystatechange = function () {
                if (xhr.readyState === 4) {callback(xhr.responseText);} // Do not explicitly handle errors, those should be visible via console output in the browser.
            };
            xhr.send(null);
        };
    } else if (isRequireBuildProcess()) {
        fs = require.nodeRequire('fs'); // nodeRequire is a method added by r.js
        fetchFile = function (path, callback) {
            callback(fs.readFileSync(path, 'utf8'));
        };
    } else if (isNode()) {
        fs = require('fs');
        fetchFile = function (path, callback) {
            callback(fs.readFileSync(path, 'utf8'));
        };
    }

    return {
          load: function (name, req, onload, config) {
            var url = req.toUrl(name + '.js');
            fetchFile(url, function (responseText) {
                var code = Babel.transform(responseText, {
                      presets: ['es2015', 'react']
                    , filename: 'embedded'
                    , sourceMaps: 'inline'
                }).code;
                if (config.isBuild) {buildMap[name] = code;}
                if (isNode() && !config.isBuild) {
                    var fileName = name + '.tmp';
                    fs.writeFileSync(fileName, code);
                    onload(req(fileName));
                    fs.unlink(fileName);
                } else {
                    onload.fromText(code);
                }
            });
          }
        , write: function (pluginName, moduleName, write) {
            if (moduleName in buildMap) {write.asModule(pluginName + '!' + moduleName, buildMap[moduleName]);}
          }
    };

});

Файл index.html

<!DOCTYPE html>
<html>
  <head>
    <script src="js/lib/requirejs/require.js"></script>
    <script src="js/config/require-config.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script src="js/index.js"></script>
  </body>
</html>

Файл require-config.js

requirejs.config({
      config: {
          loader: {
              resolveModuleSource: function (source) {return 'loader!' + source;}
          }
      }
    , paths: {
          loader: 'js/lib/loader/loader'
        , babel: 'js/lib/babel-standalone/babel'

        , react: 'js/lib/react/react'
        , react_dom: 'js/lib/react/react-dom'

        , 'main': 'js/app/main/main'
        , 'class': 'js/app/class/class'
        , 'sum': 'js/app/sum/sum'
      }
});

Файл index.js

require(['loader!main']);

Файл main.js

define(function (require) {

    require('loader!class');

    var React = require('react')
        , ReactDOM = require('react_dom');

    ReactDOM.render(<span>Hello World 123</span>, document.getElementById('root'));

});

Файл class.js

define(function (require) {

    var sum = require('loader!sum');

    console.log(sum(1, 2));

    class A {
        constructor (a) {
            console.log('Hello ' + a);
        }
    }

    new A('world!');

});

Файл sum.js

define(function (require) {

    return (a, b) => a + b;

});

RequireJS Plugin - Полное описание устройства

/*
 * RequireJS loader plugin
 */

require(['./path/to/plugin-name!path/to/file'], function (something) {
    // something это ссылка на файл ресурс 'path/to/file', который был загружен с помощью плагина './path/to/plugin-name.js'
});

;(function () {
    'use strict';
    define(['./path/to/babel-standalone/babel'], function (Babel) {
        return {
              // Загрузить и выполнить содержимое модуля
              load: function (name, req, onload, config) { // функция для загрузки содержимого файла 'path/to/file' через данный плагин (единственная обязательная функция)
                // name - строка с именем файла ресурса, которое располагается после знака ! в строке с путем до файла
                // Для пути './path/to/plugin-name!path/to/file': name = 'path/to/file'

                // req() - локальная ссылка на функцию "require", которую можно использовать здесь для загрузки любых других модулей.
                // Пути до этих модулей являются относительными путями от пути модуля, загружаемого через данный плагин.
                // Функция req содержит в себе ряд дополнительных утилит:

                // req.toUrl(modulePath) - функция, возращает полный путь до загружаемого стороннего модуля с учетов общей конфигурации config для RequireJS
                // moduleResource - представляет собой имя или путь до модуля вместе с его расширением, например 'view/templates/main.html'

                // req.defined(moduleName) - функция, которая использовалась ранее в RequireJS в версии до 0.25.0. Возвращает true, если
                // конкретный модуль уже загружен и учтен.

                // req.specified(moduleName) - функция возвращает true, если уже произведен запрос на загрузку модуля или он уже находится в процессе
                // загрузки и должен быть доступен в некоторой точке выполнения программы.

                // onload() - функция сообщает загрузчику о том, что плагин завершил загрузку модуля.
                // Данная функция, вызываемая со значением value модуля name.

                // onload.error()  - функция может быть вызвана, если плагин обнаруживает ошибку, которая не позволяет загрузить модуль правильно.
                // В данную функцию передается объект error.

                // config - конфигурационный объект, в котором содержится информация о конфигурации данного плагина.
                // Плагин i18n! использует объект config для получения текущей локали языка, если приложение хочет принудительно установить другую локаль языка.
                // Сборщик r.js установит свойство isBuild в config со значением true, если плагин или pluginBuilder будет вызван, в составе сборщика r.js

                // Плагин должен вызывать функцию onload() сразу, как только значение config.isBuild будет равно true.
                if (config.isBuild) {
                    // Обозначает, что сборщик r.js не должен ждать и может начать сборку. Данный ресурс будет получен динамически в процессе работы браузера.
                    onload();
                } else {
                    // Сделать что-то еще, что может быть асинхронным.
                }

                // Пример плагина работающего также, как и обычный require()
                req([name], function (value) {
                    onload(value);
                });

                // Иногда плагином загружается JavaScript-код в виде текстовой строки, которую надо дополнительно выполнить.
                // Для этого существует функция onload.fromText(), которая может выполнить загруженный JavaScript-код посредством функции eval()
                // RequireJS выполнит правильно работу по вызову любой анонимной функции define(), обнаруженной в  текстовой строке, после чего
                // используется полученный модуль.
                // Функция onload.fromText(text) принимает аргумент text, который представляет из себя строку с JavaScript-кодом, который надо выполнить.
                // Пример работы плагина с обработкой строки с JavaScript-кодом:
                var url = req.toUrl(name + '.txt');
                fetchText(url, function (text) {
                    text = transform(text);
                    onload.fromText(text);
                });
                // До RequireJS 2.1.0 функция onload.fromText(moduleName, text) принимала в качестве первого аругмента путь до модуля, а сам плагин
                // должен был вручную вызывать функцию require([moduleName], onload) после вызова функции onload.fromText()
              }
              // Нормализовать путь до модуля
            , normalize: function (name, normalize) { // функция для нормализации имени загружаемого файла 'path/to/file' (необходима только, если имя файла не является именем модуля)
                // Некоторые модули имеют относительные пути, поэтому они должны быть преобразованы в полные пути.
                // name - путь до модуля, который должен быть нормализован.
                // normalize() - функция, которая может быть вызвана для нормализации пути до модуля.
                // Данную функцию не нужно создавать, если вы используете стандартные обычные до модулей.
                // Например, плагин text! не имеет в себе функцию normalize().

                // Пример кода функции normalize()
                // define(['index!2?./a:./b:./c'], function (indexResource) {
                //     // indexResource will be the module that corresponds to './c'.
                // });
                function parse(name) {
                    var parts = name.split('?')
                        , index = parseInt(parts[0], 10)
                        , choices = parts[1].split(':')
                        , choice = choices[index];
                    return {
                          index: index
                        , choices: choices
                        , choice: choice
                    };
                }
                var parsed = parse(name)
                    , choices = parsed.choices;
                for (i = 0; i < choices.length; i++) {
                    choices[i] = normalize(choices[i]);
                }
                return parsed.index + '?' + choices.join(':');
              }

              // Записать содержимое модуля в итоговый файл сборки
            , write: function (pluginName, moduleName, write) { // функция используется сборщиком r.js для обозначения того, когда плагин должен начать работу для преобразований содержимого файла 'path/to/file' перед записью в итоговый файл сборки
                // Функция write нужна только, если плагин будет выводить что-то для сборщика.

                // pluginName - строка с номализованным путем до плагина.
                // Большинство плагинов не будут иметь имени, то есть будут анонимными плагинами, поэтому полезно знать нормализорванный путь до плагина в
                // файле сборщике.

                // moduleName -  строка с номализованным путем до модуля.

                // write() - функция, которая вызывается для передачи строки для записи в итоговый файл сборки.

                // write.asModule(moduleName, text) - функция, которая используется для записи модуля имеющего вызов анонимной define(), требующей втавку имени
                // и содержащего неявный вызов зависимеостей через require(""), которые должны быть помещены в итоговый файл сборки.
                // Функцию write.asModule() удобно использовать для плагинов, трансформирующих текст, таких как плагин для CoffeeScript.
                // Плагин text! использует функцию write для записи строки с текстом в итоговый файл сборки.

                // Пример кода функции write()
                if (moduleName in buildMap) {
                    var text = jsEscape(buildMap[moduleName]);
                    write('define("' + pluginName + '!' + moduleName  + '", function () { return "' + text + '";});\n');
                }
              }
            , pluginBuilder: '' // строка с именем модуля, который должен быть использован сборщиком r.js для преобразований содержимого файла 'path/to/file' перед записью в итоговый файл сборки
            // Используется, если билд плагин содержит серьезную логику работы для Node.js, которую не имеет смысла хранить в плагине, который используется в браузере.
            // Не используйте именные модули в составе плагина или pluginBuilder.
            // Текстовое содержимое pluginBuilder используется вместо содержимого основного файла плагина, но это работает только, если файлы не вызывают модули через define() по именам.
            //////////////////////////////////////////////////////////////////////////////////////////
            , onLayerEnd: function (write, data) { // функция используемая только сборщиком r.js в версии 2.1.0 и позже.
            // Функция вызывается после сборки всех модулей данного типа, например чтобы сбросить внутреннее состояние.
            // Например плагину требуется записать некоторые утилитные функции после сборки.

            // write() - функция, вызываемая со строкой вывода для записи в оптимизационный слой.

            // data - объект, хранящий в себе информацию о слое. Имеет 2 свойства:
            // name - имя модуля слоя. Может быть undefined.
            // path - путь до файла слоя. Может быть undefined, если вывод является строкой, содержащей другой script.
              }
            , writeFile: function (pluginName, name, parentRequire, write) { // функция, которая используется только сборщиком r.js, нужная только, если
            // плагину надо записать альтернативную версию зависимости, используемую плагином

           // pluginName - строка с нормализованным именем плагина

            // name - строка с номанлизованным именем модуля

            // parentRequire() - локальная функция require(). В основном используется для вызова parentRequire.toUrl() для создания путей до файлов,
            // использующихся внутри директории сборки.

            // write(fileName, text) - функция, вызываемая с двумя аргументами:
            // fileName - строка с именем файла для записи. Вы можете использовать функцию parentRequire.toUrl() для относительных путей для генерации
            // внутри директории сборки.
            // text - строка с содержимым файла в кодировке UTF-8.

            // write.asModule(moduleName, fileName, text) - функция
            // asModule - может использоваться для записи и вставки анонимных вызовов функции define() в итоговый файл сборки.

            // Пример использовани яфункции writeFile() находится в плагиен  text!

              }
        };
    });
})();

вторник, 14 марта 2017 г.

JavaScript-шаблонизатор Template аналог EJS. Версия 3 с выводом ошибок в консоль.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>TPL Engine</title>
</head>
<body><div id="output"></div>
<script type="text/javascript">

// Преобразование содержимого объекта в текст для вставки в HTML-код.

// Параметры:
// obj - конвертируемый объект.

// Пример использовния :
// var obj = {
//       a: 1
//     , b: null
//     , c: undefined
//     , d: 'text<br/>text'
//     , e: [1,2,3]
//     , f: {objkey: 'objvalue'}
//     , g: function () {console.log(1);}
//     , h: new Date()
//     , i: true
//     , j: new RegExp('a{1,2}')
//     , k: new Error('some error')
//     , l: NaN
//     , m: []
//     , n: {}
// };
// convertObjectToHtmlText(obj);
// Возвращает отформатированную строку, представляющую содержимое объекта, пригодную для вставки в HTML-код.
// Возвращает сообщение об ошибке, если в функцию не был передан объект.

// Преобразовать содержимое объекта в текст для вставки в HTML-код
function convertObjectToHtmlText (obj, options) {
    var indentString = '&nbsp;&nbsp;&nbsp;&nbsp;'
       , newLine = '<br />'
       , newLineJoin = ',' + newLine;
    if (options && options.rawHTML) {
        indentString = '    '; // 4 пробела
        newLine = '\n';
        newLineJoin = ',' + newLine;
    }
    // Функция определения типа объекта
    function objectType (obj) {
        var types = {
              'null': 'null'
            , 'undefined': 'undefined'
            , 'number': 'number'
            , 'boolean': 'boolean'
            , 'string': 'string'
            , '[object Function]': 'function'
            , '[object RegExp]': 'regexp'
            , '[object Array]': 'array'
            , '[object Date]': 'date'
            , '[object Error]': 'error'
        };
        return types[typeof obj] || types[Object.prototype.toString.call(obj)] || (obj ? 'object' : 'null');
    }
    // Функция определения числа элементов в объекте
    function objectSize (obj) {
        var size = 0
            , key;
        for (key in obj) {
            if (obj.hasOwnProperty(key)) {size++;}
        }
        return size;
    }
    // Привести элемент к нужному формату
    function formatElement (element, indent, indentFromArray) {
        indentFromArray = indentFromArray || '';
        switch (objectType(element)) {
            case 'null': return indentFromArray + 'null';
            case 'undefined': return indentFromArray + 'undefined';
            case 'number': return indentFromArray + element;
            case 'boolean': return indentFromArray + (element ? 'true' : 'false');
            case 'string': if (options && options.rawHTML) {
                                    return indentFromArray + '"'
                                                                         + element
                                                                         + '"';
                                } else {
                                    return indentFromArray + '&quot;'
                                                                         + element.replace(/&/g, '&amp;')
                                                                                        .replace(/</g, '&lt;')
                                                                                        .replace(/>/g, '&gt;')
                                                                                        .replace(/"/, '&quot;')
                                                                                        .replace(/'/g, '&#039;')
                                                                         + '&quot;';
                                }
            case 'array': return indentFromArray + (element.length > 0 ? '[' + newLine + formatArray(element, indent) + indent + ']' : '[]');
            case 'object': return indentFromArray + (objectSize(element) > 0 ? '{' + newLine + formatObject(element, indent) + indent + '}' : '{}');
            default: return indentFromArray + element.toString();
        }
    }
    // Привести массив к нужному формату
    function formatArray (array, indent) {
        var index
            , length = array.length
            , value = [];
        indent += indentString;
        for (index = 0; index < length; index += 1) {
            value.push(formatElement(array[index], indent, indent));
        }
        return value.join(newLineJoin) + newLine;
    }
    // Привести объект к нужному формату
    function formatObject (object, indent) {
        var value = []
            , property;
        indent += indentString;
        for (property in object) {
            if (object.hasOwnProperty(property)) {
                if (options && options.rawHTML) {
                    value.push(indent + '"' + property + '": ' + formatElement(object[property], indent));
                } else {
                    value.push(indent + '&quot;' + property + '&quot;: ' + formatElement(object[property], indent));
                }
            }
        }
        return value.join(newLineJoin) + newLine;
    }
    if (typeof obj === 'object') {return formatElement(obj, '');
    } else {throw new Error ('No javascript object has been provided to function convertObjectToHtmlText (obj) {...}');
    }
}

Function.prototype.trace = function () {
    var trace = []
        , current = this;
    while (current) {
        trace.push(current.signature());
        current = current.caller;
    }
    return trace;
};

Function.prototype.signature = function () {
    var signature = {
                                  name: this.getName()
                                , params: []
                                , toString: function () {
                                                    var params = ''
                                                        , i = 0
                                                        , len = this.params.length
                                                        , last = len - 1;
                                                    for (; i < len; i++) {
                                                        if (typeof this.params[i] === 'object') {
                                                            params += convertObjectToHtmlText(this.params[i], {rawHTML: true});
                                                        } else if (typeof this.params[i] === 'string') {
                                                            params += '"' + this.params[i] + '"';
                                                        } else {
                                                            params += this.params[i];
                                                        }
                                                        if (len !== 1 && last !== i) {
                                                            params += ', ';
                                                        }
                                                    }
                                                    return this.name + '(' + params + ')';
                                  }
                            };
    if (this.arguments) {
        for (var i = 0, len = this.arguments.length; i < len; i++) {
            signature.params.push(this.arguments[i]);
        }
    }
    return signature;
};

Function.prototype.getName = function () {
    if (this.name) {
        return this.name;
    }
    var definition = this.toString().split('\n')[0]
        , exp = /^function ([^\s(]+).+/;
    if (exp.test(definition)) {
        return definition.split('\n')[0].replace(exp, '$1') || 'anonymous';
    }
    return 'anonymous';
};

function getTypeOf (value) {
    if (value === window) {return 'window';}
    if (value === null) {return 'null';}
    if (value === undefined) {return 'undefined';}
    if (value !== value) {return 'nan';}
    return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}

function error (message, value) {
    var trace = arguments.callee.trace()
        , formattedTrace = ''
        , i = 0
        , len = trace.length
        , last = len - 1;
    for (; i < len; i++) {
        formattedTrace = trace[i] + ';';
        if (len !== 1 && last !== i) {
            formattedTrace += '\n';
        }
    }
    if (arguments.length === 2) {
        if (typeof value === 'object') {
            value = convertObjectToHtmlText(value, {rawHTML: true});
        }
        if (typeof value === 'string' && value.length === 0) {
            value = '""';
        }
        message += ' ' + value + '\n\nStack trace\n\n' + formattedTrace;
    }
    if (console && console.error) {
        console.error(message);
    } else {
        throw new Error(message);
    }
}

;(function (global, factory) {
    if (typeof define === "function" && define.amd) {
        define(factory);
    } else if (typeof module === "object" && typeof module.exports === "object") {
        module.exports = factory();
    } else {
        global.TPL = factory();
    }
})(this, function () {

    // Usage:
    // console.log( new TPL.Template("<div>Total: {{= value }}<div>", {delimiters: ["{{", "}}"]}).render({value: "<span>15</span>"}) );
    // console.log( new TPL.Template("<div>Total: {{- block('<div>{{= value }}</div>', {delimiters: ['{{', '}}']}, {value: value}) }}<div>", {delimiters: ["{{", "}}"]}).render({block: TPL.block, value: "<span>15</span>"}) );

    // Options:
    // delimiters       - characters to use for open and close delimiters
    // rmWhitespace - remove whitespace between HTML-tags

    // Tags:
    // <%    - scriptlet tag for control-flow, no output
    // <%% - outputs raw template part
    // <%-   - outputs the raw unescaped value into the template
    // <%=  - outputs the HTML escaped value into the template
    // <%#  - comment tag, no execution, no output
    // %>    - plain ending tag

    // Block:
    // "<div><%- block('<div><%= value %></div>', {delimiters: ['<%', '%>']}, {value: outerValue}) %></div>"

    function Template (templateString, settingsObject) {
        var dummy = {render: function () {return '';}};
        if (typeof templateString !== 'string') {error('Template must be a string, but it is: ', templateString); return dummy;}
        if (arguments.length === 2) {
            if (getTypeOf(settingsObject) !== 'object') {error('Settings must be an object, but it is: ', settingsObject); return dummy;}
            if (!('delimiters' in settingsObject || 'rmWhitespace' in settingsObject)) {error('Settings must contains property "delimiters" or property "rmWhitespace", but it is: ', settingsObject); return dummy;}
            if ('delimiters' in settingsObject) {
                if (getTypeOf(settingsObject.delimiters) !== 'array') {error('Property "delimiters" must be an array, but it is: ', settingsObject.delimiters); return dummy;}
                if (settingsObject.delimiters.length !== 2 || typeof settingsObject.delimiters[0] !== 'string' || typeof settingsObject.delimiters[1] !== 'string') {
                    error('Property "delimiters" must be an array with 2 strings, but it is: ', settingsObject.delimiters); return dummy;
                }
            }
            if ('rmWhitespace' in settingsObject && getTypeOf(settingsObject.rmWhitespace) !== 'boolean') {error('Property "rmWhitespace" must be a boolean, but it is: ', settingsObject.rmWhitespace); return dummy;}
        }
        var delimiters = ["<%", "%>"]
            , rmWhitespace = false; // removeWhitespace
        if (settingsObject) {
            if ('delimiters' in settingsObject) {delimiters = settingsObject.delimiters;}
            if ('rmWhitespace' in settingsObject) {rmWhitespace = settingsObject.rmWhitespace;}
        }
        var renderFunction = createRenderFunction(templateString, delimiters);
        return {render: function (dataObject) {
            var renderedHTML = renderFunction(dataObject, escapeHtmlFunction);
            if (rmWhitespace === true) {
                renderedHTML = removeSpaces(renderedHTML);
            }
            return renderedHTML;
        }};
    }

    function removeSpaces (renderedHTML) {
        return renderedHTML.replace(/\r/g, "") // Remove carriage return symbols
                                       .replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "") // Trim space: \s, BOM: \uFEFF, NBSP: \xA0
                                       .replace(/\s+(<[^%(?:span|b|strong|i|em|del|s|u|a|sub|sup|small|abbr|img)])/g, "$1") // Remove spaces between start tag and content
                                       .replace(/([^%(?:span|b|strong|i|em|del|s|u|a|sub|sup|small|abbr|img)]>)\s+/g, "$1") // Remove spaces between end tag and content
                                       .replace(/\s+/g, " "); // Remove spaces inside content
    }

    function createMatchRegExp (delimiters) { // delimiters = ["<%", "%>"]
        var openDelimiter = delimiters[0] // <%
            , closeDelimiter = delimiters[1] // %>
            , stringInsideDelimiters = "([\\s\\S]+?)"         // any string
            , tags = {
                  rawTemplatePart:            openDelimiter + "%" + stringInsideDelimiters + closeDelimiter // <%% "<div><%= value %></div>" %> --> "<div><%= value %></div>"
                , rawHtmlUnescapedValue: openDelimiter + "-" + stringInsideDelimiters + closeDelimiter   // <%- "1<br />2" %> --> "1<br />2"
                , htmlEscapedValue:          openDelimiter + "=" + stringInsideDelimiters + closeDelimiter  // <%= "1<br />2" %> --> "1&lt;br /&gt;2"
                , comment:                      openDelimiter + "#" + stringInsideDelimiters + closeDelimiter  // <%# any comment %> --> ""
                , evaluate:                       openDelimiter + stringInsideDelimiters + closeDelimiter           // <% if (value === 1) { %>one<% } %> --> "one"
              }
            , matchRegExp = new RegExp([
                  tags.rawTemplatePart // Important!!! Must be first!
                , tags.rawHtmlUnescapedValue
                , tags.htmlEscapedValue
                , tags.comment
                , tags.evaluate // Important!!! Must be last!
              ].join("|") + "|$", "g"); // /<%%([\s\S]+?)%>|<%-([\s\S]+?)%>|<%=([\s\S]+?)%>|<%#([\s\S]+?)%>|<%([\s\S]+?)%>|$/g
        return matchRegExp;
    }

    function createRenderFunction (templateString, delimiters) {
        var matchRegExp = createMatchRegExp(delimiters)
            , renderFunction;
        try {
            renderFunction = new Function ("_dataObject_", "_escapeHtmlFunction_" // ) {
                     , "if (!_dataObject_) {_dataObject_ = {};}" + "\n"
                    + "var _parsedTemplateString_ = '';" + "\n"
                    + "with (_dataObject_) {" + "\n"
                    +     "_parsedTemplateString_ += '" + (function(){
                                    var resultString = ""
                                        , index = 0;
                                    templateString.replace(matchRegExp, function (
                                          match
                                        , rawTemplatePartCodeString
                                        , rawUnescapedValueCodeString
                                        , htmlEscapedValueCodeString
                                        , commentCodeString
                                        , evaluateCodeString
                                        , offset
                                    ) {
                                        resultString += escapeString(templateString.slice(index, offset));
                                        index = offset + match.length;
                                        if (rawTemplatePartCodeString) {
                                            resultString += delimiters[0] + escapeString(rawTemplatePartCodeString) + delimiters[1];
                                        } else if (rawUnescapedValueCodeString) {
                                            resultString += "'" + "\n"
                                                                 + "+ (function _rawUnescapedValue_ () {" + "\n"
                                                                 +          "var _value_ = " + rawUnescapedValueCodeString + ";" + "\n"
                                                                 +          "if (_value_ === undefined || _value_ === null) {return '';} else {return _value_;}" + "\n"
                                                                 +     "})()" + "\n"
                                                                 + "+ '";
                                        } else if (htmlEscapedValueCodeString) {
                                            resultString += "'" + "\n"
                                                                 + "+ (function _htmlEscapedValue_ () {" + "\n"
                                                                 +          "var _value_ = " + htmlEscapedValueCodeString + ";" + "\n"
                                                                 +          "if (_value_ === undefined || _value_ === null) {return '';} else {return _escapeHtmlFunction_(_value_);}" + "\n"
                                                                 +     "})()" + "\n"
                                                                 + "+ '";
                                        } else if (commentCodeString) {
                                            // Do nothing
                                        } else if (evaluateCodeString) {
                                            resultString += "';" + "\n"
                                                                 + evaluateCodeString + "\n"
                                                                 + "_parsedTemplateString_ += '";
                                        }
                                        return match;
                                    });
                                    return resultString;
                                })()
                    +     "';" + "\n"
                    + "}" + "\n"
                    + "return _parsedTemplateString_;"
            // }
            );
        } catch (err) {
            error(err.message += ' while compiling template:\n', templateString);
            return function () {return '';};
        }
        return renderFunction;
    }

    function escapeString (string) {
        var escapeCharacters = {
              "'":          "\\'"
            , "\\":         "\\\\"
            , "\r":         "\\r"
            , "\n":        "\\n"
            , "\u2028": "\\u2028"
            , "\u2029": "\\u2029"
        };
        return string.replace(/\\|'|\r|\n|\u2028|\u2029/g, function (match) {return escapeCharacters[match];});
    }

    function escapeHtmlFunction (html) {
        var escapeCharacters = {
              "&":  "&amp;"
            , "<":  "&lt;"
            , ">":  "&gt;"
            , "\"": "&quot;"
            , "'":   "&#x27;"
            , "`":  "&#x60;"
        };
        return String(html).replace(/(?:&|<|>|"|'|`)/g, function (match) {return escapeCharacters[match];});
    }

    function block (templatePartString, settingsObject, dataObject) {
        return new Template(templatePartString, settingsObject).render(dataObject);
    }

    return {
          Template: Template
        , block: block
    };

});

var template = '<div><p>Total: <%- value %></p><% var a = 2; while (a--) { %><%- block(innerTemplate, {delimiters: ["{{", "}}"]}, {innerValue: innerValue}) %><% } %></div>'
    , innerTemplate = '<p>Last: {{- innerValue }}</p>';

var result = new TPL.Template(template).render({value: '<span>15</span>', block: TPL.block, innerTemplate: innerTemplate, innerValue: '<span>20</span>'});

var fragment = document.createDocumentFragment()
    , element = document.createElement('div');

element.innerHTML = result;
fragment.appendChild(element);

document.getElementsByTagName('body')[0].appendChild(fragment);

</script>
</body>
</html>

JavaScript-шаблонизатор Template аналог EJS. Версия 2.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>TPL Engine</title>
</head>
<body>
<div id="output"></div>
<script type="text/javascript">

;(function (global, factory) {
    if (typeof define === "function" && define.amd) {
        define(factory);
    } else if (typeof module === "object" && typeof module.exports === "object") {
        module.exports = factory();
    } else {
        global.TPL = factory();
    }
})(this, function () {

    // Usage:
    // console.log( new TPL.Template("<div>Total: {{= value }}<div>", {delimiters: ["{{", "}}"]}).render({value: "<span>15</span>"}) );
    // console.log( new TPL.Template("<div>Total: {{- block('<div>{{= value }}</div>', {delimiters: ['{{', '}}']}, {value: value}) }}<div>", {delimiters: ["{{", "}}"]}).render({block: TPL.block, value: "<span>15</span>"}) );

    // Options:
    // delimiters       - characters to use for open and close delimiters
    // rmWhitespace - remove whitespace between HTML-tags

    // Tags:
    // <%    - scriptlet tag for control-flow, no output
    // <%% - outputs raw template part
    // <%-   - outputs the raw unescaped value into the template
    // <%=  - outputs the HTML escaped value into the template
    // <%#  - comment tag, no execution, no output
    // %>    - plain ending tag

    // Block:
    // "<div><%- block('<div><%= value %></div>', {delimiters: ['<%', '%>']}, {value: outerValue}) %></div>"

    function Template (templateString, settingsObject) {
        var delimiters = ["<%", "%>"]
            , rmWhitespace = false; // removeWhitespace
        if (settingsObject) {
            if ('delimiters' in settingsObject) {delimiters = settingsObject.delimiters;}
            if ('rmWhitespace' in settingsObject) {rmWhitespace = settingsObject.rmWhitespace;}
        }
        var renderFunction = createRenderFunction(templateString, delimiters);
        return {render: function (dataObject) {
            var renderedHTML = renderFunction(dataObject, escapeHtmlFunction);
            if (rmWhitespace === true) {
                renderedHTML = removeSpaces(renderedHTML);
            }
            return renderedHTML;
        }};
    }

    function removeSpaces (renderedHTML) {
        return renderedHTML.replace(/\r/g, "") // Remove carriage return symbols
                                       .replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "") // Trim space: \s, BOM: \uFEFF, NBSP: \xA0
                                       .replace(/\s+(<[^%(?:span|b|strong|i|em|del|s|u|a|sub|sup|small|abbr|img)])/g, "$1") // Remove spaces between start tag and content
                                       .replace(/([^%(?:span|b|strong|i|em|del|s|u|a|sub|sup|small|abbr|img)]>)\s+/g, "$1") // Remove spaces between end tag and content
                                       .replace(/\s+/g, " "); // Remove spaces inside content
    }

    function createMatchRegExp (delimiters) { // delimiters = ["<%", "%>"]
        var openDelimiter = delimiters[0] // <%
            , closeDelimiter = delimiters[1] // %>
            , stringInsideDelimiters = "([\\s\\S]+?)"         // any string
            , tags = {
                  rawTemplatePart:            openDelimiter + "%" + stringInsideDelimiters + closeDelimiter // <%% "<div><%= value %></div>" %> --> "<div><%= value %></div>"
                , rawHtmlUnescapedValue: openDelimiter + "-" + stringInsideDelimiters + closeDelimiter   // <%- "1<br />2" %> --> "1<br />2"
                , htmlEscapedValue:          openDelimiter + "=" + stringInsideDelimiters + closeDelimiter  // <%= "1<br />2" %> --> "1&lt;br /&gt;2"
                , comment:                      openDelimiter + "#" + stringInsideDelimiters + closeDelimiter  // <%# any comment %> --> ""
                , evaluate:                       openDelimiter + stringInsideDelimiters + closeDelimiter           // <% if (value === 1) { %>one<% } %> --> "one"
              }
            , matchRegExp = new RegExp([
                  tags.rawTemplatePart // Important!!! Must be first!
                , tags.rawHtmlUnescapedValue
                , tags.htmlEscapedValue
                , tags.comment
                , tags.evaluate // Important!!! Must be last!
              ].join("|") + "|$", "g"); // /<%%([\s\S]+?)%>|<%-([\s\S]+?)%>|<%=([\s\S]+?)%>|<%#([\s\S]+?)%>|<%([\s\S]+?)%>|$/g
        return matchRegExp;
    }

    function createRenderFunction (templateString, delimiters) {
        var matchRegExp = createMatchRegExp(delimiters)
            , renderFunction;
        try {
            renderFunction = new Function ("_dataObject_", "_escapeHtmlFunction_" // ) {
                     , "if (!_dataObject_) {_dataObject_ = {};}" + "\n"
                    + "var _parsedTemplateString_ = '';" + "\n"
                    + "with (_dataObject_) {" + "\n"
                    +     "_parsedTemplateString_ += '" + (function(){
                                    var resultString = ""
                                        , index = 0;
                                    templateString.replace(matchRegExp, function (
                                          match
                                        , rawTemplatePartCodeString
                                        , rawUnescapedValueCodeString
                                        , htmlEscapedValueCodeString
                                        , commentCodeString
                                        , evaluateCodeString
                                        , offset
                                    ) {
                                        resultString += escapeString(templateString.slice(index, offset));
                                        index = offset + match.length;
                                        if (rawTemplatePartCodeString) {
                                            resultString += delimiters[0] + escapeString(rawTemplatePartCodeString) + delimiters[1];
                                        } else if (rawUnescapedValueCodeString) {
                                            resultString += "'" + "\n"
                                                                 + "+ (function(){" + "\n"
                                                                 +          "var _value_ = " + rawUnescapedValueCodeString + ";" + "\n"
                                                                 +          "if (_value_ === undefined || _value_ === null) {return '';} else {return _value_;}" + "\n"
                                                                 +     "})()" + "\n"
                                                                 + "+ '";
                                        } else if (htmlEscapedValueCodeString) {
                                            resultString += "'" + "\n"
                                                                 + "+ (function(){" + "\n"
                                                                 +          "var _value_ = " + htmlEscapedValueCodeString + ";" + "\n"
                                                                 +          "if (_value_ === undefined || _value_ === null) {return '';} else {return _escapeHtmlFunction_(_value_);}" + "\n"
                                                                 +     "})()" + "\n"
                                                                 + "+ '";
                                        } else if (commentCodeString) {
                                            // Do nothing
                                        } else if (evaluateCodeString) {
                                            resultString += "';" + "\n"
                                                                 + evaluateCodeString + "\n"
                                                                 + "_parsedTemplateString_ += '";
                                        }
                                        return match;
                                    });
                                    return resultString;
                                })()
                    +     "';" + "\n"
                    + "}" + "\n"
                    + "return _parsedTemplateString_;"
            // }
            );
        } catch (error) {
            if (error instanceof SyntaxError) {error.message += " while compiling template: " + templateString;}
            throw error;
        }
        return renderFunction;
    }

    function escapeString (string) {
        var escapeCharacters = {
              "'":          "\\'"
            , "\\":         "\\\\"
            , "\r":         "\\r"
            , "\n":        "\\n"
            , "\u2028": "\\u2028"
            , "\u2029": "\\u2029"
        };
        return string.replace(/\\|'|\r|\n|\u2028|\u2029/g, function (match) {return escapeCharacters[match];});
    }

    function escapeHtmlFunction (html) {
        var escapeCharacters = {
              "&":  "&amp;"
            , "<":  "&lt;"
            , ">":  "&gt;"
            , "\"": "&quot;"
            , "'":   "&#x27;"
            , "`":  "&#x60;"
        };
        return String(html).replace(/(?:&|<|>|"|'|`)/g, function (match) {return escapeCharacters[match];});
    }

    function block (templatePartString, settingsObject, dataObject) {
        return new Template(templatePartString, settingsObject).render(dataObject);
    }

    return {
          Template: Template
        , block: block
    };

});

// Тесты

var template = '<div><p>Total: <%- value %></p><% var a = 2; while (a--) { %><%- block(innerTemplate, {delimiters: ["{{", "}}"]}, {innerValue: innerValue}) %><% } %></div>'
    , innerTemplate = '<p>Last: {{- innerValue }}</p>';

var result = new TPL.Template(template).render({value: '<span>15</span>', block: TPL.block, innerTemplate: innerTemplate, innerValue: '<span>20</span>'});

var fragment = document.createDocumentFragment()
    , element = document.createElement('div');

element.innerHTML = result;
fragment.appendChild(element);

document.getElementsByTagName('body')[0].appendChild(fragment);

</script>
</body>
</html>