├── .gitignore ├── .travis.yml ├── .gitmodules ├── package.json ├── test ├── escape_method.coffee ├── partials_method.coffee ├── partials_parameter.coffee ├── helpers.coffee ├── object_methods.coffee └── parse_errors_test.coffee ├── dist └── v1.2.0 │ ├── milk.min.js │ ├── milk.js │ └── milk.coffee ├── Cakefile ├── README.md └── milk.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | test/*_spec.js 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | - 0.6 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ext/spec"] 2 | path = ext/spec 3 | url = https://github.com/mustache/spec.git 4 | [submodule "pages"] 5 | path = pages 6 | url = git@github.com:pvande/Milk.git 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "milk", 3 | "version": "v1.2.0", 4 | "homepage": "https://github.com/pvande/Milk", 5 | "author": "Pieter van de Bruggen (http://pvande.net/)", 6 | 7 | "description": "A Mustache implementation written in CoffeeScript", 8 | "keywords": [ "mustache", "coffeescript", "template" ], 9 | 10 | "main": "./milk", 11 | "scripts": { "test": "vows ./test/*" }, 12 | "dependencies": { "coffee-script": "0.9.6 || >=1.0.0" }, 13 | "devDependencies": { "vows": "0.5.11", "docco": "0.3.0" }, 14 | "repository": { "type": "git", "url": "https://github.com/pvande/Milk.git" } 15 | } 16 | -------------------------------------------------------------------------------- /test/escape_method.coffee: -------------------------------------------------------------------------------- 1 | vows = require 'vows' 2 | assert = require 'assert' 3 | 4 | Milk = require('../') 5 | 6 | suite = vows.describe 'Milk.escape' 7 | 8 | suite.addBatch 9 | "The exported #escape method": 10 | topic: -> Milk.escape 11 | 12 | 'should perform basic HTML escaping': (esc) -> 13 | assert.equal(esc('Interpolated &entity;'), 'Interpolated &entity;') 14 | assert.equal(esc(''), '<img src="x" />') 15 | 16 | suite.addBatch 17 | "Replacing the #escape method": 18 | topic: -> 19 | @escape = Milk.escape 20 | Milk.escape = (str) -> str.split('').reverse().join('') 21 | return -> Milk.render(arguments...) 22 | 23 | teardown: -> 24 | Milk.escape = @escape 25 | 26 | 'uses the new #escape method for HTML escaping': (render) -> 27 | tmpl = '{{{x}}} {{x}} {{&x}}' 28 | data = { x: 'content' } 29 | assert.equal(render(tmpl, data), 'content tnetnoc content') 30 | 31 | suite.export(module) 32 | -------------------------------------------------------------------------------- /test/partials_method.coffee: -------------------------------------------------------------------------------- 1 | vows = require 'vows' 2 | assert = require 'assert' 3 | 4 | Milk = require('../') 5 | 6 | suite = vows.describe 'Milk.escape' 7 | 8 | suite.addBatch 9 | "Supplying a #partials method": 10 | topic: -> 11 | @partials = Milk.partials 12 | Milk.partials = (str) -> str.split('').reverse().join('') 13 | return -> Milk.render(arguments...) 14 | 15 | teardown: -> 16 | Milk.partials = @partials 17 | 18 | 'provides a new default Partial resolution mechanism': (render) -> 19 | tmpl = '[{{>partial_name}}]' 20 | data = { } 21 | assert.equal(render(tmpl, data), '[eman_laitrap]') 22 | 23 | 'can be overridden by supplying a partial hash to #render': (render) -> 24 | tmpl = '[{{>partial_name}}]' 25 | data = { } 26 | partials = 27 | partial_name: 'from hash' 28 | assert.equal(render(tmpl, data, partials), '[from hash]') 29 | 30 | render_missing_partial = -> render('{{>miss}}', data, partials) 31 | assert.throws(render_missing_partial, /^Unknown partial 'miss'!$/) 32 | 33 | suite.export(module) 34 | -------------------------------------------------------------------------------- /test/partials_parameter.coffee: -------------------------------------------------------------------------------- 1 | vows = require 'vows' 2 | assert = require 'assert' 3 | 4 | Milk = require('../') 5 | 6 | suite = vows.describe 'Milk.escape' 7 | 8 | suite.addBatch 9 | "If the `partials` parameter is": 10 | topic: -> 11 | return (partials) -> (tmpl, data) -> Milk.render(tmpl, data, partials) 12 | 13 | "a hash": 14 | topic: (parent) -> 15 | return parent({ partial_name: 'from hash' }) 16 | 17 | 'lookups by name work properly': (render) -> 18 | tmpl = '[{{>partial_name}}]' 19 | assert.equal(render(tmpl, { }), '[from hash]') 20 | 21 | 'lookups that fail throw an error': (render) -> 22 | render_missing_partial = -> render('{{>miss}}', { }) 23 | assert.throws(render_missing_partial, /^Unknown partial 'miss'!$/) 24 | 25 | "a function": 26 | topic: (parent) -> 27 | return parent((name) -> name.split('').reverse().join('')) 28 | 29 | 'lookups are handled by that function': (render) -> 30 | tmpl = '[{{>partial_name}}]' 31 | data = { } 32 | assert.equal(render(tmpl, data), '[eman_laitrap]') 33 | 34 | suite.export(module) 35 | -------------------------------------------------------------------------------- /test/helpers.coffee: -------------------------------------------------------------------------------- 1 | vows = require 'vows' 2 | assert = require 'assert' 3 | 4 | Milk = require('../') 5 | 6 | suite = vows.describe 'Milk.helpers' 7 | 8 | suite.addBatch 9 | "Providing an object to Milk.helpers": 10 | topic: -> 11 | Milk.helpers = { key: 'helper', helper: 'helper' } 12 | return -> Milk.render(arguments...) 13 | 14 | teardown: -> Milk.helpers = [] 15 | 16 | 'should put the object on the context stack': (render) -> 17 | result = render('[{{helper}}, {{data}}]', { data: 'data' }) 18 | assert.equal(result, '[helper, data]') 19 | 20 | 'should put the object at the bottom of the context stack': (render) -> 21 | result = render('[{{helper}}, {{key}}]', { key: 'data' }) 22 | assert.equal(result, '[helper, data]') 23 | 24 | suite.addBatch 25 | "Providing an array to Milk.helpers": 26 | topic: -> 27 | Milk.helpers = [{ key: 'helper', helper: 'helper' }, { helper: 'two' }] 28 | return -> Milk.render(arguments...) 29 | 30 | teardown: -> Milk.helpers = [] 31 | 32 | 'should put each element on the context stack': (render) -> 33 | result = render('[{{key}}, {{data}}]', { data: 'data' }) 34 | assert.equal(result, '[helper, data]') 35 | 36 | 'should put each element on the context stack in order': (render) -> 37 | result = render('[{{key}}, {{helper}}]', { }) 38 | assert.equal(result, '[helper, two]') 39 | 40 | 'should put the elements at the bottom of the context stack': (render) -> 41 | result = render('[{{helper}}, {{key}}]', { key: 'data' }) 42 | assert.equal(result, '[two, data]') 43 | 44 | suite.export(module) 45 | -------------------------------------------------------------------------------- /test/object_methods.coffee: -------------------------------------------------------------------------------- 1 | vows = require 'vows' 2 | assert = require 'assert' 3 | 4 | Milk = require '../' 5 | 6 | suite = vows.describe 'Object Methods' 7 | 8 | suite.addBatch 9 | "Interpolating an object method": 10 | topic: -> ((data) -> Milk.render '[{{method}}]', data) 11 | 12 | 'calls the method': (render) -> 13 | data = { method: -> 'a, b, c' } 14 | assert.equal(render(data), '[a, b, c]') 15 | 16 | 'binds `this` to the context': (render) -> 17 | data = { method: (-> @data), data: 'foo' } 18 | assert.equal(render(data), '[foo]') 19 | 20 | 'renders the returned string': (render) -> 21 | data = { method: (-> '{{data}}'), data: 'bar' } 22 | assert.equal(render(data), '[bar]') 23 | 24 | "Building a section with an object method": 25 | topic: -> ((data) -> Milk.render '[{{#method}}{{x}}{{/method}}]', data) 26 | 27 | 'uses the returned string as the section content': (render) -> 28 | data = { method: (-> 'content') } 29 | assert.equal(render(data), '[content]') 30 | 31 | 'calls the method': (render) -> 32 | data = { method: -> 'a, b, c' } 33 | assert.equal(render(data), '[a, b, c]') 34 | 35 | 'binds `this` to the context': (render) -> 36 | data = { method: (-> @data), data: 'foo' } 37 | assert.equal(render(data), '[foo]') 38 | 39 | 'passes the raw template string as an argument': (render) -> 40 | render({ method: (tmpl) -> assert.equal(tmpl, '{{x}}') }) 41 | 42 | 'renders the returned string': (render) -> 43 | data = { method: (-> '{{data}}'), data: 'bar' } 44 | assert.equal(render(data), '[bar]') 45 | 46 | "Using an object method in a nested context": 47 | topic: -> 48 | (tmpl, data) -> 49 | data = { nested: data, key: 'WRONG' } 50 | Milk.render "[{{#nested}}#{tmpl}{{/nested}}]", data 51 | 52 | "for interpolation": 53 | topic: (T) -> ((data) -> T('{{method}}', data)) 54 | 55 | 'calls the method': (render) -> 56 | data = { method: -> 'a, b, c' } 57 | assert.equal(render(data), '[a, b, c]') 58 | 59 | 'binds `this` to the context element': (render) -> 60 | data = { method: (-> @key), key: 'foo' } 61 | assert.equal(render(data), '[foo]') 62 | 63 | 'renders the returned string': (render) -> 64 | data = { method: (-> '{{data}}'), data: 'bar' } 65 | assert.equal(render(data), '[bar]') 66 | 67 | "for a section": 68 | topic: (T) -> ((data) -> T('{{#method}}{{x}}{{/method}}', data)) 69 | 70 | 'uses the returned string as the section content': (render) -> 71 | data = { method: (-> 'content') } 72 | assert.equal(render(data), '[content]') 73 | 74 | 'calls the method': (render) -> 75 | data = { method: -> 'a, b, c' } 76 | assert.equal(render(data), '[a, b, c]') 77 | 78 | 'binds `this` to the context element': (render) -> 79 | data = { method: (-> @key), key: 'foo' } 80 | assert.equal(render(data), '[foo]') 81 | 82 | 'passes the raw template string as an argument': (render) -> 83 | render({ method: (tmpl) -> assert.equal(tmpl, '{{x}}') }) 84 | 85 | 'renders the returned string': (render) -> 86 | data = { method: (-> '{{data}}'), data: 'bar' } 87 | assert.equal(render(data), '[bar]') 88 | 89 | 90 | suite.export(module) 91 | -------------------------------------------------------------------------------- /dist/v1.2.0/milk.min.js: -------------------------------------------------------------------------------- 1 | (function(){var q,r,k,l,z,s,m=Array.prototype.slice;z={};r=function(c,a,b){var f,e,d,g;b==null&&(b=null);if(c===".")return a[a.length-1];d=c.split(/\./);c=d[0];d=2<=d.length?m.call(d,1):[];for(e=g=a.length-1;g<=-1?e<-1:e>-1;g<=-1?e+=1:e-=1)if(a[e]!=null&&typeof a[e]==="object"&&c in(f=a[e])){b=f[c];break}a=0;for(e=d.length;a":g=function(a,b){return function(c,e){var d;d=e(a).toString();b&&(d=d.replace(/^(?=.)/gm,b));return q.apply(null,[this,l(d)].concat(m.call(arguments)))}};e.push(g(h,o));break;case "#":case "^":d={name:h,start:i,error:x(j.lastIndex,"Unclosed section '"+h+"'!")};w=l(c,a,d);y=w[0];i=w[1];d["#"]=function(a,b,c){return function(e){var d,g,h,f;f=r(a,e)||[];y=f instanceof Function?f(c):c;f instanceof 6 | Array||(f=[f]);d=l(y||"",b);e.push(f);g=function(){var a,b,c;c=[];a=0;for(b=f.length;a":"gt"};return c.replace(/[&"<>]/g,function(b){return"&"+a[b]+";"})},render:function(c,a,b){var f;b==null&&(b=null);if(!((b||(b=this.partials||{}))instanceof Function))b=function(a){return function(b){if(!(b in a))throw"Unknown partial '"+b+"'!";return r(b,[a])}}(b);f=this.helpers instanceof Array? 9 | this.helpers:[this.helpers];return q(this,l(c),f.concat([a]),b)}};if(typeof exports!="undefined"&&exports!==null)for(s in k)exports[s]=k[s];else this.Milk=k}).call(this); 10 | -------------------------------------------------------------------------------- /test/parse_errors_test.coffee: -------------------------------------------------------------------------------- 1 | vows = require 'vows' 2 | assert = require 'assert' 3 | 4 | Milk = require '../' 5 | 6 | throwsError = (error, tmpl) -> 7 | topic: -> (-> Milk.render tmpl) 8 | 'throws an error': (render) -> 9 | assert.throws(render) 10 | 'throws expected error': (render) -> 11 | try 12 | render() 13 | catch e 14 | assert.match(e.toString(), ///^#{error.message}///m) 15 | return 16 | assert.ok(false, "Did not throw error!") 17 | 'gives the correct line number in the thrown error': (render) -> 18 | try 19 | render() 20 | catch e 21 | assert.equal(e.line, error.line) 22 | return 23 | assert.ok(false, "Did not throw error!") 24 | 'gives the correct character number in the thrown error': (render) -> 25 | try 26 | render() 27 | catch e 28 | assert.equal(e.char, error.character) 29 | return 30 | assert.ok(false, "Did not throw error!") 31 | 'gives the correct errorful tag in the thrown error': (render) -> 32 | try 33 | render() 34 | catch e 35 | assert.equal(e.tag, error.tag) 36 | return 37 | assert.ok(false, "Did not throw error!") 38 | 39 | suite = vows.describe 'Parse Errors' 40 | 41 | suite.addBatch 42 | "Closing the wrong section tag": 43 | throwsError( 44 | { 45 | message: "Error: End Section tag closes 'other'; expected 'section'!" 46 | line: 4 47 | character: 0 48 | tag: '{{/other}}' 49 | }, 50 | ''' 51 | Before... 52 | {{#section}} 53 | Inner... 54 | {{/other}} 55 | After... 56 | ''' 57 | ) 58 | 59 | "Not closing a nested section tag": 60 | throwsError( 61 | { 62 | message: "Error: End Section tag closes 'a'; expected 'b'!" 63 | line: 3 64 | character: 0 65 | tag: '{{/a}}' 66 | }, 67 | ''' 68 | {{#a}} 69 | {{#b}} 70 | {{/a}} 71 | ''' 72 | ) 73 | 74 | "Closing a section at the top level": 75 | throwsError( 76 | { 77 | message: "Error: End Section tag 'section' found, but not in section!" 78 | line: 2 79 | character: 0 80 | tag: '{{/section}}' 81 | }, 82 | ''' 83 | Before... 84 | {{/section}} 85 | After... 86 | ''' 87 | ) 88 | 89 | "Failing to close a top-level section": 90 | throwsError( 91 | { 92 | message: "Error: Unclosed section 'section'!" 93 | line: 2 94 | character: 0 95 | tag: '{{# section }}' 96 | }, 97 | ''' 98 | Before... 99 | {{# section }} 100 | After... 101 | ''' 102 | ) 103 | 104 | "Failing to close an indented top-level section": 105 | throwsError( 106 | { 107 | message: "Error: Unclosed section 'section'!" 108 | line: 2 109 | character: 2 110 | tag: '{{# section }}' 111 | }, 112 | ''' 113 | Before... 114 | {{# section }} 115 | After... 116 | ''' 117 | ) 118 | 119 | "Specifying too few delimiters": 120 | throwsError( 121 | { 122 | message: "Error: Set Delimiters tags should have two and only two values!" 123 | line: 1 124 | character: 0 125 | tag: '{{= $$$ =}}' 126 | }, 127 | '{{= $$$ =}}' 128 | ) 129 | 130 | "Specifying too many delimiters": 131 | throwsError( 132 | { 133 | message: "Error: Set Delimiters tags should have two and only two values!" 134 | line: 1 135 | character: 0 136 | tag: '{{= $ $ $ =}}' 137 | }, 138 | '{{= $ $ $ =}}' 139 | ) 140 | 141 | "Specifying an unknown tag type": 142 | throwsError( 143 | { 144 | message: "Error: Unknown tag type -- §" 145 | line: 1 146 | character: 0 147 | tag: '{{§ something }}' 148 | }, 149 | '{{§ something }}' 150 | ) 151 | 152 | "Specifying an errorful tag at the beginning of the line": 153 | throwsError( 154 | { 155 | message: "[^]{10}$" 156 | line: 1 157 | character: 0 158 | tag: '{{$ tag }}' 159 | }, 160 | '{{$ tag }} is over here...' 161 | ) 162 | 163 | "Specifying an errorful tag further in on a line": 164 | throwsError( 165 | { 166 | message: " [^]{10}$" 167 | line: 1 168 | character: 4 169 | tag: '{{$ tag }}' 170 | }, 171 | 'Now {{$ tag }} is over here...' 172 | ) 173 | 174 | "Specifying an errorful tag amongst valid tags on the same line": 175 | throwsError( 176 | { 177 | message: " [^]{10}" 178 | line: 1 179 | character: 35 180 | tag: '{{$ tag }}' 181 | }, 182 | 'Yes, this is a {{ tag }}, but this {{$ tag }} is {{invalid}}.' 183 | ) 184 | 185 | "Specifying an errorful tag amongst valid tags on different lines": 186 | throwsError( 187 | { 188 | message: 'This [{]{2}[$] tag [}]{2} has an error$' 189 | line: 2 190 | character: 5 191 | tag: '{{$ tag }}' 192 | }, 193 | ''' 194 | This is a {{tag}} 195 | This {{$ tag }} has an error 196 | This one is {{ fine }} 197 | ''' 198 | ) 199 | 200 | suite.export(module) 201 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | {exec} = require 'child_process' 4 | 5 | option '-o', '--output [DIR]', 'directory for compiled code' 6 | 7 | task 'benchmark', 'Run a simple benchmark of Milk', -> 8 | sys = require 'sys' 9 | Milk = require 'milk' 10 | tmpl = """ 11 |

{{header}}

12 | {{#list.length}} 13 |
    14 | {{#list}} 15 | {{#current}} 16 |
  • {{name}}
  • 17 | {{/current}} 18 | {{^current}} 19 |
  • a href="{{url}}">{{name}}
  • 20 | {{/current}} 21 | {{/list}} 22 |
23 | {{/list.length}} 24 | {{^list.length}} 25 |

The list is empty.

26 | {{/list.length}} 27 | """ 28 | 29 | start = new Date() 30 | process.addListener 'exit', -> 31 | sys.error "Time taken: #{ (new Date() - start) / 1000 } secs" 32 | 33 | for i in [0..1000000] 34 | Milk.render tmpl, 35 | header: "Colors" 36 | list: [ 37 | { name: "red", url: "#Red", current: yes } 38 | { name: "green", url: "#Green", current: no } 39 | { name: "blue", url: "#Blue", current: no } 40 | ] 41 | 42 | task 'build', 'Rebuilds all public web resources', -> 43 | invoke('build:js') 44 | invoke('build:docs') 45 | invoke('spec:html') 46 | 47 | task 'build:js', 'Builds Milk into ./pages (or --output)', (opts) -> 48 | CoffeeScript = require 'coffee-script' 49 | 50 | out = opts.output or 'pages' 51 | out = path.join(__dirname, out) unless out[0] = '/' 52 | 53 | fs.readFile path.join(__dirname, 'milk.coffee'), 'utf8', (err, data) -> 54 | throw err if err 55 | fs.writeFile path.join(out, 'milk.js'), CoffeeScript.compile(data) 56 | 57 | task 'build:docs', 'Builds documentation with Docco', -> 58 | chain = (commands...) -> 59 | exec commands.shift(), (err) -> 60 | throw err if err 61 | chain commands... if commands.length 62 | chain 'docco milk.coffee', 63 | 'mv docs/milk.html pages/index.html', 64 | 'rm -r docs', 65 | 66 | task 'spec:node', 'Creates compliance tests for the Mustache spec in Vows', -> 67 | readSpecs (file, json) -> 68 | test = """ 69 | vows = require('vows'); 70 | equal = require('assert').equal; 71 | Milk = require('../'); 72 | suite = vows.describe('Mustache Specification - #{file}'); 73 | 74 | tests = #{json}['tests']; 75 | 76 | var batch = {}; 77 | for (var i = 0; i < tests.length; i++) { 78 | (function(test) { 79 | var context = { "topic": #{topic} }; 80 | context[test.desc] = function(r) { equal(r, test.expected) }; 81 | 82 | batch[test.name] = context; 83 | })(tests[i]); 84 | } 85 | 86 | suite.addBatch(batch); 87 | suite.export(module); 88 | """ 89 | testFile = file.replace(/^~/, '').replace(/\.yml$/, '_spec.js') 90 | fs.writeFile path.join(__dirname, 'test', testFile), test 91 | 92 | task 'spec:html', 'Creates compliance tests for the Mustache spec in HTML', -> 93 | invoke('build:js') 94 | spec = (file, json) -> 95 | """ 96 | describe("Mustache Specification - #{file}", function() { 97 | var tests = #{json}['tests']; 98 | for (var i = 0; i < tests.length; i++) { 99 | it(tests[i].desc, buildTest(tests[i])); 100 | } 101 | }); 102 | """ 103 | 104 | readSpecs spec, (specs) -> 105 | lib = 'https://github.com/pivotal/jasmine/raw/1.0.1-release/lib' 106 | fs.writeFile path.join(__dirname, 'pages', 'compliance.html'), 107 | """ 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 124 | 125 | 126 | """ 127 | 128 | readSpecs = (callback, allDone = (->)) -> 129 | # Convert the YAML files to Javascript. 130 | # Requires the YAML and JSON Ruby libraries. 131 | ruby = ''' 132 | ruby -rubygems 133 | -e 'require "yaml"' 134 | -e 'require "json"' 135 | -e 'YAML::add_builtin_type("code") do |_,value| 136 | value["js"].tap do |x| 137 | def x.to_json(_) 138 | "function() { return #{self}; }" 139 | end 140 | end 141 | end' 142 | -e 'print YAML.load_file(ARGV[0]).to_json()' 143 | '''.replace(/\n/gm, ' ') 144 | 145 | results = [] 146 | dir = path.join(__dirname, 'ext', 'spec', 'specs') 147 | for file in (files = fs.readdirSync(dir)) 148 | continue unless file.match(/\.yml$/) 149 | do (file) -> 150 | exec "#{ruby} -- #{path.join(dir, file)}", (err, stdout, stderr) -> 151 | throw err if err 152 | results.push(callback(file, stdout)) 153 | allDone(results) if (files.length == results.length) 154 | 155 | topic = -> 156 | try Milk.render(test.template, test.data, test.partials || {}) 157 | catch e then "ERROR: " + e 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Milk 2 | ==== 3 | 4 | Idly the man scrawled quill along page. It was early yet, but his day 5 | had scarcely begun. Great sheaves of nearly identical work sat about his 6 | desk as the clock clicked slowly to itself. One by one, new pages joined 7 | the ranks, permeated with the smell of roasted beans as the pen drew 8 | coffee as often as ink. 9 | Exhausted, he collapsed atop his work in a fitful doze. Images began to 10 | invade his unconscious mind, of flight and fancy, and jubilant impropriety. 11 | Then just as suddenly as he had slept, he woke, the image of a small child 12 | wearing a big smile and a heavy coat of Milk on his upper lip startling him 13 | back to alertness. 14 | He saw clearly, as he looked across his paper-strewn desk, that the task 15 | could be changed – and for once, it looked like fun. 16 | 17 | Milk is a [spec-conforming](https://github.com/mustache/spec) (v1.1+λ) 18 | implementation of the [Mustache](http://mustache.github.com) templating 19 | language, written in [CoffeeScript](http://coffeescript.com). Templates can be 20 | rendered server-side (through Node.js or any other CommonJS platform), or, 21 | since CoffeeScript compiles to Javascript, on the client-side in the browser 22 | of your choice. 23 | 24 | Try Milk Now 25 | ------------ 26 | 27 | Wondering what it can do? 28 | [Hit the playground!](http://pvande.github.com/Milk/playground.html) 29 | 30 | Installation 31 | ------------ 32 | 33 | npm install milk 34 | 35 | Usage 36 | ----- 37 | 38 | Milk is built for use both in CommonJS environments and in the browser (where 39 | it will be exported as `window.Milk`). The public API is deliberately simple: 40 | 41 | ### render 42 | 43 | ``` javascript 44 | Milk.render(template, data); // => 'A rendered template' 45 | Milk.render(template, data, partials); // => 'A rendered template' 46 | ``` 47 | 48 | The `render` method is the core of Milk. In its simplest form, it takes a 49 | Mustache template string and a data object, returning the rendered template. 50 | It also takes an optional third parameter, which can be either a hash of named 51 | partial templates, or a function that takes a partial name and returns the 52 | partial. 53 | 54 | ### partials 55 | 56 | ``` javascript 57 | Milk.partials = { ... }; 58 | 59 | // equivalent to Milk.render(template, data, Milk.partials) 60 | Milk.render(template, data); 61 | ``` 62 | 63 | If your application's needs for partials are relatively simple, it may make 64 | more sense to handle partial resolution globally. To support this, your calls 65 | to `render` will automatically fall back to using `Milk.partials` when you 66 | don't supply explicit partial resolution. 67 | 68 | ### helpers 69 | 70 | ``` javascript 71 | Milk.helpers = { ... }; // will also work with an array 72 | 73 | // everything from Milk.helpers lives at the bottom of the context stack 74 | Milk.render(template, data); 75 | ``` 76 | 77 | Whether for internationalization or syntax highlighting, sometimes you'll find 78 | yourself needing certain functions available everywhere in your templates. 79 | To help enable this behavior, Milk.helpers acts as the baseline for your 80 | context stack, providing a quick way to all the global data and functions you 81 | need. 82 | 83 | ### escape 84 | 85 | ``` javascript 86 | Milk.escape(''); // => '<tag type="evil">' 87 | 88 | Milk.escape = function(str) { return str.split("").reverse().join("") }; 89 | 90 | // Milk.escape is used to handle all escaped tags 91 | var template = "{{data}} is {{{data}}}"; 92 | Milk.render(template, { "data": "reversed" }); // => "desrever is reversed" 93 | ``` 94 | 95 | `Milk.escape` is the function that Milk uses to handle escaped interpolation. 96 | As such, you can use it (e.g. from lambdas) to perform the same escaping that 97 | Milk does, or you can override it to change the behavior of escaped tags. 98 | 99 | ### VERSION 100 | 101 | ``` javascript 102 | Milk.VERSION // => '1.2.0' 103 | ``` 104 | 105 | For when you absolutely must know what version of the library you're running. 106 | 107 | Documentation 108 | ------------- 109 | 110 | Milk itself is documented more completely at http://pvande.github.com/Milk 111 | (public API documentation is 112 | [this bit](http://pvande.github.com/Milk#section-26)). 113 | 114 | The Mustache templating language is documented at http://mustache.github.com. 115 | 116 | Development 117 | ----------- 118 | 119 | A few things to note: 120 | 121 | * This project uses submodules. To get them, run `git submodule init` and `git 122 | submodule update`. 123 | * To install the npm dependencies, run `npm install .` 124 | * There are a number of `cake` tasks, including ones that build the specs. To 125 | list the tasks, simply run `cake` in the project directory. To build the 126 | specs, run `cake spec:node` or `cake spec:html`. 127 | * To run the node.js tests, run `npm test`. 128 | 129 | Copyright 130 | --------- 131 | 132 | Copyright (c) 2011 Pieter van de Bruggen. 133 | 134 | (The GIFT License, v2) 135 | 136 | Permission is hereby granted to use this software and/or its source code for 137 | whatever purpose you should choose. Seriously, go nuts. Use it to build your 138 | family CMS, your incredibly popular online text adventure, or to mass-produce 139 | Constitutions for North African countries. 140 | 141 | I don't care, it's yours. Change the name on it if you want -- in fact, if 142 | you start significantly changing what it does, I'd rather you did! Make it 143 | your own little work of art, complete with a stylish flowing signature in the 144 | corner. All I really did was give you the canvas. And my blessing. 145 | 146 | Know always right from wrong, and let others see your good works. 147 | -------------------------------------------------------------------------------- /dist/v1.2.0/milk.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Expand, Find, Milk, Parse, TemplateCache, key; 3 | var __slice = Array.prototype.slice; 4 | TemplateCache = {}; 5 | Find = function(name, stack, value) { 6 | var ctx, i, part, parts, _i, _len, _ref, _ref2, _ref3; 7 | if (value == null) { 8 | value = null; 9 | } 10 | if (name === '.') { 11 | return stack[stack.length - 1]; 12 | } 13 | _ref = name.split(/\./), name = _ref[0], parts = 2 <= _ref.length ? __slice.call(_ref, 1) : []; 14 | for (i = _ref2 = stack.length - 1, _ref3 = -1; (_ref2 <= _ref3 ? i < _ref3 : i > _ref3); (_ref2 <= _ref3 ? i += 1 : i -= 1)) { 15 | if (stack[i] == null) { 16 | continue; 17 | } 18 | if (!(typeof stack[i] === 'object' && name in (ctx = stack[i]))) { 19 | continue; 20 | } 21 | value = ctx[name]; 22 | break; 23 | } 24 | for (_i = 0, _len = parts.length; _i < _len; _i++) { 25 | part = parts[_i]; 26 | value = Find(part, [value]); 27 | } 28 | if (value instanceof Function) { 29 | value = (function(value) { 30 | return function() { 31 | var val; 32 | val = value.apply(ctx, arguments); 33 | return (val instanceof Function) && val.apply(null, arguments) || val; 34 | }; 35 | })(value); 36 | } 37 | return value; 38 | }; 39 | Expand = function() { 40 | var args, f, obj, tmpl; 41 | obj = arguments[0], tmpl = arguments[1], args = 3 <= arguments.length ? __slice.call(arguments, 2) : []; 42 | return ((function() { 43 | var _i, _len, _results; 44 | _results = []; 45 | for (_i = 0, _len = tmpl.length; _i < _len; _i++) { 46 | f = tmpl[_i]; 47 | _results.push(f.call.apply(f, [obj].concat(__slice.call(args)))); 48 | } 49 | return _results; 50 | })()).join(''); 51 | }; 52 | Parse = function(template, delimiters, section) { 53 | var BuildRegex, buffer, buildInterpolationTag, buildInvertedSectionTag, buildPartialTag, buildSectionTag, cache, content, contentEnd, d, error, escape, isStandalone, match, name, parseError, pos, sectionInfo, tag, tagPattern, tmpl, type, whitespace, _name, _ref, _ref2, _ref3; 54 | if (delimiters == null) { 55 | delimiters = ['{{', '}}']; 56 | } 57 | if (section == null) { 58 | section = null; 59 | } 60 | cache = (TemplateCache[_name = delimiters.join(' ')] || (TemplateCache[_name] = {})); 61 | if (template in cache) { 62 | return cache[template]; 63 | } 64 | buffer = []; 65 | BuildRegex = function() { 66 | var tagClose, tagOpen; 67 | tagOpen = delimiters[0], tagClose = delimiters[1]; 68 | return RegExp("([\\s\\S]*?)([" + ' ' + "\\t]*)(?:" + tagOpen + "\\s*(?:(!)\\s*([\\s\\S]+?)|(=)\\s*([\\s\\S]+?)\\s*=|({)\\s*(\\w[\\S]*?)\\s*}|([^0-9a-zA-Z._!={]?)\\s*([\\w.][\\S]*?))\\s*" + tagClose + ")", "gm"); 69 | }; 70 | tagPattern = BuildRegex(); 71 | tagPattern.lastIndex = pos = (section || { 72 | start: 0 73 | }).start; 74 | parseError = function(pos, msg) { 75 | var carets, e, endOfLine, error, indent, key, lastLine, lastTag, lineNo, parsedLines, tagStart; 76 | (endOfLine = /$/gm).lastIndex = pos; 77 | endOfLine.exec(template); 78 | parsedLines = template.substr(0, pos).split('\n'); 79 | lineNo = parsedLines.length; 80 | lastLine = parsedLines[lineNo - 1]; 81 | tagStart = contentEnd + whitespace.length; 82 | lastTag = template.substr(tagStart + 1, pos - tagStart - 1); 83 | indent = new Array(lastLine.length - lastTag.length + 1).join(' '); 84 | carets = new Array(lastTag.length + 1).join('^'); 85 | lastLine = lastLine + template.substr(pos, endOfLine.lastIndex - pos); 86 | error = new Error(); 87 | for (key in e = { 88 | "message": "" + msg + "\n\nLine " + lineNo + ":\n" + lastLine + "\n" + indent + carets, 89 | "error": msg, 90 | "line": lineNo, 91 | "char": indent.length, 92 | "tag": lastTag 93 | }) { 94 | error[key] = e[key]; 95 | } 96 | return error; 97 | }; 98 | while (match = tagPattern.exec(template)) { 99 | _ref = match.slice(1, 3), content = _ref[0], whitespace = _ref[1]; 100 | type = match[3] || match[5] || match[7] || match[9]; 101 | tag = match[4] || match[6] || match[8] || match[10]; 102 | contentEnd = (pos + content.length) - 1; 103 | pos = tagPattern.lastIndex; 104 | isStandalone = (contentEnd === -1 || template.charAt(contentEnd) === '\n') && ((_ref2 = template.charAt(pos)) === void 0 || _ref2 === '' || _ref2 === '\r' || _ref2 === '\n'); 105 | if (content) { 106 | buffer.push((function(content) { 107 | return function() { 108 | return content; 109 | }; 110 | })(content)); 111 | } 112 | if (isStandalone && (type !== '' && type !== '&' && type !== '{')) { 113 | if (template.charAt(pos) === '\r') { 114 | pos += 1; 115 | } 116 | if (template.charAt(pos) === '\n') { 117 | pos += 1; 118 | } 119 | } else if (whitespace) { 120 | buffer.push((function(whitespace) { 121 | return function() { 122 | return whitespace; 123 | }; 124 | })(whitespace)); 125 | contentEnd += whitespace.length; 126 | whitespace = ''; 127 | } 128 | switch (type) { 129 | case '!': 130 | break; 131 | case '': 132 | case '&': 133 | case '{': 134 | buildInterpolationTag = function(name, is_unescaped) { 135 | return function(context) { 136 | var value, _ref; 137 | if ((value = (_ref = Find(name, context)) != null ? _ref : '') instanceof Function) { 138 | value = Expand.apply(null, [this, Parse("" + (value()))].concat(__slice.call(arguments))); 139 | } 140 | if (!is_unescaped) { 141 | value = this.escape("" + value); 142 | } 143 | return "" + value; 144 | }; 145 | }; 146 | buffer.push(buildInterpolationTag(tag, type)); 147 | break; 148 | case '>': 149 | buildPartialTag = function(name, indentation) { 150 | return function(context, partials) { 151 | var partial; 152 | partial = partials(name).toString(); 153 | if (indentation) { 154 | partial = partial.replace(/^(?=.)/gm, indentation); 155 | } 156 | return Expand.apply(null, [this, Parse(partial)].concat(__slice.call(arguments))); 157 | }; 158 | }; 159 | buffer.push(buildPartialTag(tag, whitespace)); 160 | break; 161 | case '#': 162 | case '^': 163 | sectionInfo = { 164 | name: tag, 165 | start: pos, 166 | error: parseError(tagPattern.lastIndex, "Unclosed section '" + tag + "'!") 167 | }; 168 | _ref3 = Parse(template, delimiters, sectionInfo), tmpl = _ref3[0], pos = _ref3[1]; 169 | sectionInfo['#'] = buildSectionTag = function(name, delims, raw) { 170 | return function(context) { 171 | var parsed, result, v, value; 172 | value = Find(name, context) || []; 173 | tmpl = value instanceof Function ? value(raw) : raw; 174 | if (!(value instanceof Array)) { 175 | value = [value]; 176 | } 177 | parsed = Parse(tmpl || '', delims); 178 | context.push(value); 179 | result = (function() { 180 | var _i, _len, _results; 181 | _results = []; 182 | for (_i = 0, _len = value.length; _i < _len; _i++) { 183 | v = value[_i]; 184 | context[context.length - 1] = v; 185 | _results.push(Expand.apply(null, [this, parsed].concat(__slice.call(arguments)))); 186 | } 187 | return _results; 188 | }).apply(this, arguments); 189 | context.pop(); 190 | return result.join(''); 191 | }; 192 | }; 193 | sectionInfo['^'] = buildInvertedSectionTag = function(name, delims, raw) { 194 | return function(context) { 195 | var value; 196 | value = Find(name, context) || []; 197 | if (!(value instanceof Array)) { 198 | value = [1]; 199 | } 200 | value = value.length === 0 ? Parse(raw, delims) : []; 201 | return Expand.apply(null, [this, value].concat(__slice.call(arguments))); 202 | }; 203 | }; 204 | buffer.push(sectionInfo[type](tag, delimiters, tmpl)); 205 | break; 206 | case '/': 207 | if (section == null) { 208 | error = "End Section tag '" + tag + "' found, but not in section!"; 209 | } else if (tag !== (name = section.name)) { 210 | error = "End Section tag closes '" + tag + "'; expected '" + name + "'!"; 211 | } 212 | if (error) { 213 | throw parseError(tagPattern.lastIndex, error); 214 | } 215 | template = template.slice(section.start, (contentEnd + 1) || 9e9); 216 | cache[template] = buffer; 217 | return [template, pos]; 218 | case '=': 219 | if ((delimiters = tag.split(/\s+/)).length !== 2) { 220 | error = "Set Delimiters tags should have two and only two values!"; 221 | } 222 | if (error) { 223 | throw parseError(tagPattern.lastIndex, error); 224 | } 225 | escape = /[-[\]{}()*+?.,\\^$|#]/g; 226 | delimiters = (function() { 227 | var _i, _len, _results; 228 | _results = []; 229 | for (_i = 0, _len = delimiters.length; _i < _len; _i++) { 230 | d = delimiters[_i]; 231 | _results.push(d.replace(escape, "\\$&")); 232 | } 233 | return _results; 234 | })(); 235 | tagPattern = BuildRegex(); 236 | break; 237 | default: 238 | throw parseError(tagPattern.lastIndex, "Unknown tag type -- " + type); 239 | } 240 | tagPattern.lastIndex = pos != null ? pos : template.length; 241 | } 242 | if (section != null) { 243 | throw section.error; 244 | } 245 | if (template.length !== pos) { 246 | buffer.push(function() { 247 | return template.slice(pos); 248 | }); 249 | } 250 | return cache[template] = buffer; 251 | }; 252 | Milk = { 253 | VERSION: '1.2.0', 254 | helpers: [], 255 | partials: null, 256 | escape: function(value) { 257 | var entities; 258 | entities = { 259 | '&': 'amp', 260 | '"': 'quot', 261 | '<': 'lt', 262 | '>': 'gt' 263 | }; 264 | return value.replace(/[&"<>]/g, function(ch) { 265 | return "&" + entities[ch] + ";"; 266 | }); 267 | }, 268 | render: function(template, data, partials) { 269 | var context; 270 | if (partials == null) { 271 | partials = null; 272 | } 273 | if (!((partials || (partials = this.partials || {})) instanceof Function)) { 274 | partials = (function(partials) { 275 | return function(name) { 276 | if (!(name in partials)) { 277 | throw "Unknown partial '" + name + "'!"; 278 | } 279 | return Find(name, [partials]); 280 | }; 281 | })(partials); 282 | } 283 | context = this.helpers instanceof Array ? this.helpers : [this.helpers]; 284 | return Expand(this, Parse(template), context.concat([data]), partials); 285 | } 286 | }; 287 | if (typeof exports != "undefined" && exports !== null) { 288 | for (key in Milk) { 289 | exports[key] = Milk[key]; 290 | } 291 | } else { 292 | this.Milk = Milk; 293 | } 294 | }).call(this); 295 | -------------------------------------------------------------------------------- /milk.coffee: -------------------------------------------------------------------------------- 1 | # Milk is a simple, fast way to get more Mustache into your CoffeeScript and 2 | # Javascript. 3 | # 4 | # Mustache templates are reasonably simple -- plain text templates are 5 | # sprinkled with "tags", which are (by default) a pair of curly braces 6 | # surrounding some bit of content. A good resource for Mustache can be found 7 | # [here](mustache.github.com). 8 | TemplateCache = {} 9 | 10 | # Tags used for working with data get their data by looking up a name in a 11 | # context stack. This name corresponds to a key in a hash, and the stack is 12 | # searched top to bottom for an object with given key. Dots in names are 13 | # special: a single dot ('.') is "top of stack", and dotted names like 'a.b.c' 14 | # do a chained lookups. 15 | Find = (name, stack, value = null) -> 16 | return stack[stack.length - 1] if name == '.' 17 | [name, parts...] = name.split(/\./) 18 | for i in [stack.length - 1...-1] 19 | continue unless stack[i]? 20 | continue unless typeof stack[i] == 'object' and name of (ctx = stack[i]) 21 | value = ctx[name] 22 | break 23 | 24 | value = Find(part, [value]) for part in parts 25 | 26 | # If we find a function in the stack, we'll treat it as a method, and call it 27 | # with `this` bound to the element it came from. If a method returns a 28 | # function, we treat it as a lambda, which doesn't have a bound `this`. 29 | if value instanceof Function 30 | value = do (value) -> -> 31 | val = value.apply(ctx, arguments) 32 | return (val instanceof Function) and val.apply(null, arguments) or val 33 | 34 | # Null values will be coerced to the empty string. 35 | return value 36 | 37 | # Parsed templates are expanded by simply calling each function in turn. 38 | Expand = (obj, tmpl, args...) -> (f.call(obj, args...) for f in tmpl).join('') 39 | 40 | # For parsing, we'll basically need a template string to parse. We do need to 41 | # remember to take the tag delimiters into account for the cache -- different 42 | # parse trees can exist for the same template string! 43 | Parse = (template, delimiters = ['{{','}}'], section = null) -> 44 | cache = (TemplateCache[delimiters.join(' ')] ||= {}) 45 | return cache[template] if template of cache 46 | 47 | buffer = [] 48 | 49 | # We'll use a regular expression to handle tag discovery. A proper parser 50 | # might be faster, but this is simpler, and certainly fast enough for now. 51 | # Since the tag delimiters may change over time, we'll want to rebuild the 52 | # regex when they change. 53 | BuildRegex = -> 54 | [tagOpen, tagClose] = delimiters 55 | return /// 56 | ([\s\S]*?) # Capture the pre-tag content 57 | ([#{' '}\t]*) # Capture the pre-tag whitespace 58 | (?: #{tagOpen} \s* # Match the opening tag 59 | (?: 60 | (!) \s* ([\s\S]+?) | # Comments 61 | (=) \s* ([\s\S]+?) \s* = | # Set Delimiters 62 | ({) \s* (\w[\S]*?) \s* } | # Triple Mustaches 63 | ([^0-9a-zA-Z._!={]?) \s* ([\w.][\S]*?) # Everything else 64 | ) 65 | \s* #{tagClose} ) # Match the closing tag 66 | ///gm 67 | 68 | tagPattern = BuildRegex() 69 | tagPattern.lastIndex = pos = (section || { start: 0 }).start 70 | 71 | # Useful errors should always be prefered - we should compile as much 72 | # relevant information as possible. 73 | parseError = (pos, msg) -> 74 | (endOfLine = /$/gm).lastIndex = pos 75 | endOfLine.exec(template) 76 | 77 | parsedLines = template.substr(0, pos).split('\n') 78 | lineNo = parsedLines.length 79 | lastLine = parsedLines[lineNo - 1] 80 | tagStart = contentEnd + whitespace.length 81 | lastTag = template.substr(tagStart + 1, pos - tagStart - 1) 82 | 83 | indent = new Array(lastLine.length - lastTag.length + 1).join(' ') 84 | carets = new Array(lastTag.length + 1).join('^') 85 | lastLine = lastLine + template.substr(pos, endOfLine.lastIndex - pos) 86 | 87 | error = new Error() 88 | error[key] = e[key] for key of e = 89 | "message": "#{msg}\n\nLine #{lineNo}:\n#{lastLine}\n#{indent}#{carets}" 90 | "error": msg, "line": lineNo, "char": indent.length, "tag": lastTag 91 | return error 92 | 93 | # As we start matching things, let's pull out our captures and build indices. 94 | while match = tagPattern.exec(template) 95 | [content, whitespace] = match[1..2] 96 | type = match[3] || match[5] || match[7] || match[9] 97 | tag = match[4] || match[6] || match[8] || match[10] 98 | 99 | contentEnd = (pos + content.length) - 1 100 | pos = tagPattern.lastIndex 101 | 102 | # Standalone tags are tags on lines without any non-whitespace characters. 103 | isStandalone = (contentEnd == -1 or template.charAt(contentEnd) == '\n') && 104 | template.charAt(pos) in [ undefined, '', '\r', '\n' ] 105 | 106 | # We should just add static content to the buffer. 107 | buffer.push(do (content) -> -> content) if content 108 | 109 | # If we're dealing with a standalone tag that's not interpolation, we 110 | # should consume the newline immediately following the tag. If we're not, 111 | # we need to buffer the whitespace we captured earlier. 112 | if isStandalone and type not in ['', '&', '{'] 113 | pos += 1 if template.charAt(pos) == '\r' 114 | pos += 1 if template.charAt(pos) == '\n' 115 | else if whitespace 116 | buffer.push(do (whitespace) -> -> whitespace) 117 | contentEnd += whitespace.length 118 | whitespace = '' 119 | 120 | # Now we'll handle the tag itself: 121 | switch type 122 | 123 | # Comment tags should simply be ignored. 124 | when '!' then break 125 | 126 | # Interpolations are handled by finding the value in the context stack, 127 | # calling and rendering lambdas, and escaping the value if appropriate. 128 | when '', '&', '{' 129 | buildInterpolationTag = (name, is_unescaped) -> 130 | return (context) -> 131 | if (value = Find(name, context) ? '') instanceof Function 132 | value = Expand(this, Parse("#{value()}"), arguments...) 133 | value = @escape("#{value}") unless is_unescaped 134 | return "#{value}" 135 | buffer.push(buildInterpolationTag(tag, type)) 136 | 137 | # Partial data is looked up lazily by the given function, indented as 138 | # appropriate, and then rendered. 139 | when '>' 140 | buildPartialTag = (name, indentation) -> 141 | return (context, partials) -> 142 | partial = partials(name).toString() 143 | partial = partial.replace(/^(?=.)/gm, indentation) if indentation 144 | return Expand(this, Parse(partial), arguments...) 145 | buffer.push(buildPartialTag(tag, whitespace)) 146 | 147 | # Sections and Inverted Sections make a recursive parsing pass, allowing 148 | # us to use the call stack to handle section parsing. This will go until 149 | # it reaches the matching End Section tag, when it will return the 150 | # (cached!) template it parsed, along with the index it stopped at. 151 | when '#', '^' 152 | sectionInfo = 153 | name: tag, start: pos 154 | error: parseError(tagPattern.lastIndex, "Unclosed section '#{tag}'!") 155 | [tmpl, pos] = Parse(template, delimiters, sectionInfo) 156 | 157 | # Sections are rendered by finding the value in the context stack, 158 | # coercing it into an array (unless the value is falsey), and rendering 159 | # the template with each element of the array taking a turn atop the 160 | # context stack. If the value was a function, the template is filtered 161 | # through it before rendering. 162 | sectionInfo['#'] = buildSectionTag = (name, delims, raw) -> 163 | return (context) -> 164 | value = Find(name, context) || [] 165 | tmpl = if value instanceof Function then value(raw) else raw 166 | value = [value] unless value instanceof Array 167 | parsed = Parse(tmpl || '', delims) 168 | 169 | context.push(value) 170 | result = for v in value 171 | context[context.length - 1] = v 172 | Expand(this, parsed, arguments...) 173 | context.pop() 174 | 175 | return result.join('') 176 | 177 | # Inverted Sections render under almost opposite conditions: their 178 | # contents will only be rendered when the retrieved value is either 179 | # falsey or an empty array. 180 | sectionInfo['^'] = buildInvertedSectionTag = (name, delims, raw) -> 181 | return (context) -> 182 | value = Find(name, context) || [] 183 | value = [1] unless value instanceof Array 184 | value = if value.length is 0 then Parse(raw, delims) else [] 185 | return Expand(this, value, arguments...) 186 | 187 | buffer.push(sectionInfo[type](tag, delimiters, tmpl)) 188 | 189 | # When the parser encounters an End Section tag, it runs a couple of 190 | # quick sanity checks, then returns control back to its caller. 191 | when '/' 192 | unless section? 193 | error = "End Section tag '#{tag}' found, but not in section!" 194 | else if tag != (name = section.name) 195 | error = "End Section tag closes '#{tag}'; expected '#{name}'!" 196 | throw parseError(tagPattern.lastIndex, error) if error 197 | 198 | template = template[section.start..contentEnd] 199 | cache[template] = buffer 200 | return [template, pos] 201 | 202 | # The Set Delimiters tag needs to update the delimiters after some error 203 | # checking, and rebuild the regular expression we're using to match tags. 204 | when '=' 205 | unless (delimiters = tag.split(/\s+/)).length == 2 206 | error = "Set Delimiters tags should have two and only two values!" 207 | throw parseError(tagPattern.lastIndex, error) if error 208 | 209 | escape = /[-[\]{}()*+?.,\\^$|#]/g 210 | delimiters = (d.replace(escape, "\\$&") for d in delimiters) 211 | tagPattern = BuildRegex() 212 | 213 | # Any other tag type is probably a typo. 214 | else 215 | throw parseError(tagPattern.lastIndex, "Unknown tag type -- #{type}") 216 | 217 | # Now that we've finished with this tag, we prepare to parse the next one! 218 | tagPattern.lastIndex = if pos? then pos else template.length 219 | 220 | # At this point, we've parsed all the tags. If we've still got a `section`, 221 | # someone left a section tag open. 222 | throw section.error if section? 223 | 224 | # All the tags is not all the content; if there's anything left over, append 225 | # it to the buffer. Then we'll cache the buffer and return it! 226 | buffer.push(-> template[pos..]) unless template.length == pos 227 | return cache[template] = buffer 228 | 229 | # ### Public API 230 | 231 | # The exported object (globally `Milk` in browsers) forms Milk's public API: 232 | Milk = 233 | VERSION: '1.2.0' 234 | # Helpers are a form of context, implicitly on the bottom of the stack. This 235 | # is a global value, and may be either an object or an array. 236 | helpers: [] 237 | # Partials may also be provided globally. 238 | partials: null 239 | # The `escape` method performs basic content escaping, and may be either 240 | # called or overridden with an alternate escaping mechanism. 241 | escape: (value) -> 242 | entities = { '&': 'amp', '"': 'quot', '<': 'lt', '>': 'gt' } 243 | return value.replace(/[&"<>]/g, (ch) -> "&#{ entities[ch] };") 244 | # Rendering is simple: given a template and some data, it populates the 245 | # template. If your template uses Partial Tags, you may also supply a hash or 246 | # a function, or simply override `Milk.partials`. There is no Step Three. 247 | render: (template, data, partials = null) -> 248 | unless (partials ||= @partials || {}) instanceof Function 249 | partials = do (partials) -> (name) -> 250 | throw "Unknown partial '#{name}'!" unless name of partials 251 | return Find(name, [partials]) 252 | 253 | context = if @helpers instanceof Array then @helpers else [@helpers] 254 | return Expand(this, Parse(template), context.concat([data]), partials) 255 | 256 | # Happy hacking! 257 | if exports? 258 | exports[key] = Milk[key] for key of Milk 259 | else 260 | this.Milk = Milk 261 | -------------------------------------------------------------------------------- /dist/v1.2.0/milk.coffee: -------------------------------------------------------------------------------- 1 | # Milk is a simple, fast way to get more Mustache into your CoffeeScript and 2 | # Javascript. 3 | # 4 | # Mustache templates are reasonably simple -- plain text templates are 5 | # sprinkled with "tags", which are (by default) a pair of curly braces 6 | # surrounding some bit of content. A good resource for Mustache can be found 7 | # [here](mustache.github.com). 8 | TemplateCache = {} 9 | 10 | # Tags used for working with data get their data by looking up a name in a 11 | # context stack. This name corresponds to a key in a hash, and the stack is 12 | # searched top to bottom for an object with given key. Dots in names are 13 | # special: a single dot ('.') is "top of stack", and dotted names like 'a.b.c' 14 | # do a chained lookups. 15 | Find = (name, stack, value = null) -> 16 | return stack[stack.length - 1] if name == '.' 17 | [name, parts...] = name.split(/\./) 18 | for i in [stack.length - 1...-1] 19 | continue unless stack[i]? 20 | continue unless typeof stack[i] == 'object' and name of (ctx = stack[i]) 21 | value = ctx[name] 22 | break 23 | 24 | value = Find(part, [value]) for part in parts 25 | 26 | # If we find a function in the stack, we'll treat it as a method, and call it 27 | # with `this` bound to the element it came from. If a method returns a 28 | # function, we treat it as a lambda, which doesn't have a bound `this`. 29 | if value instanceof Function 30 | value = do (value) -> -> 31 | val = value.apply(ctx, arguments) 32 | return (val instanceof Function) and val.apply(null, arguments) or val 33 | 34 | # Null values will be coerced to the empty string. 35 | return value 36 | 37 | # Parsed templates are expanded by simply calling each function in turn. 38 | Expand = (obj, tmpl, args...) -> (f.call(obj, args...) for f in tmpl).join('') 39 | 40 | # For parsing, we'll basically need a template string to parse. We do need to 41 | # remember to take the tag delimiters into account for the cache -- different 42 | # parse trees can exist for the same template string! 43 | Parse = (template, delimiters = ['{{','}}'], section = null) -> 44 | cache = (TemplateCache[delimiters.join(' ')] ||= {}) 45 | return cache[template] if template of cache 46 | 47 | buffer = [] 48 | 49 | # We'll use a regular expression to handle tag discovery. A proper parser 50 | # might be faster, but this is simpler, and certainly fast enough for now. 51 | # Since the tag delimiters may change over time, we'll want to rebuild the 52 | # regex when they change. 53 | BuildRegex = -> 54 | [tagOpen, tagClose] = delimiters 55 | return /// 56 | ([\s\S]*?) # Capture the pre-tag content 57 | ([#{' '}\t]*) # Capture the pre-tag whitespace 58 | (?: #{tagOpen} \s* # Match the opening tag 59 | (?: 60 | (!) \s* ([\s\S]+?) | # Comments 61 | (=) \s* ([\s\S]+?) \s* = | # Set Delimiters 62 | ({) \s* (\w[\S]*?) \s* } | # Triple Mustaches 63 | ([^0-9a-zA-Z._!={]?) \s* ([\w.][\S]*?) # Everything else 64 | ) 65 | \s* #{tagClose} ) # Match the closing tag 66 | ///gm 67 | 68 | tagPattern = BuildRegex() 69 | tagPattern.lastIndex = pos = (section || { start: 0 }).start 70 | 71 | # Useful errors should always be prefered - we should compile as much 72 | # relevant information as possible. 73 | parseError = (pos, msg) -> 74 | (endOfLine = /$/gm).lastIndex = pos 75 | endOfLine.exec(template) 76 | 77 | parsedLines = template.substr(0, pos).split('\n') 78 | lineNo = parsedLines.length 79 | lastLine = parsedLines[lineNo - 1] 80 | tagStart = contentEnd + whitespace.length 81 | lastTag = template.substr(tagStart + 1, pos - tagStart - 1) 82 | 83 | indent = new Array(lastLine.length - lastTag.length + 1).join(' ') 84 | carets = new Array(lastTag.length + 1).join('^') 85 | lastLine = lastLine + template.substr(pos, endOfLine.lastIndex - pos) 86 | 87 | error = new Error() 88 | error[key] = e[key] for key of e = 89 | "message": "#{msg}\n\nLine #{lineNo}:\n#{lastLine}\n#{indent}#{carets}" 90 | "error": msg, "line": lineNo, "char": indent.length, "tag": lastTag 91 | return error 92 | 93 | # As we start matching things, let's pull out our captures and build indices. 94 | while match = tagPattern.exec(template) 95 | [content, whitespace] = match[1..2] 96 | type = match[3] || match[5] || match[7] || match[9] 97 | tag = match[4] || match[6] || match[8] || match[10] 98 | 99 | contentEnd = (pos + content.length) - 1 100 | pos = tagPattern.lastIndex 101 | 102 | # Standalone tags are tags on lines without any non-whitespace characters. 103 | isStandalone = (contentEnd == -1 or template.charAt(contentEnd) == '\n') && 104 | template.charAt(pos) in [ undefined, '', '\r', '\n' ] 105 | 106 | # We should just add static content to the buffer. 107 | buffer.push(do (content) -> -> content) if content 108 | 109 | # If we're dealing with a standalone tag that's not interpolation, we 110 | # should consume the newline immediately following the tag. If we're not, 111 | # we need to buffer the whitespace we captured earlier. 112 | if isStandalone and type not in ['', '&', '{'] 113 | pos += 1 if template.charAt(pos) == '\r' 114 | pos += 1 if template.charAt(pos) == '\n' 115 | else if whitespace 116 | buffer.push(do (whitespace) -> -> whitespace) 117 | contentEnd += whitespace.length 118 | whitespace = '' 119 | 120 | # Now we'll handle the tag itself: 121 | switch type 122 | 123 | # Comment tags should simply be ignored. 124 | when '!' then break 125 | 126 | # Interpolations are handled by finding the value in the context stack, 127 | # calling and rendering lambdas, and escaping the value if appropriate. 128 | when '', '&', '{' 129 | buildInterpolationTag = (name, is_unescaped) -> 130 | return (context) -> 131 | if (value = Find(name, context) ? '') instanceof Function 132 | value = Expand(this, Parse("#{value()}"), arguments...) 133 | value = @escape("#{value}") unless is_unescaped 134 | return "#{value}" 135 | buffer.push(buildInterpolationTag(tag, type)) 136 | 137 | # Partial data is looked up lazily by the given function, indented as 138 | # appropriate, and then rendered. 139 | when '>' 140 | buildPartialTag = (name, indentation) -> 141 | return (context, partials) -> 142 | partial = partials(name).toString() 143 | partial = partial.replace(/^(?=.)/gm, indentation) if indentation 144 | return Expand(this, Parse(partial), arguments...) 145 | buffer.push(buildPartialTag(tag, whitespace)) 146 | 147 | # Sections and Inverted Sections make a recursive parsing pass, allowing 148 | # us to use the call stack to handle section parsing. This will go until 149 | # it reaches the matching End Section tag, when it will return the 150 | # (cached!) template it parsed, along with the index it stopped at. 151 | when '#', '^' 152 | sectionInfo = 153 | name: tag, start: pos 154 | error: parseError(tagPattern.lastIndex, "Unclosed section '#{tag}'!") 155 | [tmpl, pos] = Parse(template, delimiters, sectionInfo) 156 | 157 | # Sections are rendered by finding the value in the context stack, 158 | # coercing it into an array (unless the value is falsey), and rendering 159 | # the template with each element of the array taking a turn atop the 160 | # context stack. If the value was a function, the template is filtered 161 | # through it before rendering. 162 | sectionInfo['#'] = buildSectionTag = (name, delims, raw) -> 163 | return (context) -> 164 | value = Find(name, context) || [] 165 | tmpl = if value instanceof Function then value(raw) else raw 166 | value = [value] unless value instanceof Array 167 | parsed = Parse(tmpl || '', delims) 168 | 169 | context.push(value) 170 | result = for v in value 171 | context[context.length - 1] = v 172 | Expand(this, parsed, arguments...) 173 | context.pop() 174 | 175 | return result.join('') 176 | 177 | # Inverted Sections render under almost opposite conditions: their 178 | # contents will only be rendered when the retrieved value is either 179 | # falsey or an empty array. 180 | sectionInfo['^'] = buildInvertedSectionTag = (name, delims, raw) -> 181 | return (context) -> 182 | value = Find(name, context) || [] 183 | value = [1] unless value instanceof Array 184 | value = if value.length is 0 then Parse(raw, delims) else [] 185 | return Expand(this, value, arguments...) 186 | 187 | buffer.push(sectionInfo[type](tag, delimiters, tmpl)) 188 | 189 | # When the parser encounters an End Section tag, it runs a couple of 190 | # quick sanity checks, then returns control back to its caller. 191 | when '/' 192 | unless section? 193 | error = "End Section tag '#{tag}' found, but not in section!" 194 | else if tag != (name = section.name) 195 | error = "End Section tag closes '#{tag}'; expected '#{name}'!" 196 | throw parseError(tagPattern.lastIndex, error) if error 197 | 198 | template = template[section.start..contentEnd] 199 | cache[template] = buffer 200 | return [template, pos] 201 | 202 | # The Set Delimiters tag needs to update the delimiters after some error 203 | # checking, and rebuild the regular expression we're using to match tags. 204 | when '=' 205 | unless (delimiters = tag.split(/\s+/)).length == 2 206 | error = "Set Delimiters tags should have two and only two values!" 207 | throw parseError(tagPattern.lastIndex, error) if error 208 | 209 | escape = /[-[\]{}()*+?.,\\^$|#]/g 210 | delimiters = (d.replace(escape, "\\$&") for d in delimiters) 211 | tagPattern = BuildRegex() 212 | 213 | # Any other tag type is probably a typo. 214 | else 215 | throw parseError(tagPattern.lastIndex, "Unknown tag type -- #{type}") 216 | 217 | # Now that we've finished with this tag, we prepare to parse the next one! 218 | tagPattern.lastIndex = if pos? then pos else template.length 219 | 220 | # At this point, we've parsed all the tags. If we've still got a `section`, 221 | # someone left a section tag open. 222 | throw section.error if section? 223 | 224 | # All the tags is not all the content; if there's anything left over, append 225 | # it to the buffer. Then we'll cache the buffer and return it! 226 | buffer.push(-> template[pos..]) unless template.length == pos 227 | return cache[template] = buffer 228 | 229 | # ### Public API 230 | 231 | # The exported object (globally `Milk` in browsers) forms Milk's public API: 232 | Milk = 233 | VERSION: '1.2.0' 234 | # Helpers are a form of context, implicitly on the bottom of the stack. This 235 | # is a global value, and may be either an object or an array. 236 | helpers: [] 237 | # Partials may also be provided globally. 238 | partials: null 239 | # The `escape` method performs basic content escaping, and may be either 240 | # called or overridden with an alternate escaping mechanism. 241 | escape: (value) -> 242 | entities = { '&': 'amp', '"': 'quot', '<': 'lt', '>': 'gt' } 243 | return value.replace(/[&"<>]/g, (ch) -> "&#{ entities[ch] };") 244 | # Rendering is simple: given a template and some data, it populates the 245 | # template. If your template uses Partial Tags, you may also supply a hash or 246 | # a function, or simply override `Milk.partials`. There is no Step Three. 247 | render: (template, data, partials = null) -> 248 | unless (partials ||= @partials || {}) instanceof Function 249 | partials = do (partials) -> (name) -> 250 | throw "Unknown partial '#{name}'!" unless name of partials 251 | return Find(name, [partials]) 252 | 253 | context = if @helpers instanceof Array then @helpers else [@helpers] 254 | return Expand(this, Parse(template), context.concat([data]), partials) 255 | 256 | # Happy hacking! 257 | if exports? 258 | exports[key] = Milk[key] for key of Milk 259 | else 260 | this.Milk = Milk 261 | --------------------------------------------------------------------------------