Как получить список свойств из моего шаблона EJS?

Я сохраняю строки ответов в базе данных в форме EJS и заполняю данные в Node. То, что я хочу сделать, - это использовать любое свойство, которое я хочу, независимо от того, из какой модели оно исходит, а затем в Node, async/ожидание этих моделей, когда у меня есть шаблон, основанный на том, какие свойства требуются.

Итак, если у меня есть шаблон, например:

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

Я хочу посмотреть этот шаблон и извлечь что-то вроде:

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

Или что-то в этом роде.

Ответы

Ответ 1

Если вы просто хотите вытащить простые вещи, такие как user.firstName, тогда запуск RegExp над файлом EJS, вероятно, так же хорош, как и любой другой. Скорее всего, вы ищете конкретный и известный набор объектов и свойств, чтобы вы могли нацеливать их конкретно, а не пытаться извлечь все возможные объекты/свойства.

В более общем случае дело осложняется очень быстро. Что-то вроде этого очень сложно обрабатывать:

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

Это глупый пример, но это только верхушка этого конкретного айсберга. В то время как user считывается из locals и представляет собой объект, представляющий интерес, u может быть примерно чем угодно, и мы не можем легко рисовать линии, соединяющие firstName и user через u. Точно так же что-то вроде forEach в массиве или for/in на объекте будет быстро сделать невозможным связывание свойств с соответствующей записью locals.

Однако, что мы можем сделать, это идентифицировать записи в locals или, по крайней мере, что-то очень близкое к этому.

Используя пример <%= user.firstName %>, идентификатор user может ссылаться на одну из трех вещей. Во-первых, это может быть запись в locals. Во-вторых, это может быть свойство глобального объекта. В-третьих, это может быть переменная, созданная в рамках шаблона (например, u в предыдущем примере).

Мы не можем сказать разницу между первыми двумя случаями, но есть вероятность, что вы можете легко отделить глобалы. Такие вещи, как console и Math, могут быть идентифицированы и отброшены.

Третий случай - сложный, говорящий разницу между записью в locals и переменной в шаблоне, как в этом примере:

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

В этом случае users поступает непосредственно из locals, но user не является. Для нас это требует анализа переменных видимости, аналогичного тому, который был найден в среде IDE.

Итак, вот что я пробовал:

  • Скомпилируйте шаблон в JS.
  • Разберите JS в AST, используя esprima.
  • Пройдите по AST, чтобы найти все идентификаторы. Если они кажутся глобальными, они возвращаются. Здесь "глобальный" означает либо подлинно глобальный, либо что они являются записью в объекте locals. EJS использует with (locals) {...} внутренне, поэтому нет способа узнать, какой он есть.

Я творчески назвал результат ejsprima.

Я не пытался поддерживать все опции, поддерживаемые EJS, поэтому, если вы используете пользовательские разделители или строгий режим, это не сработает. (Если вы используете строгий режим, вы должны явно писать locals.user.firstName в своем шаблоне в любом случае, что будет кричать, если это произойдет через RegExp). Он не будет пытаться выполнять любые вызовы include.

Я был бы очень удивлен, если бы где-нибудь не было ошибок, даже с некоторым фрагментом основного синтаксиса JS, но я проверил все неприятные случаи, о которых я мог думать. Включены тестовые примеры.

EJS, используемый в основной демонстрации, можно найти в верхней части HTML. Я включил бесплатный пример "глобальной записи", чтобы продемонстрировать, на что они похожи, но я бы предположил, что они не то, что вы обычно хотели бы. Интересный бит - это раздел reads.

Я разработал это против esprima 4, но лучшая версия 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 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>

Ответ 2

К сожалению, EJS не предоставляет возможности для разбора и извлечения имен переменных из шаблона. Он имеет метод compile, но этот метод возвращает функцию , которая может использоваться для визуализации строки по шаблону. Но вам нужно получить промежуточный результат, чтобы извлечь переменные.

Вы можете сделать это с помощью системы шаблонов усов.

Условные разделители Mustache {{ }}. Вы можете заменить их на пользовательские разделители. К сожалению, Усы не позволяют определять несколько разделителей (например, <%= %> и <% %>), поэтому, если вы попытаетесь скомпилировать шаблон, содержащий несколько разделителей, Mustache выдает ошибку. Возможное решение для этого - создать функцию, которая принимает шаблон и разделители, и заменяет все остальные делиметры на что-то нейтральное. И вызвать эту функцию для каждой пары разделителей:

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

Ниже простого варианта, который работает только с одной парой разделителей:

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']

Ответ 3

Я думаю, что res.locals - это то, что вы ищете в этом случае,

app.set('view engine', 'ejs');
var myUser = {
  user :
    {
    username: 'myUser',
    lastName: 'userLastName',
    location: 'USA'
  }
}

app.use(function(req, res, next){
  res.locals = myUser;
  next();
})

app.get('/', function(req, res){
  res.render('file.ejs');
})

В любом файле ejs мы можем использовать свойства по своему усмотрению,

  <body>
    <h3>The User</h3>
    <p><%=user.username%></p>
    <p><%=user.lastName%></p>
    <p><%=user.location%></p>
  </body>