JavaScriptのテンプレートエンジンを供養します

外部ライブラリが使えない状況でJSのテンプレートエンジンが有ったら良いなと思って書いたら案件そのものが先送りになったので供養します。勢いで書いたので充分なテストが出来ていませんが、多分、LOOPやIFの入れ子もイケると思います。

```html
<script id="loop" type="text/html">
<div>
    <#LOOP name="list">
    <p>
       <#VAL name="val"  format="number">&nbsp;
       <#VAL name="date" format="date:MM/DD YYYY">
       <#IF expr="__AA_LOOP_IDX > 2">&nbsp;<#VAL name="__AA_LOOP_IDX"><#/IF>
   </p>
    <#/LOOP>
</div>
</script>

<script id="if" type="text/html">
<div><#IF name="case">exactly<#ELSE>boo!<#/IF></div>
</script>

<script id="else" type="text/html">
<div><#IF name="case">boo!<#ELSE>exactly<#/IF></div>
</script>

<h2>loop</h2>
<div id="loopResult"></div>


<h2>if</h2>
<div id="ifResult"></div>

<h2>else</h2>
<div id="elseResult"></div>

<script type="text/javascript" src="assets/js/Template.js"></script>
<script type="text/javascript">
(function($){
    $('loopResult').innerHTML = AA.Template.compile($('loop').innerHTML).run({list: [
        {'val':1001, 'date':'2015/01/01'},
        {'val':1002, 'date':'2015/01/02'},
        {'val':1003, 'date':'2015/01/03'},
        {'val':1004, 'date':'2015/01/04'},
        {'val':1002, 'date':'2015/01/02'},
        {'val':1003, 'date':'2015/01/03'},
        {'val':1002, 'date':'2015/01/02'},
        {'val':1002, 'date':'2015/01/02'},
        {'val':1002, 'date':'2015/01/02'},
        {'val':1002, 'date':'2015/01/02'},
        {'val':1002, 'date':'2015/01/02'},
        {'val':1002, 'date':'2015/01/02'},
        {'val':1002, 'date':'2015/01/02'},
        {'val':1002, 'date':'2015/01/02'},
        {'val':1002, 'date':'2015/01/02'},
        {'val':1003, 'date':'2015/01/03'},
        {'val':1003, 'date':'2015/01/03'},
        {'val':1003, 'date':'2015/01/03'},
        {'val':1003, 'date':'2015/01/03'},
        {'val':1003, 'date':'2015/01/03'},
        {'val':1003, 'date':'2015/01/03'},
        {'val':1003, 'date':'2015/01/03'},
        {'val':1003, 'date':'2015/01/03'},
        {'val':1003, 'date':'2015/01/03'}
    ]});
    $('ifResult').innerHTML   = AA.Template.compile($('if').innerHTML).run({"case": true});
    $('elseResult').innerHTML = AA.Template.compile($('else').innerHTML).run({"case": false});
})(function(id){return document.getElementById(id);});
</script>
```

Template.jsの中身

```js
var AA = window.AA || {};
AA.Template = {};
AA.Template.compile = function (text) {
    return new Tag(parse(text).stack);
    function parse (text, start, endTag) {
        start = start || 0;
        var stack = [];
        for (var i = start, l = text.length, buf = '', c = ''; i < l; i++) {
            c = text.substr(i, 1);
            if (text.substr(i, 3)=== '<#/') {
                if (buf !== '') stack.push(new TextNode(buf));
                for (i += 3, buf = '', c = ''; i < l; i++) {
                    c = text.substr(i, 1);
                    if (c === '>') break;
                    buf += c;
                }
                if (!endTag)        throw new SyntaxError('invalidTag: no expected,actual:' + buf);
                if (buf !== endTag) throw new SyntaxError('invalidTag: expected:' + endTag + ', actual:' + buf);
                return {"stack": stack, pos: i};
            } else if (text.substr(i, 2) === '<#') {
                if (buf !== '')  stack.push(new TextNode(buf));
                for (i += 2, buf = '<#'; i < l; i++) {
                    c = text.substr(i, 1);
                    if (c === '"' || c === "'") {
                        for (buf += quote = c, i += 1; i < l; i++) {
                            c = text.substr(i, 1);
                            if (c === '\\')  {
                                if (substr(i + 1, 1) === "n") {
                                    buf += "\n";
                                } else if (substr(i + 1, 1) === "\\") {
                                    buf += "\\";
                                } else {
                                    buf += substr(i + 1, 1);
                                }
                                i++;
                            } else if (quote === c) {
                                buf += c;
                                break;
                            } else {
                                buf += c;
                            }
                        }
                    } else if (c === ">") {
                        buf += c;
                        var tag = buildTag(buf);
                        buf = '';
                        if (tag.getEndTag()) {
                            var result = parse(text, i + 1, tag.getEndTag());
                            tag.stack = result.stack;
                            i = result.pos;
                        }
                        stack.push(tag);
                        break;
                    } else {
                        buf += c;
                    }
                }
            } else {
                buf += c;
            }
        }
        if (buf) {
            stack.push(new TextNode(buf));
        }
        return {"stack": stack, pos: text.length};
    }

    function buildTag (text) {
        var i = 0, name = '', attributes = {};
        for (i = 0, buf = '', c = '', l = text.length; i < l; i++) {
            c = text.substr(i, 1);
            if (c === ' ' || c === "\n" || c === '>') break;
            buf += c;
        }
        name = buf;
        for (buf = '', c = '', l = text.length; i < l; i++) {
            while (i < l && text.substr(i++, 1) === ' ');
            var label = '';
            for (i--; i < l; i++)  {
                c = text.substr(i, 1);
                if (c === ' ' || c === '=' || c === '>') break;
                buf += c;
            }
            label = buf;
            var value = '', buf = '', quote = '';
            while (i < l && text.substr(i++, 1) === ' ');
            for (; i < l; i++)  {
                c = text.substr(i, 1);
                if (buf === '' && (c === '"' || c === "'")) {
                    quote = c;
                    for (i += 1; i < l; i++) {
                        c = text.substr(i, 1);
                        if (c === "\\") {
                            switch (text.substr(i + 1, 1)) {
                                case "n":  buf += "\n"; i+=2; break;
                                case "\\": buf += "\\"; i+=2; break;
                                default:   buf += text.substr(i + 1, 1); i+=2; break;
                            }
                        } else if (c === quote) {
                            break;
                        } else {
                            buf += c;
                        }
                    }
                } else if (c === ' ' || c === '>') {
                    break;
                } else {
                    buf += c;
                }
            }
            attributes[label] = buf;
            buf = '';
        }

        if (name === '<#IF') {
            return new IFTag (text, attributes);
        } else if (name === '<#LOOP') {
            return new LoopTag(text, attributes);
        } else if (name === '<#ELSE') {
            return new ElseTag(text, attributes);
        } else if (name === '<#VAL') {
            return new ValueTag(text, attributes);
        } else {
            return new TextNode(text);
        }
    }

    function Tag (stack) {
        this.stack = stack;
        this.getEndTag = function () {
            return null;
        }

        this.run = function (parameter) {
            if (Object.prototype.toString.call(parameter) === '[object Array]') {
                throw new TypeError('parameter is array');
            }
            var result = '';
            for (var i = 0, l = this.stack.length; i < l; i++)  {
                result += this.stack[i].run(parameter);
            }
            return result;
        }
        return this;
    }

    function IFTag (text, attributes) {
        this.originalText = text;
        this.attributes = attributes;
        this.stack = []
        this.getEndTag = function () {
            return 'IF';
        }

        this.run = function (parameter) {
            var current = this.evaluate(parameter, this.attributes);
            var isSkip = (function(bool) {
                return bool ?
                    function (a) {return (a instanceof ElseTag);}:
                    function (b) {return !(b instanceof ElseTag);};
            })(current);
            var result = '';
            for (var i = 0, l = this.stack.length; i < l; i++)  {
                if (isSkip(this.stack[i])) continue;
                result += this.stack[i].run(parameter);
            }
            return result;
        }

        this.evaluate = function (parameter, attributes) {
            var result = false;
            if (attributes['expr']) {
                if (!attributes['expr'].match(/^ *([a-zA-Z_-]+) *(===|==|>=|<=|<|>) *(.+?) *$/)) {
                    throw new SyntaxError(
                        'expected pattern :"^ *([a-zA-Z_-]+) *(===|==|>=|<=|<|>) *(.+?) *$", actual expr:' +
                        attributes['expr']
                    );
                }
                var comperator = {
                    "left": RegExp.$1,
                    "operator": RegExp.$2,
                    "right": RegExp.$3
                };
                comperator.left  = parameter[comperator.left]  ? parameter[comperator.left]  : this.resolveQuote(comperator.left);
                comperator.right = parameter[comperator.right] ? parameter[comperator.right] : this.resolveQuote(comperator.right);
                result = this.compare(comperator);
            } else if (attributes['name']) {
                result = parameter[attributes['name']];
            }
            return result;
        }

        this.compare = function (obj) {
            if (obj.operator === '===' && obj.left === obj.right) return true;
            if (obj.operator === '=='  && obj.left == obj.right)  return true;
            if (obj.operator === '<='  && (obj.left - 0) <= (obj.right - 0)) return true;
            if (obj.operator === '>='  && (obj.left - 0) >= (obj.right - 0)) return true;
            if (obj.operator === '<'   && (obj.left - 0) <  (obj.right - 0)) return true;
            if (obj.operator === '>'   && (obj.left - 0) >  (obj.right - 0)) return true;
            return false;
        }

        // TODO: too hack
        this.resolveQuote = function (str) {
            return str.replace(/^["']|["']$/g).replace(/\\n/g,"\n").replace(/\\/g,'');
        }
        return this;
    }

    function ElseTag (text, attributes) {
        this.originalText = text;
        this.attributes = attributes;
        this.stack = []

        this.getEndTag = function () {
            return 'IF';
        }

        this.run = function (parameter) {
            var result = '';
            for (var i = 0, l = this.stack.length; i < l; i++)  {
                result += this.stack[i].run(parameter);
            }
            return result;
        }
        return this;
    }

    function LoopTag (text, attributes) {
        this.originalText = text;
        this.attributes = attributes;
        this.stack = []
        this.getEndTag = function () {
            return 'LOOP';
        }

        this.run = function (parameter) {
            var result = '';
            var list = parameter[this.attributes['name']];
            if (!list || Object.prototype.toString.call(list) !== '[object Array]')
                throw new TypeError('invalid arguments.. wants array');

            for (var i = 0, l = list.length; i < l; i++) {
                var current = list[i];
                if (Object.prototype.toString.call(current) === '[object Array]') {
                    throw new TypeError('parameter is array... at loop(' + i + ')' );
                }
                current['__AA_LOOP_IDX'] = i;
                for (var j = 0, l2 = this.stack.length; j < l2; j++) {
                    result += this.stack[j].run(current);
                }
            }
            return result;
        }
        return this;
    }

    function ValueTag (text, attributes) {
        this.originalText = text;
        this.attributes = attributes;
        this.stack = [];
        this.getEndTag = function () {
            return null;
        }

        this.run = function (parameter) {
            var result = parameter[this.attributes['name']];
            if (result === undefined)
                throw new TypeError('invalid argument:' + this.attributes['name']);
            if (this.attributes['format']) {
                result = this.format (result, this.attributes['format']);
            }
            if (this.attributes['escape'] !== "false") {
                result = this.escape (result);
            }
            return result;
        }

        this.escape =  function (val) {
            var div = document.createElement('div');
            div.appendChild(document.createTextNode(val));
            return div.innerHTML;
        }

        this.format = function (val, format) {
            if (format === 'number') {
                return String(val).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,');
            } else if (format.match(/^date:?(.*)$/)) {
                var obj = new Date(val);
                if (RegExp.$1)  {
                    return String(RegExp.$1).
                        replace(/YYYY/g, String(obj.getYear() + 1900)).
                        replace(/YY/g, String(obj.getYear() + 1900).replace(/^\d{2}/,'')).
                        replace(/MM/g, String(obj.getMonth() + 1)).
                        replace(/DD/g, String(obj.getDate())).
                        replace(/HH/g, String(obj.getHours())).
                        replace(/II/g, String(obj.getMinutes())).
                        replace(/SS/g, String(obj.getSeconds()));
                } else {
                    return obj.toLocaDateString();
                }
            } else {
                throw new SyntaxError ('invalid format: expects: (number|date:(dateFormat)), actual: ' + format);
            }
        }
        return this;
    }

    function TextNode (text) {
        this.originalText = text;
        this.getEndTag = function () {
            return null;
        }
        this.run = function (parameter) {
            return this.originalText;
        }
        return this;
    }
}
```