├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── directives ├── bind.js ├── class.js ├── example.js ├── hide.js ├── if.js ├── include.js ├── repeat.js ├── show.js └── style.js ├── gulpfile.js ├── index.js ├── lib ├── cache.js ├── helpers.js └── number-format.js ├── locales └── en_US.js ├── package.json ├── pipes ├── currency.js ├── date.js ├── filter.js ├── json.js ├── limit-to.js ├── lowercase.js ├── number.js └── uppercase.js └── spec ├── includes ├── medium.html └── small.html ├── layout.html ├── partial.html ├── small.html ├── spec.js └── support └── jasmine.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true, 5 | }, 6 | extends: [ 7 | 'airbnb-base', 8 | ], 9 | globals: { 10 | Atomics: 'readonly', 11 | SharedArrayBuffer: 'readonly', 12 | }, 13 | parserOptions: { 14 | ecmaVersion: 2018, 15 | }, 16 | rules: { 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | spec 4 | .travis.yml 5 | gulpfile.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | script: 5 | - "npm test" 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Angular Template 2 | ============================== 3 | [![build status](https://secure.travis-ci.org/allenhwkim/angular-template.png)](http://travis-ci.org/allenhwkim/angular-template) 4 | 5 | Angular-Like HTML Template Engine For NodeJS 6 | ----------------------------------------------- 7 | 8 | Why do I need this? 9 | By unknown reason, I feel all server-side template engines are somewhat invasive. 10 | It looks like an odd language have been invaded HTML space. 11 | The only template I feel good about it is AngularJS, but it's all about client-side, not server-side part. 12 | If you are a big fan of AngularJS and you want to use AngularJS as a template engine, this node module will do the job. 13 | 14 | This template converts the following one time binding expressions on the server-side; 15 | 16 | 1. inline expression 17 | e.g. `{{ foo }}` 18 | 19 | 2. `ht-if` attribute 20 | e.g., `
..
` 21 | e.g., `
..
` 22 | e.g., `
..
` 23 | e.g., `
..
` 24 | e.g., `
..
` 25 | 26 | 27 | 3. `ht-repeat` attribute 28 | e.g., `
  • ..
  • ` 29 | e.g., `
  • ..
  • ` 30 | 31 | 4. ht-include attribute 32 | e.g., `
    ` 33 | 34 | Install 35 | ------- 36 | 37 | npm install angular-template 38 | 39 | Usage 40 | ------ 41 | 42 | var htmlTemplate = require('angular-template'); 43 | htmlTemplate('{{foo}}', {foo:'Hello'}); //Hello 44 | 45 | // or 46 | var htmlTemplate = require('angular-template'); 47 | var path = "emails/template.html"; 48 | var options = {prefix:'ng'}; // so that ng-if, ng-repeat, ng-include would work 49 | htmlTemplate(path, {name:'John'}, options); 50 | 51 | Options 52 | ------ 53 | 54 | prefix: , override default ht prefix with anything, typically ng to reuse angular templates 55 | preprocess: , enables you to modify your template before parsing it as HTML. E.g. You can remove some attributes with RegExp 56 | includeDirs: , a list of paths where to look for template 57 | cache: , specify cache key to avoid reading files from disk every time 58 | locale: an object compatible with one found in locales/en_US.js 59 | 60 | Converting Angular-Like Expressions 61 | ------------------------------------------------ 62 | This will convert the angular-like expressions into html. 63 | 64 | 1. Curly braces expression. 65 | 66 | Assuming foo has the value of `aBc` and the value is `1234` 67 | 68 | Input | Output 69 | -----------------------------------------+--------------------------------- 70 | {{foo}} | aBc 71 | {{foo|uppercase}} | ABC 72 | {{foo|lowercase}} | abc 73 | {{foo|json}} | "abc" 74 | {{45.5789 | number:2}} | 45.57 75 | {{5247.28 | currency:undefined:2}} | $5,247 76 | {{1288323623006 | date:'medium' }} | Oct 29, 2010 5:40:23 AM 77 | {{value | limitTo:2 }} | 12 78 | {{value | limitTo:2:1 }} | 23 79 | {{[1, 2, 3, 4] | limitTo:2:2 | json:0 }} | [3,4] 80 | 81 | 2. **`ht-if`** attribute 82 | 83 | Assuming foo has value `true`, and bar has value `false` 84 | 85 | Input | Output 86 | -------------------------------------+--------------------------------- 87 |

    SHOW

    |

    SHOW

    88 |

    NO SHOW

    |

    89 | 90 | 3. **`ht-include`** attribute 91 | 92 | Assuming foo.html has the following contents `{{prop}} is {{value}}` and that `prop="number", item = 20` 93 | 94 | The input and output would like; 95 | 96 | Input | Output 97 | -----------------------------------------+------------------------------------ 98 |

    99 | ht-include-context="{value:item}">

    | number is 20 100 | |

    101 | 102 | 103 | 4. **`ht-repeat`** attribute 104 | 105 | Assuming collection has the value of `{a:1, b:2, c:3, d:4, e:5}` 106 | 107 | Input | Output 108 | ----------------------------------------------+------------------------------------ 109 |
      |
        110 |
      • |
      • a : 1
      • 111 | {{key}} : {{val}} |
      • b : 2
      • 112 | |
      • c : 3
      • 113 |
      |
    • d : 4
    • 114 | |
    • e : 5
    • 115 | |
    116 | 117 | Assuming collection has the value of `[1,2,3,4,5]` 118 | 119 | Input | Output 120 | ----------------------------------------------+------------------------------------ 121 |
      |
        122 |
      • |
      • 1
      • 123 | {{num}} |
      • 2
      • 124 | |
      • 3
      • 125 |
      |
    • 4
    • 126 | |
    • 5
    • 127 | |
    128 | Assuming collection has the value of `[1,2,3,4,5]` 129 | 130 | Input | Output 131 | ----------------------------------------------------+------------------------------ 132 |
      |
        133 |
      • |
      • 2
      • 134 | {{num}} |
      • 3
      • 135 | |
      • 4
      • 136 |
      |
    137 | 138 | Assuming collection has the value of `[1,2,3,4,5]` and filterFn is `(v) => v > 1` 139 | 140 | Input | Output 141 | ---------------------------------------------------------+------------------------- 142 |
      |
        143 |
      • |
      • 2
      • 144 | {{num}} |
      • 3
      • 145 | |
      • 4
      • 146 |
      |
    • 5
    • 147 | |
    148 | 149 | 5. **`ht-class`** attribute 150 | 151 | Assuming classes has value `foo`, and classes2 has value `{baz:true}`. 152 | 153 | Input | Output 154 | -------------------------------------+--------------------------------- 155 |

    SHOW

    |

    SHOW

    156 |

    SHOW

    |

    SHOW

    157 | 158 | This accepts the same format as [ng-class](https://docs.angularjs.org/api/ng/directive/ngClass) 159 | 160 | 6. **`ht-bind`**, **`ht-bind-html`** attribute 161 | 162 | Assuming content has value `YES`, and status has value `Done`. 163 | 164 | Input | Output 165 | -------------------------------------+--------------------------------- 166 |

    |

    Done

    167 |

    SHOW

    |

    YES

    168 | 169 | 170 | 7. **`ht-style`** attribute 171 | 172 | Assuming color has value `red`, and styles has value `{color:green,'font-size':'12px'}`. 173 | 174 | Input | Output 175 | -----------------------------------------------------------+--------------------------------- 176 |

    SHOW

    |

    SHOW

    177 |

    SHOW

    |

    SHOW

    178 | 179 | This accepts the same format as [ng-style](https://docs.angularjs.org/api/ng/directive/ngStyle) 180 | 181 | 182 | 183 | LICENSE: MIT 184 | -------------------------------------------------------------------------------- /directives/bind.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function BindDirective($, data, options, angularTemplate) { 4 | ['bind', 'bind-html'].forEach(function (type) { 5 | var binds = $("*[" + options.prefix + "-" + type + "]"); 6 | binds.each(function (i, elem) { 7 | var expr = $(this).attr(options.prefix + '-' + type).trim(); 8 | $(this).text('<%=' + expr + ' %>'); 9 | $(this).removeAttr(options.prefix + '-' + type); 10 | }); 11 | }); 12 | } 13 | 14 | module.exports = BindDirective -------------------------------------------------------------------------------- /directives/class.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function ClassDirective($, data, options, angularTemplate) { 4 | /** 5 | * ht-class expression 6 | */ 7 | var classes = $("*[" + options.prefix + "-class]"); 8 | classes.each(function (i, elem) { 9 | var expr = $(this).attr(options.prefix + '-class').trim(); 10 | var classes = ($(this).attr('class') || '').trim(); 11 | if (classes) { 12 | classes += ' '; 13 | } 14 | 15 | $(this).removeAttr(options.prefix + '-class'); 16 | $(this).attr('class', classes + '<%=$helpers.generateClassList(' + expr + ')%>'); 17 | }); 18 | } 19 | 20 | ClassDirective.init = function (data, options, angularTemplate) { 21 | data.$helpers.generateClassList = function generateClassList(input) { 22 | var list; 23 | if (Array.isArray(input)) { 24 | list = input.map(generateClassList); 25 | } else if (typeof (input) === 'object') { 26 | list = Object.keys(input).map(function (key) { 27 | return input[key] ? generateClassList(key) : ''; 28 | }); 29 | } else { 30 | list = [input ? String(input) : '']; 31 | } 32 | // ignore empty values 33 | return list.filter(function (v) { 34 | return !!v; 35 | }).join(' ') 36 | } 37 | }; 38 | 39 | module.exports = ClassDirective -------------------------------------------------------------------------------- /directives/example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function ExampleDirective($, data, options, angularTemplate) { 4 | 5 | } 6 | 7 | module.exports = ExampleDirective 8 | 9 | ExampleDirective.init = function (data, options, angularTemplate) { 10 | 11 | }; -------------------------------------------------------------------------------- /directives/hide.js: -------------------------------------------------------------------------------- 1 | 2 | function HideDirective($, data, options, angularTemplate) { 3 | /** 4 | * ht-hide expression 5 | */ 6 | const selector = `${options.prefix}-hide`; 7 | const htHide = $(`*[${selector}]`); 8 | htHide.each(function (i, elem) { 9 | const expr = $(this).attr(selector).trim(); 10 | $(this).before(`<% if (!(${expr})) { %>`); 11 | $(this).after('<% } %>'); 12 | $(this).removeAttr(selector); 13 | }); 14 | } 15 | 16 | module.exports = HideDirective; 17 | -------------------------------------------------------------------------------- /directives/if.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function IfDirective($, data, options, angularTemplate) { 4 | /** 5 | * ht-if expression 6 | */ 7 | var htIfs = $("*[" + options.prefix + "-if]"); 8 | htIfs.each(function (i, elem) { 9 | var expr = $(this).attr(options.prefix + '-if').trim(); 10 | $(this).before("<% if (" + expr + ") { %>"); 11 | $(this).after("<% } %>"); 12 | $(this).removeAttr(options.prefix + '-if'); 13 | }); 14 | } 15 | 16 | module.exports = IfDirective -------------------------------------------------------------------------------- /directives/include.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | 4 | function IncludeDirective($, data, options, angularTemplate) { 5 | /** 6 | * ht-include expression 7 | */ 8 | var htIncludes = $("*[" + options.prefix + "-include]"); 9 | htIncludes.each(function (i, elem) { 10 | var context = ($(this).attr(options.prefix + '-include-context') || '{}').trim(); 11 | var repeatParents = []; 12 | var existingContextProperties = []; 13 | // parse all repeat expressions from all the parents 14 | $(this).parents("[" + options.prefix + "-repeat]").each(function (pi, parent) { 15 | var result = angularTemplate.helpers.parseRepeatExpression($(this).attr(options.prefix + '-repeat')); 16 | if (result) { 17 | repeatParents.push(result); // remember all of them in bottom-top order 18 | } 19 | }); 20 | 21 | // go through each repeat expression (if any) and generate context with correct variables, ignoring already set props. aka deeper value is more important 22 | if (repeatParents.length > 0) { 23 | // remove last char from {} or {a:b} 24 | context = context.substr(0, context.length - 1); 25 | if (context.indexOf(':') !== -1) { // is there any property in context value? e.g. {item:value} 26 | context += ','; 27 | } 28 | 29 | context += '' + repeatParents.map(function (el) { 30 | var props = []; 31 | if (existingContextProperties.indexOf(el.keyExpr) === -1) { 32 | props.push(el.keyExpr + ':' + el.keyExpr); 33 | } 34 | if (existingContextProperties.indexOf(el.valueExpr) === -1) { 35 | props.push(el.valueExpr + ':' + el.valueExpr); 36 | } 37 | return props.length > 0 ? props.join(',') : null; 38 | }).join(',') + '}'; 39 | } 40 | var expr = $(this).attr(options.prefix + '-include').trim(); 41 | if (expr.charAt(0) !== "'") { // if expression is given, try to take values from context and fallback to string value otherwise 42 | var parts = expr.split('.'); 43 | var expressions = []; 44 | for (var i = 0; i < parts.length; i++) { 45 | expressions.push(parts.slice(0, i + 1).join('.')); 46 | } 47 | $(this).append("<%= $helpers.htIncludeFunc(typeof " + parts[0] + "!=='undefined' && " + expressions.join(' && ') + " ? " + expr + " : '" + expr.replace(/'/g, "\\'") + "', data, " + context + ") %>"); 48 | } else { 49 | $(this).append("<%= $helpers.htIncludeFunc(" + expr + ", data," + context + ") %>"); 50 | } 51 | $(this).removeAttr(options.prefix + '-include'); 52 | $(this).removeAttr(options.prefix + '-include-context'); 53 | }); 54 | } 55 | 56 | IncludeDirective.init = function (data, options, angularTemplate) { 57 | data.$helpers.htIncludeFunc = function htIncludeFunc(fileName, data, context) { 58 | var includeOptions = Object.assign({}, options); 59 | var defaultDir = path.dirname(options.layoutPath); 60 | includeOptions.includeDirs = [].concat(options.includeDirs || []); 61 | if (includeOptions.includeDirs.indexOf(defaultDir) === -1) { 62 | includeOptions.includeDirs.push(defaultDir); 63 | } 64 | if (options.cache) { 65 | includeOptions.cache = options.cache + angularTemplate.cache.separator + fileName; 66 | } 67 | 68 | var includeData = context, keys, len; 69 | keys = Object.keys(data); 70 | len = keys.length; 71 | while (len--) { 72 | if (!includeData[keys[len]]) { 73 | includeData[keys[len]] = data[keys[len]]; 74 | } 75 | } 76 | 77 | var includedHtml = angularTemplate(fileName, includeData, includeOptions, true); 78 | return includedHtml; 79 | }; 80 | }; 81 | 82 | module.exports = IncludeDirective -------------------------------------------------------------------------------- /directives/repeat.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function RepeatDirective($, data, options, angularTemplate) { 4 | /** 5 | * ht-repeat expression 6 | */ 7 | var htRepeats = $("*[" + options.prefix + "-repeat]"); 8 | htRepeats.each(function (i, elem) { 9 | var expr = $(this).attr(options.prefix + '-repeat').trim(); 10 | var result = angularTemplate.helpers.parseRepeatExpression(expr); 11 | if (!result) return; 12 | var tmpName = ('_' + result.collectionName).replace(/[^a-zA-Z0-9]/g, '_'); 13 | var jsTmplStr = 14 | "<% var " + tmpName + " = " + angularTemplate.helpers.expression(result.collectionExpr, options) + ";" + 15 | " for(var " + result.keyExpr + " in " + tmpName + ") { " + 16 | " var " + result.valueExpr + " = " + tmpName + "[" + result.keyExpr + "]; %>"; 17 | 18 | $(this).before(jsTmplStr); 19 | $(this).after("<% } %>"); 20 | 21 | $(this).removeAttr(options.prefix + '-repeat'); 22 | }); 23 | } 24 | 25 | module.exports = RepeatDirective -------------------------------------------------------------------------------- /directives/show.js: -------------------------------------------------------------------------------- 1 | 2 | function ShowDirective($, data, options, angularTemplate) { 3 | /** 4 | * ht-show expression 5 | */ 6 | const selector = `${options.prefix}-show`; 7 | const htShow = $(`*[${selector}]`); 8 | htShow.each(function (i, elem) { 9 | const expr = $(this).attr(selector).trim(); 10 | $(this).before(`<% if (${expr}) { %>`); 11 | $(this).after('<% } %>'); 12 | $(this).removeAttr(selector); 13 | }); 14 | } 15 | 16 | module.exports = ShowDirective; 17 | -------------------------------------------------------------------------------- /directives/style.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function StyleDirective($, data, options, angularTemplate) { 4 | /** 5 | * ht-class expression 6 | */ 7 | var styles = $("*[" + options.prefix + "-style]"); 8 | styles.each(function (i, elem) { 9 | var expr = $(this).attr(options.prefix + '-style').trim(); 10 | var style = ($(this).attr('style') || '').trim(); 11 | if (style) { 12 | style += ';'; 13 | } 14 | 15 | $(this).removeAttr(options.prefix + '-style'); 16 | $(this).attr('style', style + '<%=$helpers.generateStyle(' + expr + ')%>'); 17 | }); 18 | } 19 | 20 | StyleDirective.init = function (data, options, angularTemplate) { 21 | data.$helpers.generateStyle = function generateStyle(input) { 22 | if (typeof (input) === 'object') { 23 | return Object.keys(input).map(function (property) { 24 | return property + ':' + input[property]; 25 | }).join(';'); 26 | } 27 | return ''; 28 | } 29 | }; 30 | 31 | module.exports = StyleDirective -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var bump = require('gulp-bump'); 3 | var shell = require('gulp-shell'); 4 | var tap = require('gulp-tap'); 5 | var gutil = require('gulp-util'); 6 | var bumpVersion = function(type) { 7 | type = type || 'patch'; 8 | var version = ''; 9 | gulp.src(['./bower.json', './package.json']) 10 | .pipe(bump({type: type})) 11 | .pipe(gulp.dest('./')) 12 | .pipe(tap(function(file, t) { 13 | version = JSON.parse(file.contents.toString()).version; 14 | })).on('end', function() { 15 | var color = gutil.colors; 16 | gulp.src('') 17 | .pipe(shell([ 18 | 'git commit --all --message "Version ' + version + '"', 19 | (type != 'patch' ? 'git tag --annotate "v' + version + '" --message "Version ' + version + '"' : 'true') 20 | ], {ignoreErrors: false})) 21 | .pipe(tap(function() { 22 | gutil.log(color.green("Version bumped to ") + color.yellow(version) + color.green(", don't forget to push!")); 23 | })); 24 | }); 25 | 26 | }; 27 | 28 | gulp.task('bump', function() { bumpVersion('patch'); }); 29 | gulp.task('bump:patch', function() { bumpVersion('patch'); }); 30 | gulp.task('bump:minor', function() { bumpVersion('minor'); }); 31 | gulp.task('bump:major', function() { bumpVersion('major'); }); 32 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var cheerio = require('cheerio'); 3 | var jsTemplate = require('js-template'); 4 | var templateRegex = new RegExp(/[<>]/); 5 | var SimpleCache = require('./lib/cache'); 6 | 7 | var angularTemplate = function (fileOrHtml, data, options, nested) { 8 | 9 | var html; 10 | options = options || {}; 11 | options.layoutPath = __filename; 12 | 13 | if (!options.locale) { 14 | options.locale = require('./locales/en_US'); 15 | } 16 | // try to reuse cached output 17 | var output = options.cache ? angularTemplate.cache.get(options.cache) : false; 18 | // namespace for all used functions within template 19 | data.$helpers = {}; 20 | // run through all supported directives and init them 21 | angularTemplate.directives.forEach(function (run) { 22 | run.init && run.init(data, options, angularTemplate); 23 | }); 24 | 25 | // bind current options to all supported pipes 26 | data.$pipes = {}; 27 | Object.keys(angularTemplate.pipes).forEach(function (pipe) { 28 | data.$pipes[pipe] = angularTemplate.pipes[pipe].bind(angularTemplate, options); 29 | }); 30 | 31 | if (!output) { 32 | // we've got a template 33 | if (templateRegex.test(fileOrHtml)) { 34 | html = fileOrHtml; 35 | } else { 36 | // we've got a file path 37 | options.layoutPath = fileOrHtml; 38 | // read template from disk 39 | html = angularTemplate.helpers.read(options.layoutPath, options); 40 | } 41 | 42 | // invoke custom function if need that manipulates html 43 | if (typeof options.preprocess === 'function') { 44 | html = options.preprocess(html); 45 | } 46 | 47 | if (!options.prefix) { 48 | options.prefix = 'ht'; 49 | } 50 | if (!options.cheerioOptions) { 51 | options.cheerioOptions = { _useHtmlParser2: true, decodeEntities: false }; 52 | } 53 | var $ = cheerio.load(html, options.cheerioOptions); 54 | 55 | // run through all supported directives 56 | angularTemplate.directives.forEach(function (run) { 57 | run($, data, options, angularTemplate); 58 | }); 59 | 60 | /** 61 | * curly-braces exprepression 62 | */ 63 | output = $.html() 64 | .replace(/<%/g, "<%") // <% 65 | .replace(/%>/g, "%>") // %> 66 | .replace(/; i </g, "; i <") // ; i < 67 | .replace(/"/g, '"') // " 68 | .replace(/'/g, "'") // ' 69 | .replace(/ && /g, " && ") // && 70 | .replace(/{{(.*?)}}/g, function (match, capture) { 71 | return "<%=" + angularTemplate.helpers.expression(capture, options) + "%>"; 72 | }) // {{ .. | pipe | pipe2}} 73 | 74 | if (options.cache) { 75 | angularTemplate.cache.put(options.cache, output); 76 | } 77 | } 78 | 79 | if (options.jsMode) { 80 | return output; 81 | } else { 82 | try { 83 | return jsTemplate(output, data); 84 | } catch (e) { 85 | if (e.raisedOnceException) { 86 | throw e.raisedOnceException; 87 | } else { 88 | var lines = output.split("\n"); 89 | for (var i = e.lineNo - 3; i < e.lineNo + 3; i++) { 90 | console.log(i + 1, lines[i]); 91 | } 92 | console.log("processing template:", options.layoutPath); 93 | console.log("error in line", e.lineNo); 94 | e.raisedOnceException = e; 95 | throw e; 96 | } 97 | } 98 | } 99 | }; 100 | 101 | // exposed prop that is used to store cached templates to avoid IO (right before calling jsTemplate) 102 | angularTemplate.cache = new SimpleCache('$$'); 103 | 104 | // list of supported and enabled directives (can be changed at runtime) 105 | angularTemplate.directives = [ 106 | require('./directives/include'), 107 | require('./directives/repeat'), 108 | require('./directives/if'), 109 | require('./directives/show'), 110 | require('./directives/hide'), 111 | require('./directives/class'), 112 | require('./directives/bind'), 113 | require('./directives/style') 114 | ]; 115 | 116 | // key/value pairs of supported pipes 117 | angularTemplate.pipes = { 118 | lowercase: require('./pipes/lowercase'), 119 | uppercase: require('./pipes/uppercase'), 120 | number: require('./pipes/number'), 121 | currency: require('./pipes/currency'), 122 | json: require('./pipes/json'), 123 | date: require('./pipes/date'), 124 | limitTo: require('./pipes/limit-to'), 125 | filter: require('./pipes/filter') 126 | }; 127 | 128 | // all internal helpers will be exposed as well and can be overriden 129 | angularTemplate.helpers = require('./lib/helpers'); 130 | 131 | module.exports = angularTemplate; 132 | -------------------------------------------------------------------------------- /lib/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function SimpleCache(separator) { 4 | this.separator = separator; 5 | this.map = {}; 6 | } 7 | 8 | SimpleCache.prototype.remove = function cacheRemove(key) { 9 | 10 | if (!key) { 11 | return; 12 | } 13 | 14 | var self = this; 15 | // find related keys and remove them 16 | Object.keys(this.map).filter(function (k) { 17 | return k === key || k.indexOf(key + self.separator) === 0; 18 | }).forEach(function (k) { 19 | delete self.map[k]; 20 | }); 21 | } 22 | 23 | SimpleCache.prototype.put = function cachePut(key, value) { 24 | this.map[key] = value; 25 | } 26 | 27 | SimpleCache.prototype.get = function cacheGet(key) { 28 | return this.map[key]; 29 | } 30 | 31 | module.exports = SimpleCache; -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | parseRepeatExpression: parseRepeatExpression, 5 | read: read, 6 | expression: expression 7 | }; 8 | 9 | var fs = require('fs'); 10 | var path = require('path'); 11 | 12 | function parseRepeatExpression(expr) { 13 | var matches = expr.match(/^(.*?) in ([^\s]*)(.*?)$/); 14 | if (!matches) return false; 15 | var keyValueExpr = matches[1].trim(); 16 | var collectionExpr = matches[2].trim(); 17 | var pipes = matches[3].trim(); 18 | // ignore if content after collection name doesn't have a pipe 19 | if (pipes && pipes.indexOf('track by') !== -1) { 20 | pipes = pipes.replace(/track by ([^\s|])*/i, ''); 21 | } 22 | var keyExpr, valueExpr, m1, m2; 23 | if (m1 = keyValueExpr.match(/^\((\w+),\s?(\w+)\)$/)) { // (k,v) 24 | keyExpr = m1[1], valueExpr = m1[2]; 25 | } else if (m2 = keyValueExpr.match(/^(\w+)$/)) { 26 | valueExpr = m2[1]; 27 | keyExpr = 'i'; 28 | } 29 | return { keyExpr: keyExpr, valueExpr: valueExpr, collectionName: collectionExpr, collectionExpr: collectionExpr + pipes }; 30 | } 31 | 32 | function read(file, options) { 33 | var html = file; // same as before, if file doesn't exist - path will be shown 34 | // absolute path 35 | if (fs.existsSync(file)) { 36 | html = fs.readFileSync(file, 'utf8'); 37 | } else if (options.includeDirs) { 38 | // relative path, check all includeDirs 39 | for (var i = 0; i < options.includeDirs.length; i++) { 40 | var filePath = path.join(options.includeDirs[i], file);//.replace(/\\/g,'/'); // have to replace \ with / or test will fail on windows 41 | if (fs.existsSync(filePath)) { 42 | html = fs.readFileSync(filePath, 'utf8'); 43 | break; 44 | } 45 | } 46 | } 47 | 48 | return html; 49 | } 50 | var RESERVED_CHARS = { 51 | '|': '#__PIPE__#', 52 | ':': '#__COLON__#' 53 | }; 54 | var ESCAPED_COLON_REGEX = /#__COLON__#/g; 55 | var ESCAPED_PIPE_REGEX = /#__PIPE__#/g; 56 | function expression(input, options) { 57 | if (input && input.indexOf('|') !== -1) { 58 | var sanitizedInput = input; 59 | // make sure that || are escaped 60 | sanitizedInput = sanitizedInput.replace(/\|\|/g, RESERVED_CHARS['|'] + RESERVED_CHARS['|']) 61 | // make sure that | or : inside quotes are escaped 62 | var pos = 0, char, inQuote; 63 | while (pos < sanitizedInput.length) { 64 | char = sanitizedInput.charAt(pos); 65 | if (inQuote) { 66 | // did we encounter reserved char that should be replaced? 67 | if (RESERVED_CHARS[char]) { 68 | sanitizedInput = sanitizedInput.substring(0, pos) + RESERVED_CHARS[char] + sanitizedInput.substring(pos + 1); 69 | pos += (RESERVED_CHARS[char].length - 1); // advance by the lenth of the replacement - 1 70 | } 71 | // found closing quote 72 | if (char === inQuote && sanitizedInput.charAt(pos - 1) !== '\\') { 73 | inQuote = null; 74 | } 75 | } else if (char === '"' || char === "'") { // found opening quote 76 | inQuote = char; 77 | } 78 | pos++; 79 | } 80 | // check if pipe is actually used 81 | if (sanitizedInput.indexOf('|') !== -1) { 82 | var steps = sanitizedInput.split('|'); 83 | var value = steps.shift().trim(); 84 | for (var i = 0; i < steps.length; i++) { 85 | var params = steps[i].split(':'); 86 | var pipe = params.shift(); 87 | params.unshift(value); 88 | value = '$pipes.' + pipe.trim() + '(' + params.join(', ') + ')'; 89 | } 90 | value = value.replace(ESCAPED_PIPE_REGEX, '|').replace(ESCAPED_COLON_REGEX, ':'); 91 | 92 | return value; 93 | } 94 | } 95 | return input; 96 | } -------------------------------------------------------------------------------- /lib/number-format.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License 3 | 4 | Copyright (c) 2010-2017 Google, Inc. http://angularjs.org 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | 24 | */ 25 | 26 | var MAX_DIGITS = 22; 27 | var DECIMAL_SEP = '.'; 28 | var ZERO_CHAR = '0'; 29 | 30 | /** 31 | * Parse a number (as a string) into three components that can be used 32 | * for formatting the number. 33 | * 34 | * (Significant bits of this parse algorithm came from https://github.com/MikeMcl/big.js/) 35 | * 36 | * @param {string} numStr The number to parse 37 | * @return {object} An object describing this number, containing the following keys: 38 | * - d : an array of digits containing leading zeros as necessary 39 | * - i : the number of the digits in `d` that are to the left of the decimal point 40 | * - e : the exponent for numbers that would need more than `MAX_DIGITS` digits in `d` 41 | * 42 | */ 43 | function parse(numStr) { 44 | var exponent = 0, digits, numberOfIntegerDigits; 45 | var i, j, zeros; 46 | 47 | // Decimal point? 48 | if ((numberOfIntegerDigits = numStr.indexOf(DECIMAL_SEP)) > -1) { 49 | numStr = numStr.replace(DECIMAL_SEP, ''); 50 | } 51 | 52 | // Exponential form? 53 | if ((i = numStr.search(/e/i)) > 0) { 54 | // Work out the exponent. 55 | if (numberOfIntegerDigits < 0) numberOfIntegerDigits = i; 56 | numberOfIntegerDigits += +numStr.slice(i + 1); 57 | numStr = numStr.substring(0, i); 58 | } else if (numberOfIntegerDigits < 0) { 59 | // There was no decimal point or exponent so it is an integer. 60 | numberOfIntegerDigits = numStr.length; 61 | } 62 | 63 | // Count the number of leading zeros. 64 | for (i = 0; numStr.charAt(i) === ZERO_CHAR; i++) { /* empty */ } 65 | 66 | if (i === (zeros = numStr.length)) { 67 | // The digits are all zero. 68 | digits = [0]; 69 | numberOfIntegerDigits = 1; 70 | } else { 71 | // Count the number of trailing zeros 72 | zeros--; 73 | while (numStr.charAt(zeros) === ZERO_CHAR) zeros--; 74 | 75 | // Trailing zeros are insignificant so ignore them 76 | numberOfIntegerDigits -= i; 77 | digits = []; 78 | // Convert string to array of digits without leading/trailing zeros. 79 | for (j = 0; i <= zeros; i++ , j++) { 80 | digits[j] = +numStr.charAt(i); 81 | } 82 | } 83 | 84 | // If the number overflows the maximum allowed digits then use an exponent. 85 | if (numberOfIntegerDigits > MAX_DIGITS) { 86 | digits = digits.splice(0, MAX_DIGITS - 1); 87 | exponent = numberOfIntegerDigits - 1; 88 | numberOfIntegerDigits = 1; 89 | } 90 | 91 | return { d: digits, e: exponent, i: numberOfIntegerDigits }; 92 | } 93 | 94 | /** 95 | * Round the parsed number to the specified number of decimal places 96 | * This function changed the parsedNumber in-place 97 | */ 98 | function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) { 99 | var digits = parsedNumber.d; 100 | var fractionLen = digits.length - parsedNumber.i; 101 | 102 | // determine fractionSize if it is not specified; `+fractionSize` converts it to a number 103 | fractionSize = (typeof (fractionSize) === 'undefined') ? Math.min(Math.max(minFrac, fractionLen), maxFrac) : +fractionSize; 104 | 105 | // The index of the digit to where rounding is to occur 106 | var roundAt = fractionSize + parsedNumber.i; 107 | var digit = digits[roundAt]; 108 | 109 | if (roundAt > 0) { 110 | // Drop fractional digits beyond `roundAt` 111 | digits.splice(Math.max(parsedNumber.i, roundAt)); 112 | 113 | // Set non-fractional digits beyond `roundAt` to 0 114 | for (var j = roundAt; j < digits.length; j++) { 115 | digits[j] = 0; 116 | } 117 | } else { 118 | // We rounded to zero so reset the parsedNumber 119 | fractionLen = Math.max(0, fractionLen); 120 | parsedNumber.i = 1; 121 | digits.length = Math.max(1, roundAt = fractionSize + 1); 122 | digits[0] = 0; 123 | for (var i = 1; i < roundAt; i++) digits[i] = 0; 124 | } 125 | 126 | if (digit >= 5) { 127 | if (roundAt - 1 < 0) { 128 | for (var k = 0; k > roundAt; k--) { 129 | digits.unshift(0); 130 | parsedNumber.i++; 131 | } 132 | digits.unshift(1); 133 | parsedNumber.i++; 134 | } else { 135 | digits[roundAt - 1]++; 136 | } 137 | } 138 | 139 | // Pad out with zeros to get the required fraction length 140 | for (; fractionLen < Math.max(0, fractionSize); fractionLen++) digits.push(0); 141 | 142 | 143 | // Do any carrying, e.g. a digit was rounded up to 10 144 | var carry = digits.reduceRight(function (carry, d, i, digits) { 145 | d = d + carry; 146 | digits[i] = d % 10; 147 | return Math.floor(d / 10); 148 | }, 0); 149 | if (carry) { 150 | digits.unshift(carry); 151 | parsedNumber.i++; 152 | } 153 | } 154 | 155 | /** 156 | * Format a number into a string 157 | * @param {number} number The number to format 158 | * @param {{ 159 | * minFrac, // the minimum number of digits required in the fraction part of the number 160 | * maxFrac, // the maximum number of digits required in the fraction part of the number 161 | * gSize, // number of digits in each group of separated digits 162 | * lgSize, // number of digits in the last group of digits before the decimal separator 163 | * negPre, // the string to go in front of a negative number (e.g. `-` or `(`)) 164 | * posPre, // the string to go in front of a positive number 165 | * negSuf, // the string to go after a negative number (e.g. `)`) 166 | * posSuf // the string to go after a positive number 167 | * }} pattern 168 | * @param {string} groupSep The string to separate groups of number (e.g. `,`) 169 | * @param {string} decimalSep The string to act as the decimal separator (e.g. `.`) 170 | * @param {[type]} fractionSize The size of the fractional part of the number 171 | * @return {string} The number formatted as a string 172 | */ 173 | function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { 174 | 175 | if (!(typeof (number) === 'string' || typeof (number) === 'number') || isNaN(number)) return ''; 176 | 177 | var isInfinity = !isFinite(number); 178 | var isZero = false; 179 | var numStr = Math.abs(number) + '', 180 | formattedText = '', 181 | parsedNumber; 182 | 183 | if (isInfinity) { 184 | formattedText = '\u221e'; 185 | } else { 186 | parsedNumber = parse(numStr); 187 | 188 | roundNumber(parsedNumber, fractionSize, pattern.minFrac, pattern.maxFrac); 189 | 190 | var digits = parsedNumber.d; 191 | var integerLen = parsedNumber.i; 192 | var exponent = parsedNumber.e; 193 | var decimals = []; 194 | isZero = digits.reduce(function (isZero, d) { return isZero && !d; }, true); 195 | 196 | // pad zeros for small numbers 197 | while (integerLen < 0) { 198 | digits.unshift(0); 199 | integerLen++; 200 | } 201 | 202 | // extract decimals digits 203 | if (integerLen > 0) { 204 | decimals = digits.splice(integerLen, digits.length); 205 | } else { 206 | decimals = digits; 207 | digits = [0]; 208 | } 209 | 210 | // format the integer digits with grouping separators 211 | var groups = []; 212 | if (digits.length >= pattern.lgSize) { 213 | groups.unshift(digits.splice(-pattern.lgSize, digits.length).join('')); 214 | } 215 | while (digits.length > pattern.gSize) { 216 | groups.unshift(digits.splice(-pattern.gSize, digits.length).join('')); 217 | } 218 | if (digits.length) { 219 | groups.unshift(digits.join('')); 220 | } 221 | formattedText = groups.join(groupSep); 222 | 223 | // append the decimal digits 224 | if (decimals.length) { 225 | formattedText += decimalSep + decimals.join(''); 226 | } 227 | 228 | if (exponent) { 229 | formattedText += 'e+' + exponent; 230 | } 231 | } 232 | if (number < 0 && !isZero) { 233 | return pattern.negPre + formattedText + pattern.negSuf; 234 | } else { 235 | return pattern.posPre + formattedText + pattern.posSuf; 236 | } 237 | } 238 | 239 | module.exports = formatNumber; -------------------------------------------------------------------------------- /locales/en_US.js: -------------------------------------------------------------------------------- 1 | // taken from angularjs 2 | module.exports = { 3 | "DATETIME_FORMATS": { 4 | "AMPMS": [ 5 | "AM", 6 | "PM" 7 | ], 8 | "DAY": [ 9 | "Sunday", 10 | "Monday", 11 | "Tuesday", 12 | "Wednesday", 13 | "Thursday", 14 | "Friday", 15 | "Saturday" 16 | ], 17 | "ERANAMES": [ 18 | "Before Christ", 19 | "Anno Domini" 20 | ], 21 | "ERAS": [ 22 | "BC", 23 | "AD" 24 | ], 25 | "FIRSTDAYOFWEEK": 6, 26 | "MONTH": [ 27 | "January", 28 | "February", 29 | "March", 30 | "April", 31 | "May", 32 | "June", 33 | "July", 34 | "August", 35 | "September", 36 | "October", 37 | "November", 38 | "December" 39 | ], 40 | "SHORTDAY": [ 41 | "Sun", 42 | "Mon", 43 | "Tue", 44 | "Wed", 45 | "Thu", 46 | "Fri", 47 | "Sat" 48 | ], 49 | "SHORTMONTH": [ 50 | "Jan", 51 | "Feb", 52 | "Mar", 53 | "Apr", 54 | "May", 55 | "Jun", 56 | "Jul", 57 | "Aug", 58 | "Sep", 59 | "Oct", 60 | "Nov", 61 | "Dec" 62 | ], 63 | "STANDALONEMONTH": [ 64 | "January", 65 | "February", 66 | "March", 67 | "April", 68 | "May", 69 | "June", 70 | "July", 71 | "August", 72 | "September", 73 | "October", 74 | "November", 75 | "December" 76 | ], 77 | "WEEKENDRANGE": [ 78 | 5, 79 | 6 80 | ], 81 | "fullDate": "EEEE, MMMM d, y", 82 | "longDate": "MMMM d, y", 83 | "medium": "MMM d, y h:mm:ss a", 84 | "mediumDate": "MMM d, y", 85 | "mediumTime": "h:mm:ss a", 86 | "short": "M/d/yy h:mm a", 87 | "shortDate": "M/d/yy", 88 | "shortTime": "h:mm a" 89 | }, 90 | "NUMBER_FORMATS": { 91 | "CURRENCY_SYM": "$", 92 | "DECIMAL_SEP": ".", 93 | "GROUP_SEP": ",", 94 | "PATTERNS": [ 95 | { 96 | "gSize": 3, 97 | "lgSize": 3, 98 | "maxFrac": 3, 99 | "minFrac": 0, 100 | "minInt": 1, 101 | "negPre": "-", 102 | "negSuf": "", 103 | "posPre": "", 104 | "posSuf": "" 105 | }, 106 | { 107 | "gSize": 3, 108 | "lgSize": 3, 109 | "maxFrac": 2, 110 | "minFrac": 2, 111 | "minInt": 1, 112 | "negPre": "-\u00a4", 113 | "negSuf": "", 114 | "posPre": "\u00a4", 115 | "posSuf": "" 116 | } 117 | ] 118 | }, 119 | "id": "en-us", 120 | "localeID": "en_US" 121 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-template", 3 | "version": "2.4.0", 4 | "description": "Angular-Like HTML Template", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jasmine" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:allenhwkim/angular-template.git" 12 | }, 13 | "author": { 14 | "name": "Allen Kim", 15 | "email": "allenhwkim@gmail.com" 16 | }, 17 | "contributors": [ 18 | { 19 | "name": "Domas Trijonis", 20 | "email": "domas.trijonis@gmail.com" 21 | }, 22 | { 23 | "name": "Fabiel León", 24 | "email": "fabiel.leon.oliva@gmail.com" 25 | } 26 | ], 27 | "license": "MIT", 28 | "dependencies": { 29 | "cheerio": "^1.0.0-rc.2", 30 | "js-template": "~0.1.4" 31 | }, 32 | "devDependencies": { 33 | "gulp": "~3.9.1", 34 | "gulp-bump": "~2.7.0", 35 | "gulp-shell": "~0.6.3", 36 | "gulp-tap": "~1.0.1", 37 | "gulp-util": "~3.0.8", 38 | "jasmine": "^2.7.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pipes/currency.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License 3 | 4 | Copyright (c) 2010-2017 Google, Inc. http://angularjs.org 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | 24 | */ 25 | var formatNumber = require('../lib/number-format'); 26 | 27 | module.exports = function currency(options, amount, currencySymbol, fractionSize) { 28 | var formats = options.locale.NUMBER_FORMATS; 29 | if (typeof (currencySymbol) === 'undefined') { 30 | currencySymbol = formats.CURRENCY_SYM; 31 | } 32 | 33 | if (typeof (fractionSize) === 'undefined') { 34 | fractionSize = formats.PATTERNS[1].maxFrac; 35 | } 36 | // if null or undefined pass it through 37 | return (amount == null) 38 | ? amount 39 | : formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, fractionSize). 40 | replace(/\u00A4/g, currencySymbol); 41 | } -------------------------------------------------------------------------------- /pipes/date.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License 3 | 4 | Copyright (c) 2010-2017 Google, Inc. http://angularjs.org 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | 24 | */ 25 | var DATE_FORMATS = { 26 | yyyy: dateGetter('FullYear', 4, 0, false, true), 27 | yy: dateGetter('FullYear', 2, 0, true, true), 28 | y: dateGetter('FullYear', 1, 0, false, true), 29 | MMMM: dateStrGetter('Month'), 30 | MMM: dateStrGetter('Month', true), 31 | MM: dateGetter('Month', 2, 1), 32 | M: dateGetter('Month', 1, 1), 33 | LLLL: dateStrGetter('Month', false, true), 34 | dd: dateGetter('Date', 2), 35 | d: dateGetter('Date', 1), 36 | HH: dateGetter('Hours', 2), 37 | H: dateGetter('Hours', 1), 38 | hh: dateGetter('Hours', 2, -12), 39 | h: dateGetter('Hours', 1, -12), 40 | mm: dateGetter('Minutes', 2), 41 | m: dateGetter('Minutes', 1), 42 | ss: dateGetter('Seconds', 2), 43 | s: dateGetter('Seconds', 1), 44 | // while ISO 8601 requires fractions to be prefixed with `.` or `,` 45 | // we can be just safely rely on using `sss` since we currently don't support single or two digit fractions 46 | sss: dateGetter('Milliseconds', 3), 47 | EEEE: dateStrGetter('Day'), 48 | EEE: dateStrGetter('Day', true), 49 | a: ampmGetter, 50 | Z: timeZoneGetter, 51 | ww: weekGetter(2), 52 | w: weekGetter(1), 53 | G: eraGetter, 54 | GG: eraGetter, 55 | GGG: eraGetter, 56 | GGGG: longEraGetter 57 | }; 58 | var ZERO_CHAR = '0'; 59 | 60 | var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))([\s\S]*)/, 61 | NUMBER_STRING = /^-?\d+$/; 62 | 63 | module.exports = function (options, date, format, timezone) { 64 | var $locale = options.locale; 65 | var text = '', 66 | parts = [], 67 | fn, match; 68 | 69 | format = format || 'mediumDate'; 70 | format = $locale.DATETIME_FORMATS[format] || format; 71 | if (typeof (date) === 'string') { 72 | date = NUMBER_STRING.test(date) ? parseInt(date, 10) : jsonStringToDate(date); 73 | } 74 | 75 | if (typeof (date) === 'number') { 76 | date = new Date(date); 77 | } 78 | 79 | if (!isDate(date) || !isFinite(date.getTime())) { 80 | return date; 81 | } 82 | 83 | while (format) { 84 | match = DATE_FORMATS_SPLIT.exec(format); 85 | if (match) { 86 | parts = concat(parts, match, 1); 87 | format = parts.pop(); 88 | } else { 89 | parts.push(format); 90 | format = null; 91 | } 92 | } 93 | 94 | var dateTimezoneOffset = date.getTimezoneOffset(); 95 | if (timezone) { 96 | dateTimezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); 97 | date = convertTimezoneToLocal(date, timezone, true); 98 | } 99 | parts.forEach(function (value) { 100 | fn = DATE_FORMATS[value]; 101 | text += fn ? fn(date, $locale.DATETIME_FORMATS, dateTimezoneOffset) 102 | : value === '\'\'' ? '\'' : value.replace(/(^'|'$)/g, '').replace(/''/g, '\''); 103 | }); 104 | 105 | return text; 106 | }; 107 | 108 | function isDate(value) { 109 | return Object.prototype.toString.call(value) === '[object Date]'; 110 | } 111 | 112 | function concat(array1, array2, index) { 113 | return array1.concat(array2.slice(index)); 114 | } 115 | 116 | var R_ISO8601_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/; 117 | // 1 2 3 4 5 6 7 8 9 10 11 118 | function jsonStringToDate(string) { 119 | var match; 120 | if ((match = string.match(R_ISO8601_STR))) { 121 | var date = new Date(0), 122 | tzHour = 0, 123 | tzMin = 0, 124 | dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear, 125 | timeSetter = match[8] ? date.setUTCHours : date.setHours; 126 | 127 | if (match[9]) { 128 | tzHour = parseInt(match[9] + match[10], 10); 129 | tzMin = parseInt(match[9] + match[11]), 10; 130 | } 131 | dateSetter.call(date, parseInt(match[1], 10), parseInt(match[2], 10) - 1, parseInt(match[3], 10)); 132 | var h = parseInt(match[4] || 0, 10) - tzHour; 133 | var m = parseInt(match[5] || 0, 10) - tzMin; 134 | var s = parseInt(match[6] || 0, 10); 135 | var ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000); 136 | timeSetter.call(date, h, m, s, ms); 137 | return date; 138 | } 139 | return string; 140 | } 141 | 142 | function padNumber(num, digits, trim, negWrap) { 143 | var neg = ''; 144 | if (num < 0 || (negWrap && num <= 0)) { 145 | if (negWrap) { 146 | num = -num + 1; 147 | } else { 148 | num = -num; 149 | neg = '-'; 150 | } 151 | } 152 | num = '' + num; 153 | while (num.length < digits) num = ZERO_CHAR + num; 154 | if (trim) { 155 | num = num.substr(num.length - digits); 156 | } 157 | return neg + num; 158 | } 159 | 160 | function dateGetter(name, size, offset, trim, negWrap) { 161 | offset = offset || 0; 162 | return function (date) { 163 | var value = date['get' + name](); 164 | if (offset > 0 || value > -offset) { 165 | value += offset; 166 | } 167 | if (value === 0 && offset === -12) value = 12; 168 | return padNumber(value, size, trim, negWrap); 169 | }; 170 | } 171 | 172 | function dateStrGetter(name, shortForm, standAlone) { 173 | return function (date, formats) { 174 | var value = date['get' + name](); 175 | var propPrefix = (standAlone ? 'STANDALONE' : '') + (shortForm ? 'SHORT' : ''); 176 | var get = uppercase(propPrefix + name); 177 | 178 | return formats[get][value]; 179 | }; 180 | } 181 | 182 | function uppercase (string) { return typeof string === 'string' ? string.toUpperCase() : string; }; 183 | 184 | function timeZoneGetter(date, formats, offset) { 185 | var zone = -1 * offset; 186 | var paddedZone = (zone >= 0) ? '+' : ''; 187 | 188 | paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) + 189 | padNumber(Math.abs(zone % 60), 2); 190 | 191 | return paddedZone; 192 | } 193 | 194 | function getFirstThursdayOfYear(year) { 195 | // 0 = index of January 196 | var dayOfWeekOnFirst = (new Date(year, 0, 1)).getDay(); 197 | // 4 = index of Thursday (+1 to account for 1st = 5) 198 | // 11 = index of *next* Thursday (+1 account for 1st = 12) 199 | return new Date(year, 0, ((dayOfWeekOnFirst <= 4) ? 5 : 12) - dayOfWeekOnFirst); 200 | } 201 | 202 | function getThursdayThisWeek(datetime) { 203 | return new Date(datetime.getFullYear(), datetime.getMonth(), 204 | // 4 = index of Thursday 205 | datetime.getDate() + (4 - datetime.getDay())); 206 | } 207 | 208 | function weekGetter(size) { 209 | return function (date) { 210 | var firstThurs = getFirstThursdayOfYear(date.getFullYear()), 211 | thisThurs = getThursdayThisWeek(date); 212 | 213 | var diff = +thisThurs - +firstThurs, 214 | result = 1 + Math.round(diff / 6.048e8); // 6.048e8 ms per week 215 | 216 | return padNumber(result, size); 217 | }; 218 | } 219 | 220 | function ampmGetter(date, formats) { 221 | return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1]; 222 | } 223 | 224 | function eraGetter(date, formats) { 225 | return date.getFullYear() <= 0 ? formats.ERAS[0] : formats.ERAS[1]; 226 | } 227 | 228 | function longEraGetter(date, formats) { 229 | return date.getFullYear() <= 0 ? formats.ERANAMES[0] : formats.ERANAMES[1]; 230 | } 231 | 232 | var ALL_COLONS = /:/g; 233 | function timezoneToOffset(timezone, fallback) { 234 | // Support: IE 9-11 only, Edge 13-15+ 235 | // IE/Edge do not "understand" colon (`:`) in timezone 236 | timezone = timezone.replace(ALL_COLONS, ''); 237 | var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; 238 | return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; 239 | } 240 | 241 | 242 | function addDateMinutes(date, minutes) { 243 | date = new Date(date.getTime()); 244 | date.setMinutes(date.getMinutes() + minutes); 245 | return date; 246 | } 247 | 248 | 249 | function convertTimezoneToLocal(date, timezone, reverse) { 250 | reverse = reverse ? -1 : 1; 251 | var dateTimezoneOffset = date.getTimezoneOffset(); 252 | var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); 253 | return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset)); 254 | } 255 | -------------------------------------------------------------------------------- /pipes/filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* 3 | The MIT License 4 | 5 | Copyright (c) 2010-2017 Google, Inc. http://angularjs.org 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | 25 | */ 26 | 27 | module.exports = function filter(options, array, expression, comparator, anyPropertyKey) { 28 | if (!Array.isArray(array)) { 29 | if (array == null) { 30 | return array; 31 | } else { 32 | throw new Error('filter only works with arrays'); 33 | } 34 | } 35 | 36 | anyPropertyKey = anyPropertyKey || '$'; 37 | var expressionType = getTypeForFilter(expression); 38 | var predicateFn; 39 | var matchAgainstAnyProp; 40 | 41 | switch (expressionType) { 42 | case 'function': 43 | predicateFn = expression; 44 | break; 45 | case 'boolean': 46 | case 'null': 47 | case 'number': 48 | case 'string': 49 | matchAgainstAnyProp = true; 50 | // falls through 51 | case 'object': 52 | predicateFn = createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp); 53 | break; 54 | default: 55 | return array; 56 | } 57 | 58 | return Array.prototype.filter.call(array, predicateFn); 59 | }; 60 | 61 | // Helper functions for `filterFilter` 62 | function createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp) { 63 | var shouldMatchPrimitives = typeof (expression) == 'object' && (anyPropertyKey in expression); 64 | var predicateFn; 65 | 66 | if (comparator === true) { 67 | comparator = equals; 68 | } else if (!isFunction(comparator)) { 69 | comparator = function (actual, expected) { 70 | if (typeof (actual) === 'undefined') { 71 | // No substring matching against `undefined` 72 | return false; 73 | } 74 | if ((actual === null) || (expected === null)) { 75 | // No substring matching against `null`; only match against `null` 76 | return actual === expected; 77 | } 78 | if (typeof (expected) === 'object' || (typeof (actual) === 'object' && !hasCustomToString(actual))) { 79 | // Should not compare primitives against objects, unless they have custom `toString` method 80 | return false; 81 | } 82 | 83 | actual = ('' + actual).toLowerCase(); 84 | expected = ('' + expected).toLowerCase(); 85 | return actual.indexOf(expected) !== -1; 86 | }; 87 | } 88 | 89 | predicateFn = function (item) { 90 | if (shouldMatchPrimitives && typeof (item) !== 'object') { 91 | return deepCompare(item, expression[anyPropertyKey], comparator, anyPropertyKey, false); 92 | } 93 | return deepCompare(item, expression, comparator, anyPropertyKey, matchAgainstAnyProp); 94 | }; 95 | 96 | return predicateFn; 97 | } 98 | 99 | function deepCompare(actual, expected, comparator, anyPropertyKey, matchAgainstAnyProp, dontMatchWholeObject) { 100 | var actualType = getTypeForFilter(actual); 101 | var expectedType = getTypeForFilter(expected); 102 | 103 | if ((expectedType === 'string') && (expected.charAt(0) === '!')) { 104 | return !deepCompare(actual, expected.substring(1), comparator, anyPropertyKey, matchAgainstAnyProp); 105 | } else if (isArray(actual)) { 106 | // In case `actual` is an array, consider it a match 107 | // if ANY of it's items matches `expected` 108 | return actual.some(function (item) { 109 | return deepCompare(item, expected, comparator, anyPropertyKey, matchAgainstAnyProp); 110 | }); 111 | } 112 | 113 | switch (actualType) { 114 | case 'object': 115 | var key; 116 | if (matchAgainstAnyProp) { 117 | for (key in actual) { 118 | // Under certain, rare, circumstances, key may not be a string and `charAt` will be undefined 119 | // See: https://github.com/angular/angular.js/issues/15644 120 | if (key.charAt && (key.charAt(0) !== '$') && 121 | deepCompare(actual[key], expected, comparator, anyPropertyKey, true)) { 122 | return true; 123 | } 124 | } 125 | return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, anyPropertyKey, false); 126 | } else if (expectedType === 'object') { 127 | for (key in expected) { 128 | var expectedVal = expected[key]; 129 | if (typeof (expectedVal) === 'function' || typeof (expectedVal) === 'undefined') { 130 | continue; 131 | } 132 | 133 | var matchAnyProperty = key === anyPropertyKey; 134 | var actualVal = matchAnyProperty ? actual : actual[key]; 135 | if (!deepCompare(actualVal, expectedVal, comparator, anyPropertyKey, matchAnyProperty, matchAnyProperty)) { 136 | return false; 137 | } 138 | } 139 | return true; 140 | } else { 141 | return comparator(actual, expected); 142 | } 143 | case 'function': 144 | return false; 145 | default: 146 | return comparator(actual, expected); 147 | } 148 | } 149 | 150 | // Used for easily differentiating between `null` and actual `object` 151 | function getTypeForFilter(val) { 152 | return (val === null) ? 'null' : typeof val; 153 | } -------------------------------------------------------------------------------- /pipes/json.js: -------------------------------------------------------------------------------- 1 | module.exports = function json(options, value, spacing) { 2 | if (spacing === undefined) { 3 | spacing = 2; 4 | } 5 | return value == null ? value : JSON.stringify(value, undefined, spacing); 6 | } -------------------------------------------------------------------------------- /pipes/limit-to.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* 3 | The MIT License 4 | 5 | Copyright (c) 2010-2017 Google, Inc. http://angularjs.org 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | 25 | */ 26 | module.exports = function limitTo(options, input, limit, begin) { 27 | if (Math.abs(Number(limit)) === Infinity) { 28 | limit = Number(limit); 29 | } else { 30 | limit = parseInt(limit, 10); 31 | } 32 | if (isNaN(limit)) return input; 33 | 34 | if (typeof (input) === 'number') input = input.toString(); 35 | 36 | begin = (!begin || isNaN(begin)) ? 0 : parseInt(begin, 10); 37 | begin = (begin < 0) ? Math.max(0, input.length + begin) : begin; 38 | 39 | if (limit >= 0) { 40 | return sliceFn(input, begin, begin + limit); 41 | } else { 42 | if (begin === 0) { 43 | return sliceFn(input, limit, input.length); 44 | } else { 45 | return sliceFn(input, Math.max(0, begin + limit), begin); 46 | } 47 | } 48 | }; 49 | 50 | function sliceFn(input, begin, end) { 51 | if (typeof (input) === 'string') return input.slice(begin, end); 52 | 53 | return Array.prototype.slice.call(input, begin, end); 54 | } -------------------------------------------------------------------------------- /pipes/lowercase.js: -------------------------------------------------------------------------------- 1 | module.exports = function lowercase(options, value, fractionSize) { 2 | return value == null ? value : String(value).toLowerCase(); 3 | } -------------------------------------------------------------------------------- /pipes/number.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License 3 | 4 | Copyright (c) 2010-2017 Google, Inc. http://angularjs.org 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | 24 | */ 25 | var formatNumber = require('../lib/number-format'); 26 | 27 | module.exports = function number(options, number, fractionSize) { 28 | var formats = options.locale.NUMBER_FORMATS; 29 | // if null or undefined pass it through 30 | return (number == null) 31 | ? number 32 | : formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, 33 | fractionSize); 34 | } 35 | -------------------------------------------------------------------------------- /pipes/uppercase.js: -------------------------------------------------------------------------------- 1 | module.exports = function uppercase(options, value, fractionSize) { 2 | return value == null ? value : String(value).toUpperCase(); 3 | } -------------------------------------------------------------------------------- /spec/includes/medium.html: -------------------------------------------------------------------------------- 1 |
    {{item.content}}
    2 | -------------------------------------------------------------------------------- /spec/includes/small.html: -------------------------------------------------------------------------------- 1 |
    {{item.content}}
    2 | -------------------------------------------------------------------------------- /spec/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: {{ title }} 6 | 7 | 8 | 9 | 10 | 11 | 23 | 24 |

    25 | {{ title }} 26 | source 27 |

    28 | 29 |
    30 |
    31 |
    {{ code }}
    32 |
    33 |
    34 |
    35 | {{ data.readme }} 36 |
    37 |
    38 |
    39 |
    40 |
    41 |
    42 | 43 | 49 | 50 | 51 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /spec/partial.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 | {{ description }} 6 |
    7 |
    8 |
    9 |
    Dependencies:
    10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 |
    NameTypeDescription
    {{ param.name }} 18 | {{ param.type && param.type.names[0] }} 19 | {{ param.description }}
    24 |
    25 |
    26 |
    Properties:
    27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | 40 |
    NameTypeDescription
    {{ prop.name }} 35 | {{ prop.type && prop.type.names[0] }} 36 | {{ prop.description }}
    41 |
    42 |
    43 |
    Attributes:
    44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 57 |
    NameTypeDescription
    {{ attr.name }} 52 | {{ attr.type && attr.type.names[0] }} 53 | {{ marked(attr.description||'') }}
    58 |
    59 |
    60 |
    Dependencies
    61 |
      62 |
    • {{ el }} Service
    • 63 |
    64 | 66 |
    Example
    67 |
    {{ example.code }}
    68 |
    69 |
    70 |
    71 | 72 |
    73 |
    74 |
    75 |

    Members

    76 |
    77 |
    78 | 79 |

    {{ member.name }}

    80 |
    81 |
    82 |
    83 |
    {{ member.description }}
    84 |
    85 |
    86 |
    87 |
    88 |
    89 |
    90 |

    Methods

    91 |
    92 |
    93 | 94 |

    95 | {{ func.name }} 96 | ({{ func.params.map(function(param){return param.name;}).join(", ") }}) 97 | 98 | -> {{ func.returns[0].type.names[0] }} 99 | 100 |

    101 |
    102 |
    103 |
    104 |
    105 | {{ func.description }} 106 |
    107 |
    108 |
    Example
    109 |
    {{ example.code }}
    110 |
    111 |
    112 |
    Parameters:
    113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 123 | 124 | 125 | 126 |
    NameTypeDescription
    {{ param.name }} 121 | {{ param.type && param.type.names[0] }} 122 | {{ param.description }}
    127 |
    128 |
    129 |
    Returns:
    130 |
    {{ func.returns[0].description }}
    131 |
    132 |
    133 |
    134 |
    135 |
    136 | -------------------------------------------------------------------------------- /spec/small.html: -------------------------------------------------------------------------------- 1 | {{item.content}} 2 | -------------------------------------------------------------------------------- /spec/spec.js: -------------------------------------------------------------------------------- 1 | var tmpl, data, expectedOutput, output; 2 | var ht = require('../index.js'); 3 | 4 | describe("ht", () => { 5 | 6 | it("expressions", () => { 7 | /******************************************************* 8 | * `{{}}` expression test 9 | *******************************************************/ 10 | expect(ht("{{foo}}", { foo: 1 })).toEqual("1"); 11 | expect(ht("{{x.foo}}", { x: { foo: 1 } })).toEqual("1"); 12 | expect(ht("{{x.foo}} {{ x.bar }}", { x: { foo: 1, bar: 2 } })).toEqual("1 2"); 13 | }); 14 | 15 | describe("pipes", () => { 16 | 17 | it('lowercase/uppercase', () => { 18 | expect(ht("{{x.foo | lowercase }} {{ x.bar | lowercase }}", { x: { foo: 'A', bar: 'B' } })).toEqual("a b"); 19 | }); 20 | 21 | it('limitTo', () => { 22 | expect(ht("{{value | limitTo:2 }}", { value: "abcd" })).toEqual("ab"); 23 | expect(ht("{{value | limitTo:2:1 }}", { value: "abcd" })).toEqual("bc"); 24 | expect(ht("{{value | limitTo }}", { value: "abcd" })).toEqual("abcd"); 25 | expect(ht("{{[1, 2, 3, 4] | limitTo:2:2 | json:0 }}", {})).toEqual("[3,4]"); 26 | }); 27 | 28 | it('number', () => { 29 | expect(ht("{{num | number:2 }}", { num: 51.2534 })).toEqual("51.25"); 30 | }); 31 | 32 | it('currency', () => { 33 | expect(ht("{{num | currency:'':2 }}", { num: 51.2534 })).toEqual("51.25"); 34 | expect(ht("{{num | currency }}", { num: 51.2534 })).toEqual("$51.25"); 35 | expect(ht("{{num | currency:undefined:0 }}", { num: 51.2534 })).toEqual("$51"); 36 | }); 37 | 38 | it('date', () => { 39 | expect(ht("{{1288323623006 | date:'medium':'+0200' }}", {})).toEqual("Oct 29, 2010 5:40:23 AM"); 40 | expect(ht("{{1288323623006 | date:\"MM/dd/yyyy 'at' h:mma\":'+0200' }}", {})).toEqual("10/29/2010 at 5:40AM"); 41 | expect(ht("{{1288323623006 | date:'MM/dd/yyyy \\'a:t\\' h:mma':'+0200' }}", {})).toEqual("10/29/2010 a:t 5:40AM"); 42 | expect(ht("{{value | date:\"dd-MM-yyyy '|' h:mma\":'+0200' }}", { value: new Date("2017-09-25T11:00:50.691Z") })).toEqual("25-09-2017 | 1:00PM"); 43 | expect(ht("{{'2018-01-14T22:26:59.680Z' | date:'MM/dd/yyyy \\'a:t\\' h:mma':'+0000' }}", {})).toEqual("01/14/2018 a:t 10:26PM"); 44 | }); 45 | 46 | it('json', () => { 47 | expect(ht("{{value | json:0 }}", { value: { a: 1 } })).toEqual('{"a":1}'); 48 | }); 49 | 50 | it('custom', () => { 51 | ht.pipes.or = function (options, value, param) { 52 | return value ? value : param; 53 | }; 54 | expect(ht("{{value | or: '12|4|b' | uppercase }}", { value: false })).toEqual("12|4|B"); 55 | }); 56 | }); 57 | 58 | it("advanced expressions", () => { 59 | /******************************************************* 60 | * advanced expressions test 61 | *******************************************************/ 62 | expect(ht("
    YES
    ", { x: { foo: 10, bar: 11 } })).toEqual(""); 63 | expect(ht("
    YES
    ", { x: { foo: true } })).toEqual("
    YES
    "); 64 | expect(ht("
    NO
    ", { x: { foo: 'foo', bar: 'bar' } })).toEqual("
    NO
    "); 65 | }); 66 | 67 | it("if", () => { 68 | /******************************************************* 69 | * `ht-if` expression test 70 | *******************************************************/ 71 | expect(ht("
    YES
    ", { x: { foo: true } })).toEqual("
    YES
    "); 72 | expect(ht("
    YES
    ", { x: { foo: true } })).toEqual(""); 73 | expect(ht("
    NO
    ", { x: { foo: true } })).toEqual("
    NO
    "); 74 | }); 75 | 76 | it("show", () => { 77 | /******************************************************* 78 | * `ht-show` expression test 79 | *******************************************************/ 80 | expect(ht("
    YES
    ", { x: { foo: true } })).toEqual("
    YES
    "); 81 | expect(ht("
    YES
    ", { x: { foo: true } })).toEqual(""); 82 | expect(ht("
    NO
    ", { x: { foo: true } })).toEqual("
    NO
    "); 83 | }); 84 | 85 | 86 | it("hide", () => { 87 | /******************************************************* 88 | * `ht-hide` expression test 89 | *******************************************************/ 90 | expect(ht("
    YES
    ", { x: { foo: true } })).toEqual(""); 91 | expect(ht("
    YES
    ", { x: { foo: true } })).toEqual("
    YES
    "); 92 | expect(ht("
    NO
    ", { x: { foo: true } })).toEqual(""); 93 | }); 94 | 95 | it("repeat", () => { 96 | /******************************************************* 97 | * `ht-repeat` expression test 98 | *******************************************************/ 99 | expect(ht("
  • {{el}}
  • ", {})).toEqual("
  • 1
  • 2
  • 3
  • "); 100 | expect(ht("
  • {{el}}
  • ", { list: [1, 2, 3] })).toEqual("
  • 1
  • 2
  • 3
  • "); 101 | expect(ht( 102 | "
  • {{v}}
  • ", 103 | { list: { a: 1, b: 2, c: 3 } } 104 | )).toEqual("
  • 1
  • 2
  • 3
  • "); 105 | 106 | expect(ht( 107 | "
  • {{v}}
  • ", 108 | { list: { a: 1, b: 2, c: 3 } } 109 | )).toEqual("
  • 1
  • 2
  • 3
  • "); 110 | 111 | expect(ht( 112 | "
  • {{k}}{{v}}
  • ", 113 | { list: { a: 1, b: 2, c: 3 } } 114 | )).toEqual("
  • a1
  • b2
  • c3
  • "); 115 | 116 | expect(ht( 117 | "{{i}}", 118 | { list: [0, 1, 2, 3] } 119 | )).toEqual("123"); 120 | 121 | expect(ht( 122 | "{{i}}", 123 | { list: [0, 1, 2, 3] } 124 | )).toEqual("12"); 125 | 126 | expect(ht( 127 | "{{i}}", 128 | { list: [0, 1, 2, 3], filterFn: (v) => v > 0 } 129 | )).toEqual("123"); 130 | 131 | expect(ht( 132 | "{{i}}", 133 | { list: [0, 1, 2, 3], filterFn: (v) => v > 0 } 134 | )).toEqual("123"); 135 | 136 | expect(ht( 137 | "{{i}}", 138 | { list: [0, 1, 2, 3], filterFn: (v) => v > 0 } 139 | )).toEqual("23"); 140 | }); 141 | 142 | it('class', () => { 143 | /******************************************************* 144 | * `ht-class` expression test 145 | *******************************************************/ 146 | expect(ht("
    YES
    ", { item: { classes: 'highlight' } })).toEqual("
    YES
    "); 147 | expect(ht("
    YES
    ", { item: { classes: { highlight: true } } })).toEqual("
    YES
    "); 148 | expect(ht("
    YES
    ", { item: { classes: { highlight: true, odd: true } } })).toEqual("
    YES
    "); 149 | expect(ht("
    YES
    ", { item: { classes: ['odd', { highlight: true }] } })).toEqual("
    YES
    "); 150 | expect(ht("
    YES
    ", { item: { classes: ['odd', { highlight: true }] } })).toEqual("
    YES
    "); 151 | expect(ht("
    YES
    ", { item: { classes: ['odd', { highlight: false }] } })).toEqual("
    YES
    "); 152 | expect(ht("
    YES
    ", { item: { highlight: true } })).toEqual("
    YES
    "); 153 | 154 | }); 155 | 156 | it('bind', () => { 157 | /******************************************************* 158 | * `ht-bind` expression test 159 | *******************************************************/ 160 | expect(ht("
    ", { title: 'YES' })).toEqual("
    YES
    "); 161 | expect(ht("
    ", { title: 'YES' })).toEqual("
    YES
    "); 162 | }); 163 | 164 | it('style', () => { 165 | 166 | expect(ht("
    YES
    ", { styles: { color: 'red' } })).toEqual("
    YES
    "); 167 | expect(ht("
    YES
    ", { styles: { 'font-size': '12px' } })).toEqual("
    YES
    "); 168 | expect(ht("
    YES
    ", { styles: { 'font-size': '12px', width: '45px' } })).toEqual("
    YES
    "); 169 | expect(ht("
    YES
    ", { fontSize: '12px' })).toEqual("
    YES
    "); 170 | }); 171 | 172 | it("include", () => { 173 | /******************************************************************* 174 | * `ht-include` expression test, passed as non existing property for backwards compatibility 175 | * file does not exist, so it will print out as html, the file name 176 | *******************************************************************/ 177 | expect(ht("
    ", {})).toMatch(/
    .*file1.html<\/div>/); 178 | 179 | /******************************************************************* 180 | * `ht-include` expression test, passed as string 181 | * file does not exist, so it will print out as html, the file name 182 | *******************************************************************/ 183 | expect(ht("
    ", {})).toMatch(/
    .*file1.html<\/div>/); 184 | 185 | /******************************************************************* 186 | * `ht-include` expression test, passed as property 187 | * file does not exist, so it will print out as html, the file name 188 | *******************************************************************/ 189 | expect(ht("
    ", { item: { template: 'file2.html' } })).toMatch(/
    .*file2.html<\/div>/); 190 | 191 | /******************************************************************* 192 | * `ht-include` expression test, passed as property in a repeat 193 | * file does not exist, so it will print out as html, the file name 194 | *******************************************************************/ 195 | var exampleResult = ht("
    ", { items: [{ template: 'file3.html' }, { content: 'foo', template: 'spec/small.html' }] }); 196 | expect(exampleResult).toMatch(/
    .*file3.html<\/div>/); 197 | expect(exampleResult).toMatch(/foo<\/span>/); 198 | 199 | /******************************************************************* 200 | * `ht-include` expression test, passed as property in a nested repeat with key value 201 | * file does not exist, so it will print out as html, the file name 202 | *******************************************************************/ 203 | var exampleResult2 = ht("
    ", { items: [{ items: [{ template: 'file3.html' }] }, { items: [{ content: 'foo', template: 'spec/small.html' }] }] }); 204 | expect(exampleResult2).toMatch(/
    .*file3.html<\/div>/); 205 | expect(exampleResult2).toMatch(/foo<\/span>/); 206 | 207 | }); 208 | 209 | it("directory", () => { 210 | /******************************************************************* 211 | * * includeDirs test 212 | * *******************************************************************/ 213 | expect(ht("
    ", { item: { content: 'test1' } }, { prefix: 'ng', includeDirs: [__dirname, __dirname + '/includes'] })).toEqual('
    test1
    '); 214 | expect(ht("
    ", { item: { content: 'test1' } }, { prefix: 'ng', includeDirs: [__dirname + '/includes', __dirname] })).toEqual('
    test1
    '); 215 | expect(ht("
    ", { item: { content: 'test1' } }, { prefix: 'ng', includeDirs: [__dirname + '/shared', __dirname + '/includes', __dirname] })).toEqual('
    test1
    '); 216 | 217 | }); 218 | 219 | it("include context", () => { 220 | expect(ht("
    ", { foo: { content: 'test1' } }, { prefix: 'ng', includeDirs: [__dirname] })).toEqual('
    test1
    '); 221 | expect(ht("
    ", { items: [{ foo: { content: 'test1' } }] }, { prefix: 'ng', includeDirs: [__dirname] })).toEqual('
    test1
    '); 222 | }); 223 | 224 | it("jsdoc template", () => { 225 | /******************************************************************* 226 | * jsdoc template test 227 | *******************************************************************/ 228 | expect(function () { 229 | ht("spec/layout.html", 230 | { nav: [], children: [{ members: [], functions: [] }] }, 231 | { jsMode: false, prefix: 'ng' }); 232 | }).not.toThrow(); 233 | }); 234 | 235 | it("cache and preprocess", () => { 236 | /******************************************************************* 237 | * cache and preprocess test 238 | *******************************************************************/ 239 | 240 | var exampleResult3 = ht("
    ", { item: { content: 'foo' } }, { 241 | prefix: 'ng', cache: 'test', preprocess: function (tpl) { 242 | tpl = tpl.replace(/span/g, 'div'); 243 | return tpl; 244 | } 245 | }); 246 | expect(exampleResult3).toMatch(/
    foo<\/div>/); 247 | expect(ht.cache.get('test')).toMatch(/spec\/small\.html/); 248 | expect(ht.cache.get('test$$spec/small.html')).toMatch(/item\.content/); 249 | 250 | expect(ht("
    ", { item: { content: 'foo' } }, { 251 | prefix: 'ng', cache: 'test', preprocess: function (tpl) { 252 | tpl = tpl.replace(/span/g, 'div'); 253 | return tpl; 254 | } 255 | })).toMatch(/
    foo<\/div>/); 256 | 257 | ht.cache.remove('test'); 258 | expect(ht.cache.get('test')).toBeUndefined(); 259 | expect(ht.cache.get('test$$spec/small.html')).toBeUndefined(); 260 | }); 261 | }); -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | --------------------------------------------------------------------------------