web-dev-qa-db-ja.com

EJSテンプレートからプロパティのリストを取得するにはどうすればよいですか?

応答文字列をEJSフォームのデータベースに保存し、ノードにデータを入力しています。私がやりたいのは、どのモデルから来たとしても、必要なanyプロパティを使用できるようにすることです。次に、Nodeで、テンプレートを取得したら、プロパティが何であるかに基づいて、それらのモデルを非同期/待機します。必須。

したがって、次のようなテンプレートがある場合:

"Hello <%=user.firstName%>."

そのテンプレートを見て、次のようなものを抽出できるようにしたいと思います。

ejsProperties = ["user", "user.firstName"]

またはそのようなもの。

15
Individual11

user.firstNameのような単純なものを引き出したいだけの場合は、EJSファイルに対してRegExpを実行するのがおそらく他の方法と同じくらい良い方法です。可能性のあるすべてのオブジェクト/プロパティを抽出しようとするのではなく、特定の既知のオブジェクトとプロパティのセットを探して、それらを具体的にターゲットにできる可能性があります。

より一般的なケースでは、物事は非常に迅速に困難になります。このようなものを処理するのは非常に難しいです:

<% var u = user; %><%= u.firstName %>

これはばかげた例ですが、それはその特定の氷山の一角にすぎません。 userlocalsから読み取られており、対象のオブジェクトですが、uはほぼすべてであり、firstNameuseru経由で結ぶ線を簡単に描くことはできません。同様に、配列のforEachやオブジェクトのfor/inのようなものは、プロパティを適切なlocalsエントリにリンクすることをすぐに不可能にします。

ただし、私たちにできることは、localsのエントリ、または少なくともそれに非常に近いものを識別することです。

<%= user.firstName %>の例を使用すると、識別子userは3つのもののいずれかを参照できます。まず、それはlocalsのエントリである可能性があります。次に、グローバルオブジェクトのプロパティである可能性があります。第3に、テンプレートのスコープ内で作成された変数である可能性があります(前の例のuのように)。

最初の2つのケースの違いを実際に区別することはできませんが、グローバルを非常に簡単に分離できる可能性があります。 consoleMathのようなものは、識別して破棄できます。

3番目のケースはトリッキーなケースで、次の例のように、localsのエントリとテンプレートの変数の違いを示します。

<% users.forEach(function(user) { %>
    <%= user.firstName %>
<% }); %>

この場合、userslocalsから直接取得されますが、userはそうではありません。それを解決するには、IDEに見られるものと同様の可変スコープ分析が必要です。

だからこれが私が試したものです:

  1. テンプレートをJSにコンパイルします。
  2. JSをAST using esprima
  3. ASTをウォークして、すべての識別子を見つけます。グローバルであると思われる場合は、返されます。ここで、「グローバル」は、真にグローバルであるか、localsオブジェクトのエントリであることを意味します。 with (locals) {...}内部的には、それがどれであるかを知る方法は実際にはありません。

私は想像力を働かせて結果をejsprimaと呼びました。

EJSがサポートするすべてのオプションをサポートしようとはしていません。そのため、カスタム区切り文字または厳密モードを使用している場合は機能しません。 (厳密モードを使用している場合は、とにかくテンプレートにlocals.user.firstNameを明示的に記述する必要があります。これは、代わりにRegExpを介して実行することを求めています)。 include呼び出しを追跡しようとはしません。

基本的なJS構文の一部を使用しても、どこかにバグが潜んでいないとしたら、非常に驚​​きますが、考えられるすべての厄介なケースをテストしました。テストケースが含まれています。

メインデモで使用されるEJSは、HTMLの上部にあります。それらがどのように見えるかを示すためだけに、「グローバル書き込み」の無償の例を含めましたが、それらは通常必要なものではないと思います。興味深いのはreadsセクションです。

私はこれをesprima4に対して開発しましたが、私が見つけた最高のCDNバージョンは2.7.3です。テストはすべてまだ合格しているので、それほど重要ではないようです。

スニペットのJSセクションに含めた唯一のコードは、「ejsprima」自体用です。それをNodeで実行するには、それをコピーし、上下を微調整してエクスポートを修正する必要があります。

// Begin 'ejsprima'
(function(exports) {
//var esprima = require('esprima');

// Simple EJS compiler that throws away the HTML sections and just retains the JavaScript code
exports.compile = function(tpl) {
    // Extract the tags
    var tags = tpl.match(/(<%(?!%)[\s\S]*?[^%]%>)/g);

    return tags.map(function(tag) {
        var parse = tag.match(/^(<%[=\-_#]?)([\s\S]*?)([-_]?%>)$/);

        switch (parse[1]) {
            case '<%=':
            case '<%-':
                return ';(' + parse[2] + ');';
            case '<%#':
                return '';
            case '<%':
            case '<%_':
                return parse[2];
        }

        throw new Error('Assertion failure');
    }).join('\n');
};

// Pull out the identifiers for all 'global' reads and writes
exports.extractGlobals = function(tpl) {
    var ast = tpl;

    if (typeof tpl === 'string') {
        // Note: This should be parseScript in esprima 4
        ast = esprima.parse(tpl);
    }

    // Uncomment this line to dump out the AST
    //console.log(JSON.stringify(ast, null, 2));

    var refs = this.processAst(ast);

    var reads = {};
    var writes = {};

    refs.forEach(function(ref) {
        ref.globalReads.forEach(function(key) {
            reads[key] = true;
        });
    });

    refs.forEach(function(ref) {
        ref.globalWrites.forEach(function(key) {
            writes[key] = true;
        })
    });

    return {
        reads: Object.keys(reads),
        writes: Object.keys(writes)
    };
};

exports.processAst = function(obj) {
    var baseScope = {
        lets: Object.create(null),
        reads: Object.create(null),
        writes: Object.create(null),

        vars: Object.assign(Object.create(null), {
            // These are all local to the rendering function
            arguments: true,
            escapeFn: true,
            include: true,
            rethrow: true
        })
    };

    var scopes = [baseScope];

    processNode(obj, baseScope);

    scopes.forEach(function(scope) {
        scope.globalReads = Object.keys(scope.reads).filter(function(key) {
            return !scope.vars[key] && !scope.lets[key];
        });

        scope.globalWrites = Object.keys(scope.writes).filter(function(key) {
            return !scope.vars[key] && !scope.lets[key];
        });

        // Flatten out the prototype chain - none of this is actually used by extractGlobals so we could just skip it
        var allVars = Object.keys(scope.vars).concat(Object.keys(scope.lets)),
            vars = {},
            lets = {};

        // An identifier can either be a var or a let not both... need to ensure inheritance sees the right one by
        // setting the alternative to false, blocking any inherited value
        for (var key in scope.lets) {
            if (hasOwn(scope.lets)) {
                scope.vars[key] = false;
            }
        }

        for (key in scope.vars) {
            if (hasOwn(scope.vars)) {
                scope.lets[key] = false;
            }
        }

        for (key in scope.lets) {
            if (scope.lets[key]) {
                lets[key] = true;
            }
        }

        for (key in scope.vars) {
            if (scope.vars[key]) {
                vars[key] = true;
            }
        }

        scope.lets = Object.keys(lets);
        scope.vars = Object.keys(vars);
        scope.reads = Object.keys(scope.reads);

        function hasOwn(obj) {
            return obj[key] && (Object.prototype.hasOwnProperty.call(obj, key));
        }
    });

    return scopes;
    
    function processNode(obj, scope) {
        if (!obj) {
            return;
        }
    
        if (Array.isArray(obj)) {
            obj.forEach(function(o) {
                processNode(o, scope);
            });
    
            return;
        }

        switch(obj.type) {
            case 'Identifier':
                scope.reads[obj.name] = true;
                return;

            case 'VariableDeclaration':
                obj.declarations.forEach(function(declaration) {
                    // Separate scopes for var and let/const
                    processLValue(declaration.id, scope, obj.kind === 'var' ? scope.vars : scope.lets);
                    processNode(declaration.init, scope);
                });

                return;

            case 'AssignmentExpression':
                processLValue(obj.left, scope, scope.writes);

                if (obj.operator !== '=') {
                    processLValue(obj.left, scope, scope.reads);
                }

                processNode(obj.right, scope);

                return;

            case 'UpdateExpression':
                processLValue(obj.argument, scope, scope.reads);
                processLValue(obj.argument, scope, scope.writes);

                return;

            case 'FunctionDeclaration':
            case 'FunctionExpression':
            case 'ArrowFunctionExpression':
                var newScope = {
                    lets: Object.create(scope.lets),
                    reads: Object.create(null),
                    vars: Object.create(scope.vars),
                    writes: Object.create(null)
                };

                scopes.Push(newScope);

                obj.params.forEach(function(param) {
                    processLValue(param, newScope, newScope.vars);
                });

                if (obj.id) {
                    // For a Declaration the name is accessible outside, for an Expression it is only available inside
                    if (obj.type === 'FunctionDeclaration') {
                        scope.vars[obj.id.name] = true;
                    }
                    else {
                        newScope.vars[obj.id.name] = true;
                    }
                }

                processNode(obj.body, newScope);

                return;

            case 'BlockStatement':
            case 'CatchClause':
            case 'ForInStatement':
            case 'ForOfStatement':
            case 'ForStatement':
                // Create a new block scope
                scope = {
                    lets: Object.create(scope.lets),
                    reads: Object.create(null),
                    vars: scope.vars,
                    writes: Object.create(null)
                };

                scopes.Push(scope);

                if (obj.type === 'CatchClause') {
                    processLValue(obj.param, scope, scope.lets);
                    processNode(obj.body, scope);

                    return;
                }

                break; // Don't return
        }

        Object.keys(obj).forEach(function(key) {
            var value = obj[key];
    
            // Labels for break/continue
            if (key === 'label') {
                return;
            }

            if (key === 'left') {
                if (obj.type === 'ForInStatement' || obj.type === 'ForOfStatement') {
                    if (obj.left.type !== 'VariableDeclaration') {
                        processLValue(obj.left, scope, scope.writes);
                        return;
                    }
                }
            }

            if (obj.computed === false) {
                // MemberExpression, ClassExpression & Property
                if (key === 'property' || key === 'key') {
                    return;
                }
            }
    
            if (value && typeof value === 'object') {
                processNode(value, scope);
            }
        });
    }
    
    // An l-value is something that can appear on the left of an = operator. It could be a simple identifier, as in
    // `var a = 7;`, or something more complicated, like a destructuring. There's a big difference between how we handle
    // `var a = 7;` and `a = 7;` and the 'target' is used to control which of these two scenarios we are in.
    function processLValue(obj, scope, target) {
        nextLValueNode(obj);
    
        function nextLValueNode(obj) {
            switch (obj.type) {
                case 'Identifier':
                    target[obj.name] = true;
                break;
    
                case 'ObjectPattern':
                    obj.properties.forEach(function(property) {
                        if (property.computed) {
                            processNode(property.key, scope);
                        }
    
                        nextLValueNode(property.value);
                    });
                break;
    
                case 'ArrayPattern':
                    obj.elements.forEach(function(element) {
                        nextLValueNode(element);
                    });
                break;
    
                case 'RestElement':
                    nextLValueNode(obj.argument);
                break;
    
                case 'AssignmentPattern':
                    nextLValueNode(obj.left);
                    processNode(obj.right, scope);
                break;
    
                case 'MemberExpression':
                    processNode(obj, scope);
                break;
    
                default: throw new Error('Unknown type: ' + obj.type);
            }
        }
    }
};
})(window.ejsprima = {});
<body>
<script type="text/ejs" id="demo-ejs">
    <body>
        <h1>Welcome <%= user.name %></h1>
        <% if (admin) { %>
            <a href="/admin">Admin</a>
        <% } %>
        <ul>
            <% friends.forEach(function(friend, index) { %>
                <li class="<%= index === 0 ? "first" : "" %> <%= friend.name === selected ? "selected" : "" %>"><%= friend.name %></li>
            <% }); %>
        </ul>
        <%
            console.log(user);
            
            exampleWrite = 'some value';
        %>
    </body>
</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/esprima/2.7.3/esprima.min.js"></script>
<script>
function runTests() {
    var assertValues = function(tpl, reads, writes) {
        var program = ejsprima.compile(tpl);

        var values = ejsprima.extractGlobals(program);

        reads = reads || [];
        writes = writes || [];

        reads.sort();
        writes.sort();

        if (!equal(reads, values.reads)) {
            console.log('Mismatched reads', reads, values.reads, tpl);
        }

        if (!equal(writes, values.writes)) {
            console.log('Mismatched writes', writes, values.writes, tpl);
        }

        function equal(arr1, arr2) {
            return JSON.stringify(arr1.slice().sort()) === JSON.stringify(arr2.slice().sort());
        }
    };

    assertValues('<% console.log("hello") %>', ['console']);
    assertValues('<% a = 7; %>', [], ['a']);
    assertValues('<% var a = 7; %>');
    assertValues('<% let a = 7; %>');
    assertValues('<% const a = 7; %>');
    assertValues('<% a = 7; var a; %>');
    assertValues('<% var a = 7, b = a + 1, c = d; %>', ['d']);
    assertValues('<% try{}catch(a){a.log()} %>');
    assertValues('<% try{}catch(a){a = 9;} %>');
    assertValues('<% try{}catch(a){b.log()} %>', ['b']);
    assertValues('<% try{}catch(a){}a; %>', ['a']);
    assertValues('<% try{}catch(a){let b;}b; %>', ['b']);
    assertValues('<% try{}finally{let a;}a; %>', ['a']);
    assertValues('<% (function(a){a();b();}) %>', ['b']);
    assertValues('<% (function(a){a();b = 8;}) %>', [], ['b']);
    assertValues('<% (function(a){a();a = 8;}) %>');
    assertValues('<% (function name(a){}) %>');
    assertValues('<% (function name(a){});name(); %>', ['name']);
    assertValues('<% function name(a){} %>');
    assertValues('<% function name(a){}name(); %>');
    assertValues('<% a.map(b => b + c); %>', ['a', 'c']);
    assertValues('<% a.map(b => b + c); b += 6; %>', ['a', 'b', 'c'], ['b']);

    assertValues('<% var {a} = {b: c}; %>', ['c']);
    assertValues('<% var {a} = {b: c}; a(); %>', ['c']);
    assertValues('<% var {[d]: a} = {b: c}; a(); %>', ['c', 'd']);
    assertValues('<% var {[d]: a} = {b: c}; a(); %>', ['c', 'd']);
    assertValues('<% var {[d + e]: a} = {b: c}; a(); %>', ['c', 'd', 'e']);
    assertValues('<% var {[d + e[f = g]]: a} = {b: c}; a(); %>', ['c', 'd', 'e', 'g'], ['f']);
    assertValues('<% ({a} = {b: c}); %>', ['c'], ['a']);
    assertValues('<% ({a: d.e} = {b: c}); %>', ['c', 'd']);
    assertValues('<% ({[a]: d.e} = {b: c}); %>', ['a', 'c', 'd']);
    assertValues('<% var {a = 7} = {}; %>', []);
    assertValues('<% var {a = b} = {}; %>', ['b']);
    assertValues('<% var {[a]: b = (c + d)} = {}; %>', ['a', 'c', 'd']);

    assertValues('<% var [a] = [b]; a(); %>', ['b']);
    assertValues('<% var [{a}] = [b]; a(); %>', ['b']);
    assertValues('<% [{a}] = [b]; %>', ['b'], ['a']);
    assertValues('<% [...a] = [b]; %>', ['b'], ['a']);
    assertValues('<% let [...a] = [b]; %>', ['b']);
    assertValues('<% var [a = b] = [c]; %>', ['b', 'c']);
    assertValues('<% var [a = b] = [c], b; %>', ['c']);

    assertValues('<% ++a %>', ['a'], ['a']);
    assertValues('<% ++a.b %>', ['a']);
    assertValues('<% var a; ++a %>');
    assertValues('<% a += 1 %>', ['a'], ['a']);
    assertValues('<% var a; a += 1 %>');

    assertValues('<% a.b = 7 %>', ['a']);
    assertValues('<% a["b"] = 7 %>', ['a']);
    assertValues('<% a[b] = 7 %>', ['a', 'b']);
    assertValues('<% a[b + c] = 7 %>', ['a', 'b', 'c']);
    assertValues('<% var b; a[b + c] = 7 %>', ['a', 'c']);
    assertValues('<% a in b; %>', ['a', 'b']);
    assertValues('<% "a" in b; %>', ['b']);
    assertValues('<% "a" in b.c; %>', ['b']);

    assertValues('<% if (a === b) {c();} %>', ['a', 'b', 'c']);
    assertValues('<% if (a = b) {c();} else {d = e} %>', ['b', 'c', 'e'], ['a', 'd']);

    assertValues('<% a ? b : c %>', ['a', 'b', 'c']);
    assertValues('<% var a = b ? c : d %>', ['b', 'c', 'd']);

    assertValues('<% for (a in b) {} %>', ['b'], ['a']);
    assertValues('<% for (var a in b.c) {} %>', ['b']);
    assertValues('<% for (let {a} in b) {} %>', ['b']);
    assertValues('<% for ({a} in b) {} %>', ['b'], ['a']);
    assertValues('<% for (var {[a + b]: c} in d) {} %>', ['a', 'b', 'd']);
    assertValues('<% for ({[a + b]: c} in d) {} %>', ['a', 'b', 'd'], ['c']);
    assertValues('<% for (var a in b) {a = a + c;} %>', ['b', 'c']);
    assertValues('<% for (const a in b) console.log(a); %>', ['b', 'console']);
    assertValues('<% for (let a in b) console.log(a); %>', ['b', 'console']);
    assertValues('<% for (let a in b) {let b = 5;} %>', ['b']);
    assertValues('<% for (let a in b) {let b = 5;} console.log(a); %>', ['console', 'a', 'b']);
    assertValues('<% for (const a in b) {let b = 5;} console.log(a); %>', ['console', 'a', 'b']);
    assertValues('<% for (var a in b) {let b = 5;} console.log(a); %>', ['console', 'b']);

    assertValues('<% for (a of b) {} %>', ['b'], ['a']);
    assertValues('<% for (var a of b.c) {} %>', ['b']);
    assertValues('<% for (let {a} of b) {} %>', ['b']);
    assertValues('<% for ({a} of b) {} %>', ['b'], ['a']);
    assertValues('<% for (var {[a + b]: c} of d) {} %>', ['a', 'b', 'd']);
    assertValues('<% for ({[a + b]: c} of d) {} %>', ['a', 'b', 'd'], ['c']);
    assertValues('<% for (var a of b) {a = a + c;} %>', ['b', 'c']);
    assertValues('<% for (const a of b) console.log(a); %>', ['b', 'console']);
    assertValues('<% for (let a of b) console.log(a); %>', ['b', 'console']);
    assertValues('<% for (let a of b) {let b = 5;} %>', ['b']);
    assertValues('<% for (let a of b) {let b = 5;} console.log(a); %>', ['console', 'a', 'b']);
    assertValues('<% for (const a of b) {let b = 5;} console.log(a); %>', ['console', 'a', 'b']);
    assertValues('<% for (var a of b) {let b = 5;} console.log(a); %>', ['console', 'b']);

    assertValues('<% for (var i = 0 ; i < 10 ; ++i) {} %>');
    assertValues('<% for (var i = 0 ; i < len ; ++i) {} %>', ['len']);
    assertValues('<% for (var i = 0, len ; i < len ; ++i) {} %>');
    assertValues('<% for (i = 0 ; i < len ; ++i) {} %>', ['i', 'len'], ['i']);
    assertValues('<% for ( ; i < len ; ++i) {} %>', ['i', 'len'], ['i']);
    assertValues('<% var i; for ( ; i < len ; ++i) {} %>', ['len']);
    assertValues('<% for (var i = 0 ; i < 10 ; ++i) {i += j;} %>', ['j']);
    assertValues('<% for (var i = 0 ; i < 10 ; ++i) {j += i;} %>', ['j'], ['j']);
    assertValues('<% for (const i = 0; i < 10 ; ++i) console.log(i); %>', ['console']);
    assertValues('<% for (let i = 0 ; i < 10 ; ++i) console.log(i); %>', ['console']);
    assertValues('<% for (let i = 0 ; i < len ; ++i) {let len = 5;} %>', ['len']);
    assertValues('<% for (let i = 0 ; i < len ; ++i) {let len = 5;} console.log(i); %>', ['console', 'i', 'len']);
    assertValues('<% for (var i = 0 ; i < len ; ++i) {let len = 5;} console.log(i); %>', ['console', 'len']);

    assertValues('<% while(++i){console.log(i);} %>', ['console', 'i'], ['i']);
    assertValues('<% myLabel:while(true){break myLabel;} %>');

    assertValues('<% var a = `Hello ${user.name}`; %>', ['user']);

    assertValues('<% this; null; true; false; NaN; undefined; %>', ['NaN', 'undefined']);

    // Scoping
    assertValues([
        '<%',
            'var a = 7, b;',
            'let c = 8;',
            'a = b + c - d;',
        
            '{',
                'let e = 6;',
                'f = g + e + b + c;',
            '}',
        '%>'
    ].join('\n'), ['d', 'g'], ['f']);
        
    assertValues([
        '<%',
            'var a = 7, b;',
            'let c = 8;',
            'a = b + c - d;',
        
            '{',
                'let e = 6;',
                'f = g + e + b + c;',
            '}',
        
            'e = c;',
        '%>'
    ].join('\n'), ['d', 'g'], ['e', 'f']);
        
    assertValues([
        '<%',
            'var a = 7, b;',
            'let c = 8;',
            'a = b + c - d;',
        
            '{',
                'var e = 6;',
                'f = g + e + b + c;',
            '}',
        
            'e = c;',
        '%>'
    ].join('\n'), ['d', 'g'], ['f']);
        
    assertValues([
        '<%',
            'var a;',
            'let b;',
            'const c = 0;',
        
            '{',
                'var d;',
                'let e;',
                'const f = 1;',
            '}',
        
            'var g = function h(i) {',
                'arguments.length;',
                'a(); b(); c(); d(); e(); f(); g(); h(); i();',
            '};',
        '%>'
    ].join('\n'), ['e', 'f']);
        
    assertValues([
        '<%',
            'var a;',
            'let b;',
            'const c = 0;',
        
            '{',
                'var d;',
                'let e;',
                'const f = 1;',
            '}',
        
            'var g = function h(i) {};',
            'arguments.length;',
            'a(); b(); c(); d(); e(); f(); g(); h(); i();',
        '%>'
    ].join('\n'), ['e', 'f', 'h', 'i']);
        
    assertValues([
        '<%',
            'var a;',
            'let b;',
            'const c = 0;',
        
            '{',
                'var d;',
                'let e;',
                'const f = 1;',
        
                'arguments.length;',
                'a(); b(); c(); d(); e(); f(); g(); h(); i();',
            '}',
        
            'var g = function h(i) {};',
        '%>'
    ].join('\n'), ['h', 'i']);
        
    assertValues([
        '<%',
            'var a;',
            'let b;',
            'const c = 0;',
        
            '{',
                'var d;',
                'let e;',
                'const f = 1;',
        
                'var g = function h(i) {',
                    'arguments.length;',
                    'a(); b(); c(); d(); e(); f(); g(); h(); i();',
                '};',
            '}',
        '%>'
    ].join('\n'));
        
    assertValues([
        '<%',
            'var a;',
            'let b;',
            'const c = 0;',
        
            'var g = function h(i) {',
                '{',
                    'var d;',
                    'let e;',
                    'const f = 1;',
                '}',
        
                'arguments.length;',
                'a(); b(); c(); d(); e(); f(); g(); h(); i();',
            '};',
        '%>'
    ].join('\n'), ['e', 'f']);
        
    assertValues([
        '<%',
            'var a;',
            'let b;',
            'const c = 0;',
        
            'var g = function h(i) {',
                '{',
                    'var d;',
                    'let e;',
                    'const f = 1;',
        
                    'arguments.length;',
                    'a(); b(); c(); d(); e(); f(); g(); h(); i();',
                '}',
            '};',
        '%>'
    ].join('\n'));
        
    // EJS parsing
    assertValues('Hello <%= user.name %>', ['user']);
    assertValues('Hello <%- user.name %>', ['user']);
    assertValues('Hello <%# user.name %>');
    assertValues('Hello <%_ user.name _%>', ['user']);
    assertValues('Hello <%_ user.name _%>', ['user']);
    assertValues('Hello <%% console.log("<%= user.name %>") %%>', ['user']);
    assertValues('Hello <% console.log("<%% user.name %%>") %>', ['console']);
    assertValues('<% %><%a%>', ['a']);
    assertValues('<% %><%=a%>', ['a']);
    assertValues('<% %><%-a_%>', ['a']);
    assertValues('<% %><%__%>');
        
    assertValues([
        '<body>',
            '<h1>Welcome <%= user.name %></h1>',
            '<% if (admin) { %>',
                '<a href="/admin">Admin</a>',
            '<% } %>',
            '<ul>',
                '<% friends.forEach(function(friend, index) { %>',
                    '<li class="<%= index === 0 ? "first" : "" %> <%= friend.name === selected ? "selected" : "" %>"><%= friend.name %></li>',
                '<% }); %>',
            '</ul>',
        '</body>'
    ].join('\n'), ['user', 'admin', 'friends', 'selected']);
        
    assertValues([
        '<body>',
            '<h1>Welcome <%= user.name %></h1>',
            '<% if (admin) { %>',
                '<a href="/admin">Admin</a>',
            '<% } %>',
            '<ul>',
                '<% friends.forEach(function(user, index) { %>',
                    '<li class="<%= index === 0 ? "first" : "" %> <%= user.name === selected ? "selected" : "" %>"><%= user.name %></li>',
                '<% }); %>',
            '</ul>',
        '</body>'
    ].join('\n'), ['user', 'admin', 'friends', 'selected']);
    
    console.log('Tests complete, if you didn\'t see any other messages then they passed');
}
</script>
<script>
function runDemo() {
    var script = document.getElementById('demo-ejs'),
        tpl = script.innerText,
        js = ejsprima.compile(tpl);
        
    console.log(ejsprima.extractGlobals(js));
}
</script>
<button onclick="runTests()">Run Tests</button>
<button onclick="runDemo()">Run Demo</button>
</body>

したがって、要約すると、これにより、localsに必要なすべてのエントリを正確に識別できるようになると思います。一般に、これらのオブジェクト内で使用されているプロパティを特定することはできません。精度の低下を気にしない場合は、RegExpを使用することをお勧めします。

5
skirtle

残念ながら、EJSは、テンプレートから変数名を解析および抽出する機能を提供していません。 compileメソッドがありますが、このメソッドは function を返します。これは、テンプレートごとに文字列をレンダリングするために使用できます。ただし、変数を抽出するには、中間結果を取得する必要があります。

Mustacheテンプレートシステム を使用してそれを行うことができます。

口ひげのデフォルトの区切り文字は{{ }}。それらを置き換えることができます カスタム区切り文字に 。残念ながら、Mustacheでは複数の区切り文字を定義できません(<%= %>および<% %>たとえば)、したがって、複数の区切り文字を含むテンプレートをコンパイルしようとすると、Mustacheはエラーをスローします。そのための可能な解決策は、テンプレートと区切り文字を受け入れ、他のすべての区切り文字をニュートラルなものに置き換える関数を作成することです。そして、区切り文字のペアごとにこの関数を呼び出します。

let vars = [];
vars.concat(parseTemplate(template, ['<%', '%>']));
vars.concat(parseTemplate(template, ['<%=', '%>']));
...
let uniqVars = _.uniq(vars);

1組の区切り文字でのみ機能する単純なバリアントの下:

let _        = require('lodash');
let Mustache = require('Mustache');

let template = 'Hello <%= user.firstName %> <%= user.lastName %> <%= date %>';
let customTags = ['<%=', '%>'];

let tokens = Mustache.parse(template, customTags);
let vars = _.chain(tokens)
  .filter(token => token[0] === 'name')
  .map(token => {
    let v = token[1].split('.');
    return v;
  })
  .flatten()
  .uniq()
  .value();

console.log(vars); // prints ['user', 'firstName', 'lastName', 'date']
1
alexmac