diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..bb0f303 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,7 @@ +{ + "extends": "airbnb-base", + "env": { + "node": true, + "jest": true + } +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f9cf254 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,27 @@ +language: node_js +node_js: + - "6.0" + - "6.1" + - "6.2" + - "6.3" + - "6.4" + - "6.5" + - "6.6" + - "6.7" + - "6.8" + - "6.9" + - "6.10" + - "7.0" + - "7.1" + - "7.2" + - "7.3" + - "7.4" + - "7.5" + - "7.6" + - "7.7" + - "7.8" + - "7.9" + - "7.10" +script: + - npm test + - npm run lint diff --git a/AUTHORS b/AUTHORS index 54e80d3..f5bc9b2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1 +1,2 @@ Timur Shemsedinov +Sergey Nanovsky diff --git a/README.md b/README.md index 7c8a9cf..5d75749 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,42 @@ -## Tickplate +# Tickplate +Backtick template engine for JavaScript -Back-tick template engine for JavaScript +## Install +``` +$ npm install --save tickplate +``` ## Usage - -- Install: `npm install tickplate` -- Require: `const t = require('tickplate');` -- Place tag `t` before templated string - -## Examples: - ```js -const t = require('tickplate'); - -const data = { - hello: 'Ave!', - myFriend: { - name: 'Marcus Aurelius', - toString() { - return this.name - } +const tickplate = require('tickplate'); + +const template = '\ +
\n\ +

${article.title}

\n\ +

${article.description}

\n\ + ${article.author.firstName} ${article.author.lastName}\n\ + ${article.formattedDate()}\n\ +
\ +'; + +const context = { + article: { + title: 'Lorem ipsum', + description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit', + author: { + firstName: 'FirstName', + lastName: 'LastName', + }, + createdAt: new Date(), + formattedDate() { + const date = this.createdAt.getDate(); + const month = this.createdAt.getMonth(); + const year = this.createdAt.getFullYear(); + return `${date}.${month}.${year}`; + }, }, - positions: ['imperor', 'philosopher', 'writer'] }; -const template1 = t`Example: ${'hello'} ${'myFriend'} great ${'positions'} of Rome`; - -console.log(template1(data)); +const result = tickplate(template, context); +console.log(result); ``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..252e965 --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/tickplate'); diff --git a/lib/__tests__/tickplate.js b/lib/__tests__/tickplate.js new file mode 100644 index 0000000..7640898 --- /dev/null +++ b/lib/__tests__/tickplate.js @@ -0,0 +1,48 @@ +/* eslint-disable no-template-curly-in-string */ + +const tickplate = require('../tickplate'); + +describe('tickplate', () => { + it('should insert values from parameters', () => { + const template = '

${person.firstName} ${person.lastName}

'; + const context = { + person: { + firstName: 'FirstName', + lastName: 'LastName', + }, + }; + const actual = tickplate(template, context); + const expected = `

${context.person.firstName} ${context.person.lastName}

`; + expect(actual).toBe(expected); + }); + + it('should evaluate functions from parameters', () => { + const template = '

${test()}

'; + const context = { + test: () => 1 + 1, + }; + const actual = tickplate(template, context); + const expected = `

${context.test()}

`; + expect(actual).toBe(expected); + }); + + it('should correctly escape backticks', () => { + const template = '

`${value}``

'; + const context = { + value: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit.', + }; + const actual = tickplate(template, context); + const expected = `

`${context.value}``

`; + expect(actual).toBe(expected); + }); + + it('should work with nested template literals', () => { + const template = '

${`${`${value}`}`}

'; + const context = { + value: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit.', + }; + const actual = tickplate(template, context); + const expected = `

${`${`${context.value}`}`}

`; + expect(actual).toBe(expected); + }); +}); diff --git a/lib/tickplate.js b/lib/tickplate.js new file mode 100644 index 0000000..54378bc --- /dev/null +++ b/lib/tickplate.js @@ -0,0 +1,69 @@ +const vm = require('vm'); + +/** + * Replace backticks with corresponding HTML entity + * @param {String} string String to be escaped + * @return {String} Escaped string + */ +const escapeBackticks = string => string.replace(/`/g, '`'); + +/** + * Format correct body for template literal string + * @param {String} string String to be formatted + * @return {String} Body for template literal string + */ +const formatBody = (string) => { + const delimiters = { + start: '${', + end: '}', + }; + let result = ''; + let buffer = ''; + let startFound = false; + let endFound = false; + let nested = 0; + for (let i = 0; i < string.length; i += 1) { + const startDelimiter = i < string.length - 1 ? string[i] + string[i + 1] : null; + if (startDelimiter && startDelimiter === delimiters.start) { + if (startFound) { + nested += 1; + } else { + if (buffer.length !== 0) { + result += escapeBackticks(buffer); + buffer = ''; + } + startFound = true; + } + } else if (startFound && string[i] === delimiters.end) { + if (nested !== 0) { + nested -= 1; + } else { + endFound = true; + } + } + buffer += string[i]; + if (startFound && endFound) { + result += buffer; + startFound = false; + endFound = false; + buffer = ''; + } + if (buffer.length !== 0 && i === string.length - 1) { + result += escapeBackticks(buffer); + } + } + return result; +}; + +/** + * Compile template with passed context + * @param {String} template String representing template + * @param {Object} context Object representing template context + * @return {String} Compiled template + */ +const tickplate = (template, context) => { + const code = `\`${formatBody(template)}\``; + return vm.runInNewContext(code, context); +}; + +module.exports = tickplate; diff --git a/package.json b/package.json index a876c98..e3c1efa 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "tickplate", "version": "0.0.1", "author": "", - "description": "Back-tick template engine for JavaScript", + "description": "Backtick template engine for JavaScript", "license": "MIT", "keywords": [ "tickplate", @@ -20,12 +20,19 @@ "url": "https://github.com/metarhia/tickplate/issues", "email": "timur.shemsedinov@gmail.com" }, - "main": "./tickplate.js", + "main": "./index.js", "engines": { "node": ">=6.0.0" }, "readmeFilename": "README.md", "scripts": { - "test": "node ./test.js" + "test": "jest", + "lint": "eslint ." + }, + "devDependencies": { + "eslint": "^3.19.0", + "eslint-config-airbnb-base": "^11.1.3", + "eslint-plugin-import": "^2.2.0", + "jest": "^20.0.1" } } diff --git a/test.js b/test.js deleted file mode 100644 index c030749..0000000 --- a/test.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -const t = require('./tickplate.js'); - -const data = { - hello: 'Ave!', - myFriend: { - name: 'Marcus Aurelius', - toString() { - return this.name - } - }, - positions: ['imperor', 'philosopher', 'writer'] -}; - -const template1 = t`Example: ${'hello'} ${'myFriend'} great ${'positions'} of Rome`; - -console.log(template1(data)); diff --git a/tickplate.js b/tickplate.js deleted file mode 100644 index 6fe1e28..0000000 --- a/tickplate.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -const tickplate = (strings, ...keys) => { - console.dir({ strings, keys }); - return (values) => { - console.dir({ values }); - const result = [strings[0]]; - keys.forEach((key, i) => { - result.push(values[key], strings[i + 1]); - }); - return result.join(''); - }; -}; - -module.exports = tickplate;