├── test ├── expected │ ├── custom_options │ └── default_options ├── fixtures │ ├── 123 │ ├── 123-tpl.js │ └── 123.tpl.js └── tpl_compiler_test.js ├── .gitignore ├── .jshintrc ├── LICENSE-MIT ├── package.json ├── tasks ├── template.js.tpl └── tpl_compiler.js ├── Gruntfile.js └── README.md /test/expected/custom_options: -------------------------------------------------------------------------------- 1 | Testing: 1 2 3 !!! -------------------------------------------------------------------------------- /test/expected/default_options: -------------------------------------------------------------------------------- 1 | Testing, 1 2 3. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | tmp 4 | .idea -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "boss": true, 11 | "eqnull": true, 12 | "node": true 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/123: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 |

${title}

6 | 11 |
12 | 13 |
14 |
15 |
16 | {@each moreList as item} 17 |
${item.title}
18 |
${item.desc}
19 | {@/each} 20 |
21 |
22 |
23 | 24 |
25 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 弘树 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-tpl-compiler", 3 | "description": "Grunt plugin for juicer-based template compile to kissy module", 4 | "version": "0.1.6", 5 | "homepage": "https://github.com/dickeylth/grunt-tpl-compiler", 6 | "author": { 7 | "name": "弘树", 8 | "email": "tiehang.lth@alibaba-inc.com", 9 | "url": "http://dickeylth.github.io" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/dickeylth/grunt-tpl-compiler.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/dickeylth/grunt-tpl-compiler/issues" 17 | }, 18 | "licenses": [ 19 | { 20 | "type": "MIT", 21 | "url": "https://github.com/dickeylth/grunt-tpl-compiler/blob/master/LICENSE-MIT" 22 | } 23 | ], 24 | "engines": { 25 | "node": ">= 0.8.0" 26 | }, 27 | "scripts": { 28 | "test": "grunt test" 29 | }, 30 | "devDependencies": { 31 | "grunt-contrib-jshint": "~0.6.0", 32 | "grunt-contrib-clean": "~0.4.0", 33 | "grunt-contrib-nodeunit": "~0.2.0", 34 | "grunt": "~0.4.2" 35 | }, 36 | "peerDependencies": { 37 | "grunt": "~0.4.2 || ^1.0.0" 38 | }, 39 | "keywords": [ 40 | "gruntplugin" 41 | ], 42 | "dependencies": { 43 | "ast-query": "^0.2.4", 44 | "cheerio": "^0.17.0", 45 | "juicer": "^0.6.5-stable-p2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tasks/template.js.tpl: -------------------------------------------------------------------------------- 1 | /** 2 | * KISSY Template Module for ${page} 3 | */ 4 | KISSY.add(function (S, Juicer){ 5 | 6 | "use strict"; 7 | 8 | /** 9 | * 维护所有用到的模板 10 | * @class Template 11 | * @constructor 12 | */ 13 | 14 | var templates = { 15 | {@each templates as template, idx} 16 | 17 | ${template.key}: '$${template.value}'{@if idx != (templates.length - 1)},{@/if} 18 | 19 | {@/each} 20 | }; 21 | 22 | return { 23 | 24 | /** 25 | * 注册模板自定义函数 26 | * @param name 需要替换的模板中用到的名字 27 | * @param fn 自定义函数 28 | */ 29 | register: function(name, fn){ 30 | 31 | Juicer.register(name, fn); 32 | 33 | }, 34 | 35 | /** 36 | * 覆盖已有模板 37 | * @param key {String} template 模板键 38 | * @param tmpl {String} 模板 39 | */ 40 | set: function(key, tmpl){ 41 | 42 | templates[key] = tmpl; 43 | 44 | }, 45 | 46 | /** 47 | * 获取已有模板 48 | * @param key {String} 模板Key 49 | * @returns {String} 模板内容 50 | */ 51 | get: function(key){ 52 | 53 | return templates[key]; 54 | 55 | }, 56 | 57 | /** 58 | * 根据指定的模板key和数据渲染生成html 59 | * @param key 模板的key 60 | * @param data json数据 61 | * @returns {String} 62 | */ 63 | render: function(key, data){ 64 | 65 | return Juicer(templates[key], data); 66 | 67 | } 68 | 69 | }; 70 | 71 | }, { 72 | requires: ['gallery/juicer/1.3/index'] 73 | }); -------------------------------------------------------------------------------- /test/fixtures/123-tpl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * KISSY Template Module for 123 3 | */ 4 | KISSY.add(function (S, Juicer) { 5 | 'use strict'; 6 | /** 7 | * 维护所有用到的模板 8 | * @class Template 9 | * @constructor 10 | */ 11 | var templates = { 12 | ProdList: '

${title}

', 13 | MoreList: '
{@each moreList as item}
${item.title}
${item.desc}
{@/each}
' 14 | }; 15 | return { 16 | /** 17 | * 注册模板自定义函数 18 | * @param name 需要替换的模板中用到的名字 19 | * @param fn 自定义函数 20 | */ 21 | register: function (name, fn) { 22 | Juicer.register(name, fn); 23 | }, 24 | /** 25 | * 覆盖已有模板 26 | * @param key {String} template 模板键 27 | * @param tmpl {String} 模板 28 | */ 29 | set: function (key, tmpl) { 30 | templates[key] = tmpl; 31 | }, 32 | /** 33 | * 获取已有模板 34 | * @param key {String} 模板Key 35 | * @returns {String} 模板内容 36 | */ 37 | get: function (key) { 38 | return templates[key]; 39 | }, 40 | /** 41 | * 根据指定的模板key和数据渲染生成html 42 | * @param key 模板的key 43 | * @param data json数据 44 | * @returns {String} 45 | */ 46 | render: function (key, data) { 47 | return Juicer(templates[key], data); 48 | } 49 | }; 50 | }, { requires: ['gallery/juicer/1.3/index'] }); -------------------------------------------------------------------------------- /test/fixtures/123.tpl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * KISSY Template Module for 123 3 | */ 4 | KISSY.add(function (S, Juicer) { 5 | 'use strict'; 6 | /** 7 | * 维护所有用到的模板 8 | * @class Template 9 | * @constructor 10 | */ 11 | var templates = { 12 | ProdList: '

${title}

', 13 | MoreList: '
{@each moreList as item}
${item.title}
${item.desc}
{@/each}
' 14 | }; 15 | return { 16 | /** 17 | * 注册模板自定义函数 18 | * @param name 需要替换的模板中用到的名字 19 | * @param fn 自定义函数 20 | */ 21 | register: function (name, fn) { 22 | Juicer.register(name, fn); 23 | }, 24 | /** 25 | * 覆盖已有模板 26 | * @param key {String} template 模板键 27 | * @param tmpl {String} 模板 28 | */ 29 | set: function (key, tmpl) { 30 | templates[key] = tmpl; 31 | }, 32 | /** 33 | * 获取已有模板 34 | * @param key {String} 模板Key 35 | * @returns {String} 模板内容 36 | */ 37 | get: function (key) { 38 | return templates[key]; 39 | }, 40 | /** 41 | * 根据指定的模板key和数据渲染生成html 42 | * @param key 模板的key 43 | * @param data json数据 44 | * @returns {String} 45 | */ 46 | render: function (key, data) { 47 | return Juicer(templates[key], data); 48 | } 49 | }; 50 | }, { requires: ['gallery/juicer/1.3/index'] }); -------------------------------------------------------------------------------- /test/tpl_compiler_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var grunt = require('grunt'); 4 | 5 | /* 6 | ======== A Handy Little Nodeunit Reference ======== 7 | https://github.com/caolan/nodeunit 8 | 9 | Test methods: 10 | test.expect(numAssertions) 11 | test.done() 12 | Test assertions: 13 | test.ok(value, [message]) 14 | test.equal(actual, expected, [message]) 15 | test.notEqual(actual, expected, [message]) 16 | test.deepEqual(actual, expected, [message]) 17 | test.notDeepEqual(actual, expected, [message]) 18 | test.strictEqual(actual, expected, [message]) 19 | test.notStrictEqual(actual, expected, [message]) 20 | test.throws(block, [error], [message]) 21 | test.doesNotThrow(block, [error], [message]) 22 | test.ifError(value) 23 | */ 24 | 25 | exports.tpl_compiler = { 26 | setUp: function(done) { 27 | // setup here if necessary 28 | done(); 29 | }, 30 | default_options: function(test) { 31 | 32 | test.expect(1); 33 | 34 | // 处理 35 | var result = grunt.file.read('test/fixtures/123-tpl.js'); 36 | test.equal(/data\-spmClick/.test(result), true, '标签属性保留驼峰'); 37 | 38 | // 39 | // var actual = grunt.file.read('tmp/default_options'); 40 | // var expected = grunt.file.read('test/expected/default_options'); 41 | // test.equal(actual, expected, 'should describe what the default behavior is.'); 42 | 43 | test.done(); 44 | }, 45 | custom_options: function(test) { 46 | // test.expect(1); 47 | // 48 | // var actual = grunt.file.read('tmp/custom_options'); 49 | // var expected = grunt.file.read('test/expected/custom_options'); 50 | // test.equal(actual, expected, 'should describe what the custom option(s) behavior is.'); 51 | 52 | test.done(); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt-tpl-compiler 3 | * https://github.com/dickeylth/grunt-tpl-compiler 4 | * 5 | * Copyright (c) 2014 弘树 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | module.exports = function(grunt) { 12 | 13 | // Project configuration. 14 | grunt.initConfig({ 15 | jshint: { 16 | }, 17 | 18 | // Before generating any new files, remove any previously-created files. 19 | clean: { 20 | tests: ['tmp'] 21 | }, 22 | 23 | // Configuration to be run (and then tested). 24 | tpl_compiler: { 25 | default_options: { 26 | options: { 27 | ext: '-tpl', 28 | replaceEscapeMap: { 29 | '\xB0': '°' 30 | } 31 | }, 32 | files: { 33 | 'tmp/default_options': ['test/fixtures/123'] 34 | } 35 | }, 36 | custom_options: { 37 | options: { 38 | ext: '.tpl', 39 | replaceEscapeMap: { 40 | '\xB0': '°', 41 | '<': "<" 42 | } 43 | }, 44 | files: { 45 | 'tmp/custom_options': ['test/fixtures/123'] 46 | } 47 | } 48 | }, 49 | 50 | // Unit tests. 51 | nodeunit: { 52 | tests: ['test/*_test.js'] 53 | } 54 | 55 | }); 56 | 57 | // Actually load this plugin's task(s). 58 | grunt.loadTasks('tasks'); 59 | 60 | // These plugins provide necessary tasks. 61 | grunt.loadNpmTasks('grunt-contrib-jshint'); 62 | grunt.loadNpmTasks('grunt-contrib-clean'); 63 | grunt.loadNpmTasks('grunt-contrib-nodeunit'); 64 | 65 | // Whenever the "test" task is run, first clean the "tmp" dir, then run this 66 | // plugin's task(s), then test the result. 67 | grunt.registerTask('test', ['clean', 'tpl_compiler', 'nodeunit']); 68 | 69 | // By default, lint and run all tests. 70 | grunt.registerTask('default', ['test']); 71 | 72 | }; 73 | -------------------------------------------------------------------------------- /tasks/tpl_compiler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt-tpl-compiler 3 | * https://github.com/dickeylth/grunt-tpl-compiler 4 | * 5 | * Copyright (c) 2014 弘树 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | var fs = require('fs'); 12 | var path = require('path'); 13 | var program = require("ast-query"); 14 | var juicer = require('juicer'); 15 | var cheerio = require('cheerio'); 16 | 17 | module.exports = function (grunt) { 18 | 19 | // Please see the Grunt documentation for more information regarding task 20 | // creation: http://gruntjs.com/creating-tasks 21 | 22 | grunt.registerMultiTask('tpl_compiler', 'Grunt plugin for juicer-based template compile to kissy module', function () { 23 | // Merge task-specific and/or target-specific options with these defaults. 24 | var options = this.options({ 25 | ext: '-tpl' 26 | }); 27 | 28 | var jsTpl = fs.readFileSync(path.join(__dirname, './template.js.tpl')).toString(); 29 | 30 | // Iterate over all specified file groups. 31 | this.files.forEach(function (f) { 32 | // Concat specified files. 33 | var src = f.src.filter(function (filepath) { 34 | // Warn on and remove invalid source files (if nonull was set). 35 | if (!grunt.file.exists(filepath)) { 36 | grunt.log.warn('Source file "' + filepath + '" not found.'); 37 | return false; 38 | } else { 39 | return true; 40 | } 41 | }).map(function (filepath) { 42 | // Read file source. 43 | return grunt.file.read(filepath); 44 | }); 45 | 46 | // cheerio 解析模板所在 HTML 的 DOM 47 | src = src.join(''); 48 | var $ = cheerio.load(src, { 49 | normalizeWhitespace: true, 50 | decodeEntities: false, 51 | lowerCaseAttributeNames: false 52 | }); 53 | 54 | // 获取各个指定的模板 55 | var templates = []; 56 | $('[data-tpl]').each(function(idx, node){ 57 | var tplKey = $(node).attr('data-tpl'); 58 | var tplValue = $(node).html(); 59 | templates.push({ 60 | key: tplKey, 61 | value: tplValue 62 | }); 63 | }); 64 | 65 | var srcPath = f.src; 66 | if (Array.isArray(srcPath)) { 67 | srcPath = srcPath[0]; 68 | } 69 | 70 | var basename = path.basename(srcPath), 71 | dirname = path.dirname(srcPath), 72 | toFileName = basename.split('.')[0] + options.ext + '.js', 73 | toFilePath = path.join(dirname, toFileName); 74 | 75 | if(!fs.existsSync(toFilePath)){ 76 | 77 | // 如果是初次生成对应 js 文件 78 | 79 | // 与 模板 js 合并 80 | juicer.set('strip',false); 81 | var toJsContent = juicer(jsTpl, { 82 | templates: templates, 83 | page: basename 84 | }); 85 | 86 | // Write the destination file. 87 | grunt.file.write(toFilePath, toJsContent); 88 | 89 | // Print a success message. 90 | grunt.log.writeln('File "' + toFilePath + '" created.'); 91 | 92 | } else { 93 | 94 | var curJsContent = fs.readFileSync(toFilePath).toString(), 95 | template = "{{@each templates as template, idx}" + 96 | "${template.key}: '$${template.value}'" + 97 | "{@if idx != (templates.length - 1)},{@/if}" + 98 | "{@/each}}"; 99 | 100 | if(curJsContent) { 101 | 102 | var tree = program(curJsContent); 103 | tree.var('templates').value(juicer(template, { 104 | templates: templates 105 | })); 106 | 107 | grunt.file.write(toFilePath, tree.toString()); 108 | 109 | // Print a success message. 110 | grunt.log.writeln('File "' + toFilePath + '" updated.'); 111 | } 112 | 113 | } 114 | 115 | }); 116 | }); 117 | 118 | }; 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grunt-tpl-compiler 2 | 3 | > Grunt plugin for juicer-based template compile to kissy module 4 | > 5 | > 将基于 [juicer](http://juicer.name) 语法的模板文件编译为 KISSY 模块。 6 | 7 | ## Getting Started 8 | This plugin requires Grunt `~0.4.2` 9 | 10 | If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide, as it explains how to create a [Gruntfile](http://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. Once you're familiar with that process, you may install this plugin with this command: 11 | 12 | ```shell 13 | npm install grunt-tpl-compiler --save-dev 14 | ``` 15 | 16 | Once the plugin has been installed, it may be enabled inside your Gruntfile with this line of JavaScript: 17 | 18 | ```js 19 | grunt.loadNpmTasks('grunt-tpl-compiler'); 20 | ``` 21 | 22 | ## The "tpl_compiler" task 23 | 24 | ### Overview 25 | In your project's Gruntfile, add a section named `tpl_compiler` to the data object passed into `grunt.initConfig()`. 26 | 27 | ```js 28 | grunt.initConfig({ 29 | tpl_compiler: { 30 | options: { 31 | // Task-specific options go here. 32 | }, 33 | your_target: { 34 | // Target-specific file lists and/or options go here. 35 | }, 36 | }, 37 | }); 38 | ``` 39 | 40 | ### Options 41 | 42 | #### options.ext 43 | Type: `String` 44 | Default value: `'tpl'` 45 | 46 | 生成 js 文件的后缀字符串 47 | 48 | ### Usage Examples 49 | 50 | #### Default Options 51 | In this example, the default options are used to do something with whatever. So if the `testing` file has the content `Testing` and the `123` file had the content `1 2 3`, the generated result would be `Testing, 1 2 3.` 52 | 53 | ```js 54 | grunt.initConfig({ 55 | tpl_compiler: { 56 | options: { 57 | ext: '-tpl', 58 | replaceEscapeMap: { 59 | '\xB0': '°' 60 | } 61 | }, 62 | main: { 63 | files: [ 64 | { 65 | expand: true, 66 | cwd: 'src/', 67 | src: ['**/*.tpl.html'], 68 | dest: 'src/' 69 | } 70 | ] 71 | } 72 | }, 73 | }); 74 | ``` 75 | 76 | #### 词汇 77 | 78 | - HTML 模板文件 79 | - 一个普通的 HTML 文件,其中以 [juicer](http://juicer.name) 语法书写要用到的模板文件,模板文件通过 DOM 节点的 `data-tpl` 属性指定各个模板的钩子。 80 | - JavaScript 模板文件 81 | - 从 HTML 模板文件生成的对应的 JavaScript 文件,该文件为一个普通的 KISSY 模块,对 HTML 模板文件中的各个模板进行抽离,对外暴露 `get`、`set`、`register`、`render` 方法以读写模板字符串。 82 | - 一个 HTML 模板文件对应生成一个 JavaScript 模板文件。 83 | 84 | #### 使用方法 85 | 86 | 1. 在 "src" 目录下的指定 HTML 模板文件中编辑,根据 grunt config 中的 `main->files->src` 指定,如上面的 `**/*.tpl.html` 即为 `.tpl.html` 后缀的 html 文件视为需要处理的 HTML 模板文件,示例: 87 | 88 | [src/pages/index.tpl.html] 89 | 90 | ``` html 91 | 92 | ... 93 | 94 |
95 | 96 |
97 |

${title}

98 | 103 |
104 | 105 |
106 |
107 |
108 | {@each moreList as item} 109 |
${item.title}
110 |
${item.desc}
111 | {@/each} 112 |
113 |
114 |
115 | 116 |
117 | 118 | 119 | ``` 120 | 121 | 其中指定了两个模板:`
...
` 和 `
`。 122 | 123 | 2. 执行 `grunt tpl_compiler`,生成对应的 JavaScript 模板文件: 124 | 125 | [src/pages/index-tpl.js] 126 | 127 | ``` javascript 128 | /** 129 | * KISSY Template Module for test 130 | */ 131 | KISSY.add(function (S, Juicer) { 132 | 'use strict'; 133 | /** 134 | * 维护所有用到的模板 135 | * @class Template 136 | * @constructor 137 | */ 138 | var templates = { 139 | ProdList: '

${title}

', 140 | MoreList: '
{@each moreList as item}
${item.title}
${item.desc}
{@/each}
' 141 | }; 142 | return { 143 | /** 144 | * 注册模板自定义函数 145 | * @param name 需要替换的模板中用到的名字 146 | * @param fn 自定义函数 147 | */ 148 | register: function (name, fn) { 149 | Juicer.register(name, fn); 150 | }, 151 | /** 152 | * 覆盖已有模板 153 | * @param key {String} template 模板键 154 | * @param tmpl {String} 模板 155 | */ 156 | set: function (key, tmpl) { 157 | templates[key] = tmpl; 158 | }, 159 | /** 160 | * 获取已有模板 161 | * @param key {String} 模板Key 162 | * @returns {String} 模板内容 163 | */ 164 | get: function (key) { 165 | return templates[key]; 166 | }, 167 | /** 168 | * 根据指定的模板key和数据渲染生成html 169 | * @param key 模板的key 170 | * @param data json数据 171 | * @returns {String} 172 | */ 173 | render: function (key, data) { 174 | return Juicer(templates[key], data); 175 | } 176 | }; 177 | }, { requires: ['gallery/juicer/1.3/index'] }); 178 | ``` 179 | 180 | 3. 这样其他模块中需要依赖模板的地方,直接通过 KISSY 模块 `require` 该 JavaScript 模板文件即可,通用的 `register` 方法也可以写在该 JavaScript 模板文件中。 181 | 182 | 4. 修改 `src/pages/index.tpl.html`,重新构建后生成的 `src/pages/index-tpl.js` 中 **只会覆盖 `var templates = {...}` 部分**。因此如果直接修改 `src/pages/index-tpl.js` 中除 `var templates = {...}` 的部分,重新构建时修改内容 **会保留而不会被覆盖掉**。 183 | 184 | 5. 建议将该任务加入到 `watch` 中实时编译模板文件,保证本地服务实时取的是最新的模块。 185 | 186 | 187 | ## Contributing 188 | In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using [Grunt](http://gruntjs.com/). 189 | 190 | ## Release History 191 | 192 | - [0.1.6] fix for `this.files.src` is `Array`. 193 | - [0.1.4] 用 cheerio 替换 jsdom,避免 windows 下 jsdom 安装失败,移除 htmlmin 194 | - [0.1.3] Bugfix for html escape 195 | - [0.1.0] 基本功能完成 196 | --------------------------------------------------------------------------------