Skip to content

Commit

Permalink
add util filter to Leopard
Browse files Browse the repository at this point in the history
  • Loading branch information
RyanLiu0235 committed Feb 9, 2018
1 parent 0f5d656 commit 47389fc
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 113 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "./dist/leopard.js",
"scripts": {
"build": "rollup -c",
"test": "./node_modules/.bin/mocha",
"test": "./node_modules/.bin/mocha --recursive",
"coverage": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- -R spec",
"codecov": "node ./codecov.js",
"benchmark": "node ./benchmark.js"
Expand Down
25 changes: 25 additions & 0 deletions src/filter/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* util for adding filters to Leopard,
* return Leopard for chaining invoking
*
* @param {String} name
* @param {Function} handler
* @return {Leopard}
*/
function filter(name, handler) {
/* istanbul ignore if */
if (typeof handler !== 'function') {
throw new TypeError(
'Leopard: filter requires a function as handler, but got \"' +
typeof handler + '\" in filter \"' + name + '\"'
)
}
/* istanbul ignore if */
if (name in this.prototype) {
throw new Error('Leopard: filter \"' + name + '\" has been declared')
}
this.prototype[name] = handler
return this
}

module.exports = filter
File renamed without changes.
114 changes: 11 additions & 103 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,107 +1,15 @@
var filters = require('./filters')
var escape = require('./utils').escape
var escapeQuotes = function(str) {
return str.replace(/"/g, '\\"')
}

/**
* check if there is filters in expressions
* expect format:
* - 'name | capitalize | reverse'
* and this will be compile into:
* - 'reverse(capitalize(name))'
*
* @param {String} line
* @return {String}
*/
var parseFilters = function(line) {
var segments = line.split('|')
return segments.reduce((accumulator, f) => f.trim() + '(' + accumulator.trim() + ')')
}

/**
* parse the given `tpl` and return Function body string
*
* @param {String} tpl
* @param {Object} data
* @return {String}
*/
var parser = function(tpl, data) {
data = data || {}
var delimeterRE = /<%(.+?)%>/g
var curMatched = null
var matched = null
var body = 'var lines = [];\n' +
'var rst;\n' +
'with(' + JSON.stringify(data) + ') {\n'

/**
* push a string into lines
*
* @param {String} str
*/
function push(str) {
body += 'lines.push(' + str + ');\n'
}

/**
* generate Function body
*
* @param {String} line
*/
var generate = function(line) {
if (line.length > 0) {
var type = line.charAt(0)
var Leopard = require('./instance')
var filter = require('./filter')
var presets = require('./filter/presets')

switch (type) {
// for interpolations we should check filters
case '=':
push('escape(' + parseFilters(line.substr(1).trim()) + ')')
break
case '-':
push(parseFilters(line.substr(1).trim()))
break
default:
body += line + '\n'
}
}
}
// mount `filter` to Leopard as a util
Leopard.filter = filter

while (curMatched = delimeterRE.exec(tpl)) {
// This is raw HTML
var html = tpl.substring(
matched !== null ? matched.index + matched[0].length : 0,
curMatched.index
)
html && push('\"' + escapeQuotes(html) + '\"')
var js = curMatched[1].trim()
js && generate(js)
matched = curMatched
}
var end = tpl.substr(matched.index + matched[0].length)
end && push('\"' + escapeQuotes(end) + '\"')
body += 'rst = lines.join(\"\");\n' +
'}\n' +
'return rst;'

return body
// mount presets to Leopard.prototype so that every instance can use them
var presetFilters = Object.keys(presets)
for (var i = 0, l = presetFilters.length, name; i < l; i++) {
name = presetFilters[i]
Leopard.filter(name, presets[name])
}

/**
* parse the given template and return HTML string
*
* @param {String} tpl
* @param {Object} data
* @return {String}
*/
var compiler = function(tpl, data) {
var body = parser(tpl, data)

// 注入过滤器
var fun = new Function('escape', ...Object.keys(filters), body)
return fun.call(this, escape, ...Object.values(filters))
}

var leo = compiler

module.exports = leo
module.exports = Leopard
106 changes: 106 additions & 0 deletions src/instance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
var escape = require('./utils').escape
var escapeQuotes = function(str) {
return str.replace(/"/g, '\\"')
}

function Leopard() {}
var p = Leopard.prototype

/**
* check if there is filters in expressions
* expect format:
* - 'name | capitalize | reverse'
* and this will be compile into:
* - 'reverse(capitalize(name))'
*
* @param {String} line
* @return {String}
*/
var parseFilters = function(line) {
var segments = line.split('|')
return segments.reduce((accumulator, f) => f.trim() + '(' + accumulator.trim() + ')')
}

/**
* parse the given `tpl` and return Function body string
*
* @param {String} tpl
* @param {Object} data
* @return {String}
*/
p.parse = function(tpl, data) {
data = data || {}
var delimeterRE = /<%(.+?)%>/g
var curMatched = null
var matched = null
var body = 'var lines = [];\n' +
'var rst;\n' +
'with(' + JSON.stringify(data) + ') {\n'

/**
* push a string into lines
*
* @param {String} str
*/
function push(str) {
body += 'lines.push(' + str + ');\n'
}

/**
* generate Function body
*
* @param {String} line
*/
var generate = function(line) {
if (line.length > 0) {
var type = line.charAt(0)

switch (type) {
// for interpolations we should check filters
case '=':
push('escape(' + parseFilters(line.substr(1).trim()) + ')')
break
case '-':
push(parseFilters(line.substr(1).trim()))
break
default:
body += line + '\n'
}
}
}

while (curMatched = delimeterRE.exec(tpl)) {
// This is raw HTML
var html = tpl.substring(
matched !== null ? matched.index + matched[0].length : 0,
curMatched.index
)
html && push('\"' + escapeQuotes(html) + '\"')
var js = curMatched[1].trim()
js && generate(js)
matched = curMatched
}
var end = tpl.substr(matched.index + matched[0].length)
end && push('\"' + escapeQuotes(end) + '\"')
body += 'rst = lines.join(\"\");\n' +
'}\n' +
'return rst;'

return body
}

/**
* parse the given template and return HTML string
*
* @param {String} tpl
* @param {Object} data
* @return {String}
*/
p.compile = function(tpl, data) {
var body = this.parse(tpl, data)
// 注入过滤器
var fun = new Function('escape', ...Object.keys(p), body)
return fun.call(p, escape, ...Object.values(p))
}

module.exports = Leopard
28 changes: 28 additions & 0 deletions test/filter/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
var assert = require('assert')
var Leo = require('../../src')
var filter = require('../../src/filter')

var noop = function() {}

describe('filter util', function() {
it('should throw when handler is not a function', function() {
try {
filter.call(Leo, 'test', 1)
} catch (e) {
assert.ok(e instanceof TypeError)
}
})

it('should throw when name is Leo\'s reserved word', function() {
try {
filter.call(Leo, 'parse', noop)
} catch (e) {
assert.ok(e instanceof Error)
}
})

it('should mount a filter to Leopard.prototype', function() {
filter.call(Leo, 'test', noop)
assert.ok('test' in Leo.prototype)
})
})
12 changes: 12 additions & 0 deletions test/filter/presets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
var assert = require('assert')
var presets = require('../../src/filter/presets')

describe('filter presets', function() {
it('capitalize', function() {
assert.strictEqual(presets.capitalize('leopard'), 'Leopard')
})

it('reverse', function() {
assert.strictEqual(presets.reverse('leopard'), 'drapoel')
})
})
25 changes: 16 additions & 9 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
var assert = require('assert')
var escape = require('../src/utils').escape
var leo = require('../src')
var Leo = require('../src')

var basicData = {
a: 'Leopard',
Expand All @@ -17,18 +17,21 @@ var conditionData = {

describe('leopard', function() {
it('inits with simplest config', function() {
var template = leo('<p>This is <%= a %>!</p>', basicData)
var leo = new Leo()
var template = leo.compile('<p>This is <%= a %>!</p>', basicData)
assert.strictEqual(template, '<p>This is Leopard!</p>')
})

it('handles multiple expressions', function() {
var template = leo('<p>This is <%= a %>, AKA <%= b %>!</p>', basicData)
var leo = new Leo()
var template = leo.compile('<p>This is <%= a %>, AKA <%= b %>!</p>', basicData)
assert.strictEqual(template, '<p>This is Leopard, AKA leo!</p>')
})

it('handles complex expressions', function() {
var template = leo('<p>m + n = <%= m + n %></p>', basicData)
var template_2 = leo(
var leo = new Leo()
var template = leo.compile('<p>m + n = <%= m + n %></p>', basicData)
var template_2 = leo.compile(
'<p>I am Leopard<%= \', AKA \' + (isOk ? nickname : realname) + \'!\' %></p>',
conditionData
)
Expand All @@ -37,12 +40,13 @@ describe('leopard', function() {
})

it('handles html interpolations and text interpolations', function() {
var leo = new Leo()
var string = '<p>html tags can be escaped and rendered as string: <%= html %>.' +
' Or can still rendered as html: <%- html %></p>'
var data = {
html: '<em>Leopard</em>'
}
var template = leo(string, data)
var template = leo.compile(string, data)
assert.strictEqual(template, '<p>html tags can be escaped and rendered as string: ' +
escape(data.html) +
'. Or can still rendered as html: ' +
Expand All @@ -51,31 +55,34 @@ describe('leopard', function() {
})

it('handles conditions', function() {
var leo = new Leo()
var conditions = '<% if (isOk) { %>' +
'<span class=\"nickname\"><%= nickname %></span>' +
'<% } else { %>' +
'<span class=\"realname\"><%= realname %></span>' +
'<% } %>'

var template = leo(conditions, conditionData)
var template = leo.compile(conditions, conditionData)
assert.strictEqual(template, '<span class=\"realname\">leopard</span>')
})

it('handles loops', function() {
var leo = new Leo()
var loops = 'Now I repeat: ' +
'<ul>' +
'<% for (var i = 0; i < 2; i++) { %>' +
'<li><%= i %>: I am Leopard!</li>' +
'<% } %>' +
'</ul>'

var template = leo(loops)
var template = leo.compile(loops)
assert.strictEqual(template, 'Now I repeat: <ul><li>0: I am Leopard!</li><li>1: I am Leopard!</li></ul>')
})

it('handles filters in interpolations', function() {
var leo = new Leo()
var filters = '<p><%= \"leopard\" | capitalize | reverse %></p>'
var template = leo(filters)
var template = leo.compile(filters)
assert.strictEqual(template, '<p>drapoeL</p>')
})
})

0 comments on commit 47389fc

Please sign in to comment.