",
5 | "description": "Embedded JavaScript HTML templates. An implementation of EJS focused on run-time performance, HTML syntax checking, minified HTML output and custom HTML elements.",
6 | "main": "./index.js",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/sitegui/ejs-html"
10 | },
11 | "keywords": [
12 | "ejs",
13 | "html",
14 | "template",
15 | "engine",
16 | "minification",
17 | "custom elements",
18 | "web components"
19 | ],
20 | "dependencies": {
21 | "source-map": "^0.7.0"
22 | },
23 | "license": "MIT",
24 | "engines": {
25 | "node": ">=6"
26 | },
27 | "scripts": {
28 | "test": "mocha test"
29 | },
30 | "devDependencies": {
31 | "mocha": "^5.0.0",
32 | "should": "^13.2.1"
33 | }
34 | }
--------------------------------------------------------------------------------
/test/compile.js:
--------------------------------------------------------------------------------
1 | /* globals describe, it*/
2 | 'use strict'
3 |
4 | let compile = require('..').compile,
5 | sourceMap = require('source-map')
6 | require('should')
7 |
8 | describe('compile', () => {
9 | it('should compile to run in the server', () => {
10 | compile('Hi <%=name.first%> <%=name.last%>!', {
11 | vars: ['name']
12 | })({
13 | name: {
14 | first: 'Gui',
15 | last: 'S'
16 | }
17 | }).should.be.equal('Hi Gui S!')
18 | })
19 |
20 | it('should compile to run in the client', () => {
21 | let code = compile.standAlone('Hi <%=name.first%> <%=name.last%>!', {
22 | vars: ['name']
23 | })
24 |
25 | // eslint-disable-next-line no-new-func
26 | let render = new Function('locals, renderCustom', code)
27 |
28 | render({
29 | name: {
30 | first: 'Gui',
31 | last: 'S'
32 | }
33 | }).should.be.equal('Hi Gui S!')
34 | })
35 |
36 | it('should support transformers', () => {
37 | compile('Hi Deep
', {
38 | transformer: function transformer(tokens) {
39 | tokens.forEach(token => {
40 | if (token.type === 'element') {
41 | if (token.name === 'i') {
42 | token.name = 'em'
43 | }
44 | transformer(token.children)
45 | }
46 | })
47 | }
48 | })().should.be.equal('Hi Deep
')
49 | })
50 |
51 | it('should add extended exception context', () => {
52 | let source = 'a\n<% throw new Error("hi") %>\nb',
53 | options = {
54 | filename: 'file.ejs'
55 | },
56 | message = `file.ejs:2
57 | 1 | a
58 | 2 >> | <% throw new Error("hi") %>
59 | 3 | b
60 |
61 | hi`
62 |
63 | // Non-stand alone compilation
64 | compile(source, options).should.throw(message)
65 |
66 | // Stand alone compilation
67 | let code = compile.standAlone(source, options)
68 | // eslint-disable-next-line no-new-func
69 | let render = new Function('locals, renderCustom', code)
70 | render.should.throw(message)
71 | })
72 |
73 | it('should not add extended exception context when compileDebug is false', () => {
74 | let source = 'a\n<% throw new Error("hi") %>\nb',
75 | options = {
76 | filename: 'file.ejs',
77 | compileDebug: false
78 | }
79 |
80 | // Non-stand alone compilation
81 | compile(source, options).should.throw('hi')
82 |
83 | // Stand alone compilation
84 | let code = compile.standAlone(source, options)
85 | // eslint-disable-next-line no-new-func
86 | let render = new Function('locals, renderCustom', code)
87 | render.should.throw('hi')
88 | })
89 |
90 | it('should compile custom tags when compileDebug is false', () => {
91 | compile('', {
92 | compileDebug: false
93 | })({}, () => 'hi').should.be.equal('hi')
94 | })
95 |
96 | it('should generate source map', function (done) {
97 | if (process.version < 'v8') {
98 | return this.skip()
99 | }
100 |
101 | let source = `Basic tags: <%= user %> <%- user %> <% if (true) { %>
102 |
103 |
104 | outside
105 | inside
106 |
107 | not named
108 | named
109 | <% } %>`,
110 | fn = compile(source, {
111 | sourceMap: true
112 | })
113 |
114 | fn.code.should.be.eql('"use strict";locals=locals||{};let __c=locals.__contents||{};let __o="Basic tags: "+(__l.s=__l.e=1,__e(user))+" "+(__l.s=__l.e=1,(user))+" ";__l.s=__l.e=1;if (true) {\n' +
115 | '__o+="\\n"+(__l.s=3,__l.e=6,(renderCustom("custom-tag",{"simple":"yes","active":true,"concat":"a and "+String((__l.s=__l.e=3,b)),"obj":(__l.s=__l.e=3,{a: 2}),__contents:{"":"\\noutside\\n","named":"inside"}},__l.s=3,__l.e=6)))+"\\n"+(__l.s=__l.e=7,(__c[""]&&/\\S/.test(__c[""])?__c[""]:"not named"))+"\\n"+(__l.s=__l.e=8,(__c[""]&&/\\S/.test(__c[""])?__c[""]:"named"))+"\\n";__l.s=__l.e=9;}\n' +
116 | 'return __o;')
117 | fn.map.should.be.eql('{"version":3,"sources":["ejs"],"names":[],"mappings":"uGAAgB,I,uBAAY,I,qBAAW,W;iEACO,C,mIACO,C,wBAAe,M,kOAM9D,C","file":"ejs.js"}')
118 | fn.mapWithCode.should.be.eql('{"version":3,"sources":["ejs"],"names":[],"mappings":"uGAAgB,I,uBAAY,I,qBAAW,W;iEACO,C,mIACO,C,wBAAe,M,kOAM9D,C","file":"ejs.js","sourcesContent":["Basic tags: <%= user %> <%- user %> <% if (true) { %>\\n\\t\\t\\t\\">\\n\\t\\t\\t\\" obj=\\"<%= {a: 2} %>\\">\\n\\t\\t\\t\\toutside\\n\\t\\t\\t\\tinside\\n\\t\\t\\t\\n\\t\\t\\tnot named\\n\\t\\t\\tnamed\\n\\t\\t\\t<% } %>"]}')
119 |
120 | new sourceMap.SourceMapConsumer(fn.map).then(consumer => {
121 | consumer.computeColumnSpans()
122 | let codes = []
123 | consumer.eachMapping(mapping => {
124 | if (!mapping.originalLine) {
125 | return
126 | }
127 | let length = mapping.lastGeneratedColumn - mapping.generatedColumn + 1,
128 | original = extract(source, mapping.originalLine, mapping.originalColumn, length),
129 | generated = extract(fn.code, mapping.generatedLine, mapping.generatedColumn, length)
130 | original.should.be.eql(generated)
131 | codes.push(original)
132 | })
133 |
134 | codes.should.be.eql([
135 | 'user',
136 | 'user',
137 | 'if (true) {',
138 | 'b',
139 | 'b',
140 | '{a: 2}',
141 | '}'
142 | ])
143 |
144 | done()
145 | })
146 |
147 | function extract(str, line, column, length) {
148 | return str.split('\n')[line - 1].slice(column, column + length)
149 | }
150 | })
151 |
152 | it('should check for placeholder emptiness the same way regardless compileDebug', () => {
153 | compile('out in')({}).should.be.eql('out in')
154 |
155 | compile('out in', {
156 | compileDebug: false
157 | })({}).should.be.eql('out in')
158 | })
159 |
160 | it('should check for boolean attributes the same way regardless compileDebug', () => {
161 | compile('')({
162 | x: false
163 | }).should.be.eql('')
164 |
165 | compile('', {
166 | compileDebug: false
167 | })({
168 | x: false
169 | }).should.be.eql('')
170 | })
171 | })
--------------------------------------------------------------------------------
/test/createCode.js:
--------------------------------------------------------------------------------
1 | /* globals describe, it*/
2 | 'use strict'
3 |
4 | let ejs = require('..'),
5 | prepareOptions = ejs._prepareOptions,
6 | createCode = ejs.compile._createCode
7 | require('should')
8 |
9 | describe('createCode', () => {
10 | it('should handle special static cases', () => {
11 | check('', {}, false, ['return""'])
12 | check('', {}, true, ['""'])
13 |
14 | check('Hello', {}, false, ['return"Hello"'])
15 | check('Hello', {}, true, ['"Hello"'])
16 | })
17 |
18 | it('should generate a single expression when possible', () => {
19 | check('Hello <%= locals.firstName %> <%= locals.lastName %>', {
20 | compileDebug: false
21 | }, false, [
22 | 'locals=locals||{};',
23 | 'let __c=locals.__contents||{};',
24 | 'return "Hello "+__e(locals.firstName)+" "+__e(locals.lastName);'
25 | ])
26 | check('Hello <%= locals.firstName %> <%= locals.lastName %>', {
27 | compileDebug: false
28 | }, true, ['"Hello "+__e(locals.firstName)+" "+__e(locals.lastName)'])
29 |
30 | check('<%- locals.firstName %> <%- locals.lastName %>', {
31 | compileDebug: false
32 | }, false, [
33 | 'locals=locals||{};',
34 | 'let __c=locals.__contents||{};',
35 | 'return ""+(locals.firstName)+" "+(locals.lastName);'
36 | ])
37 | check('<%- locals.firstName %> <%- locals.lastName %>', {
38 | compileDebug: false
39 | }, true, ['""+(locals.firstName)+" "+(locals.lastName)'])
40 | })
41 |
42 | it('should compile with debug markers', () => {
43 | check([
44 | 'First',
45 | '<%=a',
46 | '+b',
47 | '%> and <%= b %>'
48 | ].join('\n'), {}, false, [
49 | 'locals=locals||{};',
50 | 'let __c=locals.__contents||{};',
51 | 'return "First\\n"+(__l.s=2,__l.e=3,__e(a\n+b))+" and "+(__l.s=__l.e=4,__e(b));'
52 | ])
53 |
54 | check([
55 | 'First',
56 | '<%=',
57 | 'a',
58 | '%> and <%= b %>'
59 | ].join('\n'), {}, true, [
60 | '"First\\n"+(__l.s=__l.e=3,__e(a))+" and "+(__l.s=__l.e=4,__e(b))'
61 | ])
62 | })
63 |
64 | it('should generate multiple statements when needed', () => {
65 | check('<% if (true) { %>true<% } %>', {
66 | compileDebug: false
67 | }, false, [
68 | 'locals=locals||{};',
69 | 'let __c=locals.__contents||{};',
70 | 'let __o="";',
71 | 'if (true) {\n',
72 | '__o+="true";',
73 | '}\n',
74 | 'return __o;'
75 | ])
76 | check('<% if (true) { %>true<% } %>', {
77 | compileDebug: false
78 | }, true, [
79 | '(function(){',
80 | 'let __o="";',
81 | 'if (true) {\n',
82 | '__o+="true";',
83 | '}\n',
84 | 'return __o;',
85 | '})()'
86 | ])
87 |
88 | check('<% if (true) { %>true<% } %>', {}, false, [
89 | 'locals=locals||{};',
90 | 'let __c=locals.__contents||{};',
91 | 'let __o="";',
92 | '__l.s=__l.e=1;',
93 | 'if (true) {\n',
94 | '__o+="true";',
95 | '__l.s=__l.e=1;',
96 | '}\n',
97 | 'return __o;'
98 | ])
99 | check('<% if (true) { %>true<% } %>', {}, true, [
100 | '(function(){',
101 | 'let __o="";',
102 | '__l.s=__l.e=1;',
103 | 'if (true) {\n',
104 | '__o+="true";',
105 | '__l.s=__l.e=1;',
106 | '}\n',
107 | 'return __o;',
108 | '})()'
109 | ])
110 | })
111 |
112 | it('should compile in sloppy mode', () => {
113 | check('<%= name %>', {
114 | strictMode: false,
115 | compileDebug: false
116 | }, false, [
117 | 'locals=locals||{};',
118 | 'let __c=locals.__contents||{};',
119 | 'with(locals){',
120 | 'return __e(name);',
121 | '}'
122 | ])
123 | })
124 |
125 | it('should compile with explicit locals bindings', () => {
126 | check('<%= name %>', {
127 | vars: ['name'],
128 | compileDebug: false
129 | }, false, [
130 | 'locals=locals||{};',
131 | 'let __c=locals.__contents||{};',
132 | 'let name=locals.name;',
133 | 'return __e(name);'
134 | ])
135 |
136 | check('<%= a+b %>', {
137 | vars: ['a', 'b'],
138 | compileDebug: false
139 | }, false, [
140 | 'locals=locals||{};',
141 | 'let __c=locals.__contents||{};',
142 | 'let a=locals.a,b=locals.b;',
143 | 'return __e(a+b);'
144 | ])
145 | })
146 |
147 | it('should', () => {
148 | check('<% a() %> ', {}, true, [
149 | '(function(){',
150 | 'let __o="";',
151 | '__l.s=__l.e=1;',
152 | 'a()\n',
153 | '__o+=" ";',
154 | 'return __o;',
155 | '})()'
156 | ])
157 | })
158 | })
159 |
160 | function check(source, options, asInnerExpression, code) {
161 | options = prepareOptions(options)
162 | let tokens = ejs.reduce(ejs.parse(source), options)
163 |
164 | let builder = createCode(tokens, options, asInnerExpression)
165 | builder.build(source).code.should.be.equal(code.join(''))
166 | }
--------------------------------------------------------------------------------
/test/custom.js:
--------------------------------------------------------------------------------
1 | /* globals describe, it*/
2 | 'use strict'
3 |
4 | let compile = require('..').compile
5 | require('should')
6 |
7 | describe('custom', () => {
8 | let renderDialog
9 | it('should compile custom tag definition', () => {
10 | renderDialog = compile(`
11 |
12 | <%= title %>
13 | <% if (closable) { %>
14 |
X
15 | <% } %>
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
`, {
25 | vars: ['title', 'closable']
26 | })
27 | })
28 |
29 | let renderView
30 | it('should compile custom tag usage', () => {
31 | renderView = compile(`
32 | HTML Content
33 | `)
34 | })
35 |
36 | it('should render custom tags', () => {
37 | renderView({}, (name, locals) => {
38 | name.should.be.equal('custom-dialog')
39 | locals.should.be.eql({
40 | title: 'Wanna Know?',
41 | closable: true,
42 | __contents: {
43 | '': '\nHTML Content\n'
44 | }
45 | })
46 | return renderDialog(locals)
47 | }).should.be.equal(`
48 |
49 | Wanna Know?
50 |
X
51 |
52 |
53 |
HTML Content
54 |
55 |
56 |
57 |
58 |
59 |
`)
60 | })
61 |
62 | it('should support multiple and named placeholders', () => {
63 | check(`
64 |
65 |
66 | `, `
67 | outside
68 | inside
69 | `, `
70 | outside
71 |
72 | inside
73 |
74 | outside
75 |
76 | inside`)
77 | })
78 |
79 | it('should allow passing complex JS values', () => {
80 | let myObj = {}
81 | compile('', {
82 | vars: ['someObj']
83 | })({
84 | someObj: myObj
85 | }, (_, locals) => {
86 | locals.ref.should.be.equal(myObj)
87 | })
88 | })
89 |
90 | it('should use default placeholder when no content is provided', () => {
91 | check('default', '', 'default')
92 | check('default', '\n', 'default')
93 | check('default', ' ', 'default')
94 | check('default', 'x', 'x')
95 | })
96 |
97 | it('should not treat any boolean-like attribute as true', () => {
98 | check('<%=locals.bool%>', '', 'true')
99 | })
100 |
101 | it('should turn dash notation to camel case', () => {
102 | check('<%=locals.userName%>', '', 'gui')
103 | })
104 | })
105 |
106 | function check(customSource, source, expected) {
107 | compile(source)({}, (_, locals) => compile(customSource)(locals)).should.be.equal(expected)
108 | }
--------------------------------------------------------------------------------
/test/escape.js:
--------------------------------------------------------------------------------
1 | /* globals describe, it*/
2 | 'use strict'
3 |
4 | let escape = require('..').escape
5 | require('should')
6 |
7 | describe('escape', () => {
8 | it('should escape html', () => {
9 | escape.html('a && b << c >> d "" e \'\' f').should.be
10 | .equal('a && b << c >> d "" e '' f')
11 | })
12 |
13 | it('should escape js value to put inside double quotes', () => {
14 | escape.js('a \\\\ b \n\n c \r\r d "" e').should.be
15 | .equal('a \\\\\\\\ b \\n\\n c \\r\\r d \\"\\" e')
16 | })
17 | })
--------------------------------------------------------------------------------
/test/parse.js:
--------------------------------------------------------------------------------
1 | /* globals describe, it*/
2 | 'use strict'
3 |
4 | let parse = require('..').parse
5 | require('should')
6 |
7 | describe('parse', () => {
8 | it('should parse a literal text', () => {
9 | parse('A literal text').should.be.eql([{
10 | type: 'text',
11 | start: getPos(''),
12 | end: getPos('A literal text'),
13 | content: 'A literal text'
14 | }])
15 |
16 | parse('Multi\nline').should.be.eql([{
17 | type: 'text',
18 | start: getPos(''),
19 | end: getPos('Multi\nline'),
20 | content: 'Multi\nline'
21 | }])
22 | })
23 |
24 | it('should parse EJS tags', () => {
25 | parse('<%eval%><% %><%=escaped%><%-raw%>literal <%% text').should.be.eql([{
26 | type: 'ejs-eval',
27 | start: getPos('<%'),
28 | end: getPos('<%eval'),
29 | content: 'eval'
30 | }, {
31 | type: 'ejs-eval',
32 | start: getPos('<%eval%><% '),
33 | end: getPos('<%eval%><% '),
34 | content: ''
35 | }, {
36 | type: 'ejs-escaped',
37 | start: getPos('<%eval%><% %><%='),
38 | end: getPos('<%eval%><% %><%=escaped'),
39 | content: 'escaped'
40 | }, {
41 | type: 'ejs-raw',
42 | start: getPos('<%eval%><% %><%=escaped%><%-'),
43 | end: getPos('<%eval%><% %><%=escaped%><%-raw'),
44 | content: 'raw'
45 | }, {
46 | type: 'text',
47 | start: getPos('<%eval%><% %><%=escaped%><%-raw%>'),
48 | end: getPos('<%eval%><% %><%=escaped%><%-raw%>literal <%% text'),
49 | content: 'literal <%% text'
50 | }])
51 | })
52 |
53 | it('should parse comment tags', () => {
54 | parse('').should.be.eql([{
55 | type: 'comment',
56 | start: getPos('')).should.be.eql([])
36 | })
37 |
38 | it('should parse doctype tags', () => {
39 | reduce(parse('')).should.be.eql([''])
40 | })
41 |
42 | it('should parse basic element tags', () => {
43 | reduce(parse('')).should.be.eql([''])
44 | })
45 |
46 | it('should parse open tags with literal attributes', () => {
47 | let source = 'ngle\' c="duble" d="" checked="yes!">
',
48 | expected = 'ngle\' c="duble" d checked>
'
49 | reduce(parse(source)).should.be.eql([expected])
50 | })
51 |
52 | it('should parse open tags with dynamic attributes', () => {
53 | let source = ''
54 | reduce(parse(source)).should.be.eql([
55 | ''
61 | ])
62 | })
63 |
64 | it('should normalize whitespace between attributes', () => {
65 | minify('').should.be.equal('')
66 | })
67 |
68 | it('should collapse whitespaces in html text', () => {
69 | minify(' no\n need for spaces ').should.be.equal(' no\nneed for spaces ')
70 |
71 | minify('even <%a%> between <%x%> js ta<%g%>s')
72 | .should.be.equal('even <%a%>between <%x%>js ta<%g%>s')
73 | })
74 |
75 | it('should collapse whitespace in class attribute', () => {
76 | minify('').should.be.equal('')
77 |
78 | minify('')
79 | .should.be.equal('')
80 | })
81 |
82 | it('should collapse boolean attributes', () => {
83 | minify('')
84 | .should.be.equal('')
85 |
86 | minify('')
87 | .should.be.equal('>')
88 | })
89 |
90 | it('should keep whitespace inside -like tags', () => {
91 | minify(' x x x
x ')
92 | .should.be.equal(' x x x
x ')
93 | })
94 |
95 | it('should treat spaces around EJS tags correctly', () => {
96 | minify('before <%= 2 %> after').should.be.equal('before <%=2%> after')
97 | minify('before <%- 2 %> after').should.be.equal('before <%-2%> after')
98 | minify('before <% 2 %> after').should.be.equal('before <%2%>after')
99 | })
100 | })
101 |
102 | function getPos(str) {
103 | let lines = str.split('\n')
104 | return {
105 | pos: str.length,
106 | line: lines.length,
107 | column: lines[lines.length - 1].length + 1
108 | }
109 | }
110 |
111 | function minify(source) {
112 | return reduce(parse(source)).map(e => {
113 | if (typeof e === 'string') {
114 | return e
115 | } else if (e.type === 'source-builder') {
116 | return `<%-${e.sourceBuilder.build(source).code}%>`
117 | }
118 | let c = e.type === 'ejs-eval' ? '' : (e.type === 'ejs-raw' ? '-' : '=')
119 | return `<%${c}${e.content}%>`
120 | }).join('')
121 | }
--------------------------------------------------------------------------------
/test/strict.js:
--------------------------------------------------------------------------------
1 | /* globals describe, it*/
2 | 'use strict'
3 |
4 | let compile = require('..').compile
5 | require('should')
6 |
7 | describe('strict', () => {
8 | it('should compile in strict mode by default', () => {
9 | compile('<% this.x = 1 %>').should.throw(/Cannot set property/)
10 |
11 | // eslint-disable-next-line no-new-func
12 | let render = new Function('locals, renderCustom', compile.standAlone('<% this.x = 2 %>'))
13 | render.should.throw(/Cannot set property/)
14 | })
15 |
16 | it('should compile in sloppy mode', () => {
17 | compile('<% this.x = 3 %>', {
18 | strictMode: false
19 | })()
20 | global.x.should.be.equal(3)
21 |
22 | // eslint-disable-next-line no-new-func
23 | let render = new Function('locals, renderCustom', compile.standAlone('<% this.x = 4 %>', {
24 | strictMode: false
25 | }))
26 | render()
27 | global.x.should.be.equal(4)
28 | })
29 | })
--------------------------------------------------------------------------------