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

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>

Комментариев нет:

Отправить комментарий