/ *
* EJS Embedded JavaScript templates
* Copyright 2112 Matthew Eernisse ( mde @ fleegix . org )
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
*
* /
'use strict' ;
/ * *
* @ file Embedded JavaScript templating engine .
* @ author Matthew Eernisse < mde @ fleegix . org >
* @ author Tiancheng "Timothy" Gu < timothygu99 @ gmail . com >
* @ project EJS
* @ license { @ link http : //www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0}
* /
/ * *
* EJS internal functions .
*
* Technically this "module" lies in the same file as { @ link module : ejs } , for
* the sake of organization all the private functions re grouped into this
* module .
*
* @ module ejs - internal
* @ private
* /
/ * *
* Embedded JavaScript templating engine .
*
* @ module ejs
* @ public
* /
var fs = require ( 'fs' ) ;
var path = require ( 'path' ) ;
var utils = require ( './utils' ) ;
var scopeOptionWarned = false ;
var _VERSION _STRING = require ( '../package.json' ) . version ;
var _DEFAULT _DELIMITER = '%' ;
var _DEFAULT _LOCALS _NAME = 'locals' ;
var _REGEX _STRING = '(<%%|%%>|<%=|<%-|<%_|<%#|<%|%>|-%>|_%>)' ;
var _OPTS = [ 'cache' , 'filename' , 'delimiter' , 'scope' , 'context' ,
'debug' , 'compileDebug' , 'client' , '_with' , 'root' , 'rmWhitespace' ,
'strict' , 'localsName' ] ;
var _TRAILING _SEMCOL = /;\s*$/ ;
var _BOM = /^\uFEFF/ ;
/ * *
* EJS template function cache . This can be a LRU object from lru - cache NPM
* module . By default , it is { @ link module : utils . cache } , a simple in - process
* cache that grows continuously .
*
* @ type { Cache }
* /
exports . cache = utils . cache ;
/ * *
* Name of the object containing the locals .
*
* This variable is overriden by { @ link Options } ` .localsName ` if it is not
* ` undefined ` .
*
* @ type { String }
* @ public
* /
exports . localsName = _DEFAULT _LOCALS _NAME ;
/ * *
* Get the path to the included file from the parent file path and the
* specified path .
*
* @ param { String } name specified path
* @ param { String } filename parent file path
* @ param { Boolean } isDir parent file path whether is directory
* @ return { String }
* /
exports . resolveInclude = function ( name , filename , isDir ) {
var dirname = path . dirname ;
var extname = path . extname ;
var resolve = path . resolve ;
var includePath = resolve ( isDir ? filename : dirname ( filename ) , name ) ;
var ext = extname ( name ) ;
if ( ! ext ) {
includePath += '.ejs' ;
}
return includePath ;
} ;
/ * *
* Get the path to the included file by Options
*
* @ param { String } path specified path
* @ param { Options } options compilation options
* @ return { String }
* /
function getIncludePath ( path , options ) {
var includePath ;
if ( path . charAt ( 0 ) == '/' ) {
includePath = exports . resolveInclude ( path . replace ( /^\/*/ , '' ) , options . root || '/' , true ) ;
}
else {
if ( ! options . filename ) {
throw new Error ( '`include` use relative path requires the \'filename\' option.' ) ;
}
includePath = exports . resolveInclude ( path , options . filename ) ;
}
return includePath ;
}
/ * *
* Get the template from a string or a file , either compiled on - the - fly or
* read from cache ( if enabled ) , and cache the template if needed .
*
* If ` template ` is not set , the file specified in ` options.filename ` will be
* read .
*
* If ` options.cache ` is true , this function reads the file from
* ` options.filename ` so it must be set prior to calling this function .
*
* @ memberof module : ejs - internal
* @ param { Options } options compilation options
* @ param { String } [ template ] template source
* @ return { ( TemplateFunction | ClientFunction ) }
* Depending on the value of ` options.client ` , either type might be returned .
* @ static
* /
function handleCache ( options , template ) {
var func ;
var filename = options . filename ;
var hasTemplate = arguments . length > 1 ;
if ( options . cache ) {
if ( ! filename ) {
throw new Error ( 'cache option requires a filename' ) ;
}
func = exports . cache . get ( filename ) ;
if ( func ) {
return func ;
}
if ( ! hasTemplate ) {
template = fs . readFileSync ( filename ) . toString ( ) . replace ( _BOM , '' ) ;
}
}
else if ( ! hasTemplate ) {
// istanbul ignore if: should not happen at all
if ( ! filename ) {
throw new Error ( 'Internal EJS error: no file name or template '
+ 'provided' ) ;
}
template = fs . readFileSync ( filename ) . toString ( ) . replace ( _BOM , '' ) ;
}
func = exports . compile ( template , options ) ;
if ( options . cache ) {
exports . cache . set ( filename , func ) ;
}
return func ;
}
/ * *
* Get the template function .
*
* If ` options.cache ` is ` true ` , then the template is cached .
*
* @ memberof module : ejs - internal
* @ param { String } path path for the specified file
* @ param { Options } options compilation options
* @ return { ( TemplateFunction | ClientFunction ) }
* Depending on the value of ` options.client ` , either type might be returned
* @ static
* /
function includeFile ( path , options ) {
var opts = utils . shallowCopy ( { } , options ) ;
opts . filename = getIncludePath ( path , opts ) ;
return handleCache ( opts ) ;
}
/ * *
* Get the JavaScript source of an included file .
*
* @ memberof module : ejs - internal
* @ param { String } path path for the specified file
* @ param { Options } options compilation options
* @ return { Object }
* @ static
* /
function includeSource ( path , options ) {
var opts = utils . shallowCopy ( { } , options ) ;
var includePath ;
var template ;
includePath = getIncludePath ( path , opts ) ;
template = fs . readFileSync ( includePath ) . toString ( ) . replace ( _BOM , '' ) ;
opts . filename = includePath ;
var templ = new Template ( template , opts ) ;
templ . generateSource ( ) ;
return {
source : templ . source ,
filename : includePath ,
template : template
} ;
}
/ * *
* Re - throw the given ` err ` in context to the ` str ` of ejs , ` filename ` , and
* ` lineno ` .
*
* @ implements RethrowCallback
* @ memberof module : ejs - internal
* @ param { Error } err Error object
* @ param { String } str EJS source
* @ param { String } filename file name of the EJS file
* @ param { String } lineno line number of the error
* @ static
* /
function rethrow ( err , str , filename , lineno ) {
var lines = str . split ( '\n' ) ;
var start = Math . max ( lineno - 3 , 0 ) ;
var end = Math . min ( lines . length , lineno + 3 ) ;
// Error context
var context = lines . slice ( start , end ) . map ( function ( line , i ) {
var curr = i + start + 1 ;
return ( curr == lineno ? ' >> ' : ' ' )
+ curr
+ '| '
+ line ;
} ) . join ( '\n' ) ;
// Alter exception message
err . path = filename ;
err . message = ( filename || 'ejs' ) + ':'
+ lineno + '\n'
+ context + '\n\n'
+ err . message ;
throw err ;
}
/ * *
* Copy properties in data object that are recognized as options to an
* options object .
*
* This is used for compatibility with earlier versions of EJS and Express . js .
*
* @ memberof module : ejs - internal
* @ param { Object } data data object
* @ param { Options } opts options object
* @ static
* /
function cpOptsInData ( data , opts ) {
_OPTS . forEach ( function ( p ) {
if ( typeof data [ p ] != 'undefined' ) {
opts [ p ] = data [ p ] ;
}
} ) ;
}
/ * *
* Compile the given ` str ` of ejs into a template function .
*
* @ param { String } template EJS template
*
* @ param { Options } opts compilation options
*
* @ return { ( TemplateFunction | ClientFunction ) }
* Depending on the value of ` opts.client ` , either type might be returned .
* @ public
* /
exports . compile = function compile ( template , opts ) {
var templ ;
// v1 compat
// 'scope' is 'context'
// FIXME: Remove this in a future version
if ( opts && opts . scope ) {
if ( ! scopeOptionWarned ) {
console . warn ( '`scope` option is deprecated and will be removed in EJS 3' ) ;
scopeOptionWarned = true ;
}
if ( ! opts . context ) {
opts . context = opts . scope ;
}
delete opts . scope ;
}
templ = new Template ( template , opts ) ;
return templ . compile ( ) ;
} ;
/ * *
* Render the given ` template ` of ejs .
*
* If you would like to include options but not data , you need to explicitly
* call this function with ` data ` being an empty object or ` null ` .
*
* @ param { String } template EJS template
* @ param { Object } [ data = { } ] template data
* @ param { Options } [ opts = { } ] compilation and rendering options
* @ return { String }
* @ public
* /
exports . render = function ( template , d , o ) {
var data = d || { } ;
var opts = o || { } ;
// No options object -- if there are optiony names
// in the data, copy them to options
if ( arguments . length == 2 ) {
cpOptsInData ( data , opts ) ;
}
return handleCache ( opts , template ) ( data ) ;
} ;
/ * *
* Render an EJS file at the given ` path ` and callback ` cb(err, str) ` .
*
* If you would like to include options but not data , you need to explicitly
* call this function with ` data ` being an empty object or ` null ` .
*
* @ param { String } path path to the EJS file
* @ param { Object } [ data = { } ] template data
* @ param { Options } [ opts = { } ] compilation and rendering options
* @ param { RenderFileCallback } cb callback
* @ public
* /
exports . renderFile = function ( ) {
var args = Array . prototype . slice . call ( arguments ) ;
var filename = args . shift ( ) ;
var cb = args . pop ( ) ;
var data = args . shift ( ) || { } ;
var opts = args . pop ( ) || { } ;
var result ;
// Don't pollute passed in opts obj with new vals
opts = utils . shallowCopy ( { } , opts ) ;
// No options object -- if there are optiony names
// in the data, copy them to options
if ( arguments . length == 3 ) {
// Express 4
if ( data . settings && data . settings [ 'view options' ] ) {
cpOptsInData ( data . settings [ 'view options' ] , opts ) ;
}
// Express 3 and lower
else {
cpOptsInData ( data , opts ) ;
}
}
opts . filename = filename ;
try {
result = handleCache ( opts ) ( data ) ;
}
catch ( err ) {
return cb ( err ) ;
}
return cb ( null , result ) ;
} ;
/ * *
* Clear intermediate JavaScript cache . Calls { @ link Cache # reset } .
* @ public
* /
exports . clearCache = function ( ) {
exports . cache . reset ( ) ;
} ;
function Template ( text , opts ) {
opts = opts || { } ;
var options = { } ;
this . templateText = text ;
this . mode = null ;
this . truncate = false ;
this . currentLine = 1 ;
this . source = '' ;
this . dependencies = [ ] ;
options . client = opts . client || false ;
options . escapeFunction = opts . escape || utils . escapeXML ;
options . compileDebug = opts . compileDebug !== false ;
options . debug = ! ! opts . debug ;
options . filename = opts . filename ;
options . delimiter = opts . delimiter || exports . delimiter || _DEFAULT _DELIMITER ;
options . strict = opts . strict || false ;
options . context = opts . context ;
options . cache = opts . cache || false ;
options . rmWhitespace = opts . rmWhitespace ;
options . root = opts . root ;
options . localsName = opts . localsName || exports . localsName || _DEFAULT _LOCALS _NAME ;
if ( options . strict ) {
options . _with = false ;
}
else {
options . _with = typeof opts . _with != 'undefined' ? opts . _with : true ;
}
this . opts = options ;
this . regex = this . createRegex ( ) ;
}
Template . modes = {
EVAL : 'eval' ,
ESCAPED : 'escaped' ,
RAW : 'raw' ,
COMMENT : 'comment' ,
LITERAL : 'literal'
} ;
Template . prototype = {
createRegex : function ( ) {
var str = _REGEX _STRING ;
var delim = utils . escapeRegExpChars ( this . opts . delimiter ) ;
str = str . replace ( /%/g , delim ) ;
return new RegExp ( str ) ;
} ,
compile : function ( ) {
var src ;
var fn ;
var opts = this . opts ;
var prepended = '' ;
var appended = '' ;
var escape = opts . escapeFunction ;
if ( ! this . source ) {
this . generateSource ( ) ;
prepended += ' var __output = [], __append = __output.push.bind(__output);' + '\n' ;
if ( opts . _with !== false ) {
prepended += ' with (' + opts . localsName + ' || {}) {' + '\n' ;
appended += ' }' + '\n' ;
}
appended += ' return __output.join("");' + '\n' ;
this . source = prepended + this . source + appended ;
}
if ( opts . compileDebug ) {
src = 'var __line = 1' + '\n'
+ ' , __lines = ' + JSON . stringify ( this . templateText ) + '\n'
+ ' , __filename = ' + ( opts . filename ?
JSON . stringify ( opts . filename ) : 'undefined' ) + ';' + '\n'
+ 'try {' + '\n'
+ this . source
+ '} catch (e) {' + '\n'
+ ' rethrow(e, __lines, __filename, __line);' + '\n'
+ '}' + '\n' ;
}
else {
src = this . source ;
}
if ( opts . debug ) {
console . log ( src ) ;
}
if ( opts . client ) {
src = 'escape = escape || ' + escape . toString ( ) + ';' + '\n' + src ;
if ( opts . compileDebug ) {
src = 'rethrow = rethrow || ' + rethrow . toString ( ) + ';' + '\n' + src ;
}
}
if ( opts . strict ) {
src = '"use strict";\n' + src ;
}
try {
fn = new Function ( opts . localsName + ', escape, include, rethrow' , src ) ;
}
catch ( e ) {
// istanbul ignore else
if ( e instanceof SyntaxError ) {
if ( opts . filename ) {
e . message += ' in ' + opts . filename ;
}
e . message += ' while compiling ejs' ;
}
throw e ;
}
if ( opts . client ) {
fn . dependencies = this . dependencies ;
return fn ;
}
// Return a callable function which will execute the function
// created by the source-code, with the passed data as locals
// Adds a local `include` function which allows full recursive include
var returnedFn = function ( data ) {
var include = function ( path , includeData ) {
var d = utils . shallowCopy ( { } , data ) ;
if ( includeData ) {
d = utils . shallowCopy ( d , includeData ) ;
}
return includeFile ( path , opts ) ( d ) ;
} ;
return fn . apply ( opts . context , [ data || { } , escape , include , rethrow ] ) ;
} ;
returnedFn . dependencies = this . dependencies ;
return returnedFn ;
} ,
generateSource : function ( ) {
var opts = this . opts ;
if ( opts . rmWhitespace ) {
// Have to use two separate replace here as `^` and `$` operators don't
// work well with `\r`.
this . templateText =
this . templateText . replace ( /\r/g , '' ) . replace ( /^\s+|\s+$/gm , '' ) ;
}
// Slurp spaces and tabs before <%_ and after _%>
this . templateText =
this . templateText . replace ( /[ \t]*<%_/gm , '<%_' ) . replace ( /_%>[ \t]*/gm , '_%>' ) ;
var self = this ;
var matches = this . parseTemplateText ( ) ;
var d = this . opts . delimiter ;
if ( matches && matches . length ) {
matches . forEach ( function ( line , index ) {
var opening ;
var closing ;
var include ;
var includeOpts ;
var includeObj ;
var includeSrc ;
// If this is an opening tag, check for closing tags
// FIXME: May end up with some false positives here
// Better to store modes as k/v with '<' + delimiter as key
// Then this can simply check against the map
if ( line . indexOf ( '<' + d ) === 0 // If it is a tag
&& line . indexOf ( '<' + d + d ) !== 0 ) { // and is not escaped
closing = matches [ index + 2 ] ;
if ( ! ( closing == d + '>' || closing == '-' + d + '>' || closing == '_' + d + '>' ) ) {
throw new Error ( 'Could not find matching close tag for "' + line + '".' ) ;
}
}
// HACK: backward-compat `include` preprocessor directives
if ( ( include = line . match ( /^\s*include\s+(\S+)/ ) ) ) {
opening = matches [ index - 1 ] ;
// Must be in EVAL or RAW mode
if ( opening && ( opening == '<' + d || opening == '<' + d + '-' || opening == '<' + d + '_' ) ) {
includeOpts = utils . shallowCopy ( { } , self . opts ) ;
includeObj = includeSource ( include [ 1 ] , includeOpts ) ;
if ( self . opts . compileDebug ) {
includeSrc =
' ; (function(){' + '\n'
+ ' var __line = 1' + '\n'
+ ' , __lines = ' + JSON . stringify ( includeObj . template ) + '\n'
+ ' , __filename = ' + JSON . stringify ( includeObj . filename ) + ';' + '\n'
+ ' try {' + '\n'
+ includeObj . source
+ ' } catch (e) {' + '\n'
+ ' rethrow(e, __lines, __filename, __line);' + '\n'
+ ' }' + '\n'
+ ' ; }).call(this)' + '\n' ;
} else {
includeSrc = ' ; (function(){' + '\n' + includeObj . source +
' ; }).call(this)' + '\n' ;
}
self . source += includeSrc ;
self . dependencies . push ( exports . resolveInclude ( include [ 1 ] ,
includeOpts . filename ) ) ;
return ;
}
}
self . scanLine ( line ) ;
} ) ;
}
} ,
parseTemplateText : function ( ) {
var str = this . templateText ;
var pat = this . regex ;
var result = pat . exec ( str ) ;
var arr = [ ] ;
var firstPos ;
while ( result ) {
firstPos = result . index ;
if ( firstPos !== 0 ) {
arr . push ( str . substring ( 0 , firstPos ) ) ;
str = str . slice ( firstPos ) ;
}
arr . push ( result [ 0 ] ) ;
str = str . slice ( result [ 0 ] . length ) ;
result = pat . exec ( str ) ;
}
if ( str ) {
arr . push ( str ) ;
}
return arr ;
} ,
scanLine : function ( line ) {
var self = this ;
var d = this . opts . delimiter ;
var newLineCount = 0 ;
function _addOutput ( ) {
if ( self . truncate ) {
// Only replace single leading linebreak in the line after
// -%> tag -- this is the single, trailing linebreak
// after the tag that the truncation mode replaces
// Handle Win / Unix / old Mac linebreaks -- do the \r\n
// combo first in the regex-or
line = line . replace ( /^(?:\r\n|\r|\n)/ , '' ) ;
self . truncate = false ;
}
else if ( self . opts . rmWhitespace ) {
// Gotta be more careful here.
// .replace(/^(\s*)\n/, '$1') might be more appropriate here but as
// rmWhitespace already removes trailing spaces anyway so meh.
line = line . replace ( /^\n/ , '' ) ;
}
if ( ! line ) {
return ;
}
// Preserve literal slashes
line = line . replace ( /\\/g , '\\\\' ) ;
// Convert linebreaks
line = line . replace ( /\n/g , '\\n' ) ;
line = line . replace ( /\r/g , '\\r' ) ;
// Escape double-quotes
// - this will be the delimiter during execution
line = line . replace ( /"/g , '\\"' ) ;
self . source += ' ; __append("' + line + '")' + '\n' ;
}
newLineCount = ( line . split ( '\n' ) . length - 1 ) ;
switch ( line ) {
case '<' + d :
case '<' + d + '_' :
this . mode = Template . modes . EVAL ;
break ;
case '<' + d + '=' :
this . mode = Template . modes . ESCAPED ;
break ;
case '<' + d + '-' :
this . mode = Template . modes . RAW ;
break ;
case '<' + d + '#' :
this . mode = Template . modes . COMMENT ;
break ;
case '<' + d + d :
this . mode = Template . modes . LITERAL ;
this . source += ' ; __append("' + line . replace ( '<' + d + d , '<' + d ) + '")' + '\n' ;
break ;
case d + d + '>' :
this . mode = Template . modes . LITERAL ;
this . source += ' ; __append("' + line . replace ( d + d + '>' , d + '>' ) + '")' + '\n' ;
break ;
case d + '>' :
case '-' + d + '>' :
case '_' + d + '>' :
if ( this . mode == Template . modes . LITERAL ) {
_addOutput ( ) ;
}
this . mode = null ;
this . truncate = line . indexOf ( '-' ) === 0 || line . indexOf ( '_' ) === 0 ;
break ;
default :
// In script mode, depends on type of tag
if ( this . mode ) {
// If '//' is found without a line break, add a line break.
switch ( this . mode ) {
case Template . modes . EVAL :
case Template . modes . ESCAPED :
case Template . modes . RAW :
if ( line . lastIndexOf ( '//' ) > line . lastIndexOf ( '\n' ) ) {
line += '\n' ;
}
}
switch ( this . mode ) {
// Just executing code
case Template . modes . EVAL :
this . source += ' ; ' + line + '\n' ;
break ;
// Exec, esc, and output
case Template . modes . ESCAPED :
this . source += ' ; __append(escape(' +
line . replace ( _TRAILING _SEMCOL , '' ) . trim ( ) + '))' + '\n' ;
break ;
// Exec and output
case Template . modes . RAW :
this . source += ' ; __append(' +
line . replace ( _TRAILING _SEMCOL , '' ) . trim ( ) + ')' + '\n' ;
break ;
case Template . modes . COMMENT :
// Do nothing
break ;
// Literal <%% mode, append as raw output
case Template . modes . LITERAL :
_addOutput ( ) ;
break ;
}
}
// In string mode, just add the output
else {
_addOutput ( ) ;
}
}
if ( self . opts . compileDebug && newLineCount ) {
this . currentLine += newLineCount ;
this . source += ' ; __line = ' + this . currentLine + '\n' ;
}
}
} ;
/ * *
* Escape characters reserved in XML .
*
* This is simply an export of { @ link module : utils . escapeXML } .
*
* If ` markup ` is ` undefined ` or ` null ` , the empty string is returned .
*
* @ param { String } markup Input string
* @ return { String } Escaped string
* @ public
* @ func
* * /
exports . escapeXML = utils . escapeXML ;
/ * *
* Express . js support .
*
* This is an alias for { @ link module : ejs . renderFile } , in order to support
* Express . js out - of - the - box .
*
* @ func
* /
exports . _ _express = exports . renderFile ;
// Add require support
/* istanbul ignore else */
if ( require . extensions ) {
require . extensions [ '.ejs' ] = function ( module , flnm ) {
var filename = flnm || /* istanbul ignore next */ module . filename ;
var options = {
filename : filename ,
client : true
} ;
var template = fs . readFileSync ( filename ) . toString ( ) ;
var fn = exports . compile ( template , options ) ;
module . _compile ( 'module.exports = ' + fn . toString ( ) + ';' , filename ) ;
} ;
}
/ * *
* Version of EJS .
*
* @ readonly
* @ type { String }
* @ public
* /
exports . VERSION = _VERSION _STRING ;
/* istanbul ignore if */
if ( typeof window != 'undefined' ) {
window . ejs = exports ;
}