├── VERSION ├── packages └── ember-forms │ ├── lib │ ├── controls.js │ ├── helpers.js │ ├── fields │ │ ├── textarea.js │ │ ├── text.js │ │ ├── password.js │ │ ├── select.js │ │ ├── base.js │ │ └── date.js │ ├── main.js │ ├── fields.js │ ├── labels.js │ ├── buttons.js │ ├── helpers │ │ ├── buttons.js │ │ └── field.js │ ├── label.js │ ├── core.js │ ├── controls │ │ └── unbound_select.js │ └── form.js │ ├── tests │ ├── controls │ │ └── unbound_select.js │ ├── fields │ │ ├── select.js │ │ └── date.js │ └── integration.js │ └── package.json ├── .gitignore ├── .travis.yml ├── project.json ├── generators └── license.js ├── Gemfile ├── config.ru ├── LICENSE ├── .jshintrc ├── tests ├── minispade.js ├── qunit │ ├── run-qunit.js │ ├── qunit.css │ └── qunit.js ├── index.html └── handlebars.js ├── Gemfile.lock ├── README.md ├── lib └── github_uploader.rb ├── Assetfile └── Rakefile /VERSION: -------------------------------------------------------------------------------- 1 | 0.5.0.pre 2 | -------------------------------------------------------------------------------- /packages/ember-forms/lib/controls.js: -------------------------------------------------------------------------------- 1 | require('ember-forms/controls/unbound_select'); 2 | -------------------------------------------------------------------------------- /packages/ember-forms/lib/helpers.js: -------------------------------------------------------------------------------- 1 | require("ember-forms/helpers/buttons"); 2 | require("ember-forms/helpers/field"); 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | .bundle 3 | tmp/ 4 | tests/source/ 5 | dist/ 6 | 7 | .DS_Store 8 | .project 9 | 10 | tests/ember-forms-tests.js 11 | .github-upload-token 12 | -------------------------------------------------------------------------------- /packages/ember-forms/lib/fields/textarea.js: -------------------------------------------------------------------------------- 1 | require("ember-forms/fields/base"); 2 | 3 | EF.TextareaField = EF.BaseField.extend({ 4 | InputView: Ember.TextArea 5 | }); 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | bundler_args: --without development 4 | before_script: 5 | - "export DISPLAY=:99.0" 6 | - "sh -e /etc/init.d/xvfb start" 7 | - "rake clean" 8 | -------------------------------------------------------------------------------- /packages/ember-forms/lib/fields/text.js: -------------------------------------------------------------------------------- 1 | require("ember-forms/fields/base"); 2 | 3 | EF.TextField = EF.BaseField.extend({ 4 | InputView: Ember.TextField.extend({ 5 | attributeBindings: ['name', 'placeholder'] 6 | }) 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ember-forms/lib/main.js: -------------------------------------------------------------------------------- 1 | require("ember-forms/core"); 2 | require("ember-forms/labels"); 3 | require("ember-forms/controls"); 4 | require("ember-forms/fields"); 5 | require("ember-forms/form"); 6 | require("ember-forms/helpers"); 7 | -------------------------------------------------------------------------------- /packages/ember-forms/lib/fields.js: -------------------------------------------------------------------------------- 1 | require("ember-forms/fields/text"); 2 | require("ember-forms/fields/textarea"); 3 | require("ember-forms/fields/select"); 4 | require("ember-forms/fields/date"); 5 | require("ember-forms/fields/password"); 6 | -------------------------------------------------------------------------------- /packages/ember-forms/lib/fields/password.js: -------------------------------------------------------------------------------- 1 | require("ember-forms/fields/base"); 2 | 3 | EF.PasswordField = EF.BaseField.extend({ 4 | InputView: Ember.TextField.extend({ 5 | attributeBindings: ['name', 'placeholder'], 6 | type: 'password' 7 | }) 8 | }); 9 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-forms-project", 3 | "bpm": "1.0.0", 4 | "dependencies": { 5 | "ember-forms": ">= 0", 6 | "ember": ">= 0" 7 | }, 8 | 9 | "bpm:build": { 10 | "bpm_libs.js": { 11 | "minifier": "uglify-js" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /generators/license.js: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Project: Ember Form 3 | // Copyright: ©2012 Josep Jaume Rey and Contributors 4 | // License: Licensed under MIT license (see license.js) 5 | // ========================================================================== 6 | 7 | -------------------------------------------------------------------------------- /packages/ember-forms/lib/labels.js: -------------------------------------------------------------------------------- 1 | EF.Labels = Ember.Object.create({ 2 | months: Ember.A(["January", "February", "March", "April", "May", 3 | "June", "July", "August", "September", "October", "November", 4 | "December"]), 5 | dayPrompt: '- Day -', 6 | monthPrompt: '- Month -', 7 | yearPrompt: '- Year -', 8 | }); 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem "rake-pipeline", :git => "https://github.com/livingsocial/rake-pipeline.git" 4 | gem "rake-pipeline-web-filters", :git => "https://github.com/wycats/rake-pipeline-web-filters.git" 5 | gem "colored" 6 | gem "uglifier", "~> 1.0.3" 7 | 8 | group :development do 9 | gem "rack" 10 | gem "kicker" 11 | gem 'rest-client' 12 | gem 'github_api' 13 | end 14 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'rake-pipeline' 3 | require 'rake-pipeline/middleware' 4 | 5 | class NoCache 6 | def initialize(app) 7 | @app = app 8 | end 9 | 10 | def call(env) 11 | @app.call(env).tap do |status, headers, body| 12 | headers["Cache-Control"] = "no-store" 13 | end 14 | end 15 | end 16 | 17 | use NoCache 18 | use Rake::Pipeline::Middleware, "Assetfile" 19 | run Rack::Directory.new('.') 20 | -------------------------------------------------------------------------------- /packages/ember-forms/tests/controls/unbound_select.js: -------------------------------------------------------------------------------- 1 | test("adds the proper options to an unbound select", function(){ 2 | 3 | var unboundSelect = EF.UnboundSelect.create({ 4 | prompt: 'Hola', 5 | content: [1, 2, 3] 6 | }); 7 | 8 | Ember.run(function(){ 9 | unboundSelect.appendTo("#qunit-fixture"); 10 | unboundSelect.set('value', 2); 11 | }); 12 | 13 | equal(unboundSelect.$("option").length, 4, "renders three options"); 14 | var selectedOption = unboundSelect.$("option[selected]"); 15 | equal(selectedOption.text(), "2", "it selects an element"); 16 | 17 | Ember.run(function(){ 18 | unboundSelect.destroy(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/ember-forms/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-forms", 3 | "summary": "Short package description", 4 | "description": "", 5 | "homepage": "https://github.com/josepjaume/ember-forms", 6 | "author": "Josep Jaume", 7 | "version": "0.0.1.pre", 8 | 9 | "directories": { 10 | "lib": "lib" 11 | }, 12 | 13 | "dependencies": { 14 | "spade": "~> 1.0", 15 | "ember-runtime": ">= 0" 16 | }, 17 | "dependencies:development": { 18 | "spade-qunit": "~> 1.0.0" 19 | }, 20 | "bpm:build": { 21 | "bpm_libs.js": { 22 | "files": ["lib"], 23 | "modes": "*" 24 | }, 25 | "ember-forms/bpm_tests.js": { 26 | "files": ["tests"], 27 | "modes": ["debug"] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/ember-forms/lib/buttons.js: -------------------------------------------------------------------------------- 1 | var findFormRecursively = EF.findFormRecursively; 2 | 3 | /** 4 | @class 5 | Represents a submit button. 6 | 7 | * name: The button text 8 | */ 9 | EF.SubmitButton = Ember.View.extend({ 10 | tagName: 'button', 11 | attributeBindings: ['type'], 12 | type: 'submit', 13 | name: Ember.computed(function(){ return this.get('parentView.submitName'); }), 14 | template: Ember.Handlebars.compile("{{view.name}}") 15 | }); 16 | 17 | EF.Buttons = Ember.ContainerView.extend({ 18 | classNames: ['buttons'], 19 | childViews: [EF.SubmitButton], 20 | form: Ember.computed(function(){ return findFormRecursively(this); }), 21 | submitName: Ember.computed(function(){ return this.get('form.submitName'); }) 22 | }); 23 | -------------------------------------------------------------------------------- /packages/ember-forms/lib/fields/select.js: -------------------------------------------------------------------------------- 1 | require("ember-forms/fields/base"); 2 | 3 | EF.SelectField = EF.BaseField.extend({ 4 | InputView: Ember.Select.extend({ 5 | init: function(){ 6 | var labelPath = this.get('field.optionLabelPath'), 7 | promptValue = this.get('field.prompt'), 8 | valuePath = this.get('field.optionValuePath'); 9 | 10 | if(labelPath){ this.set('optionLabelPath', 'content.' + labelPath); } 11 | if(valuePath){ this.set('optionValuePath', 'content.' + valuePath); } 12 | if(promptValue){ this.set('prompt', this.get('field.prompt')); } 13 | 14 | this._super(); 15 | }, 16 | content: Ember.computed(function(){ 17 | return this.get('field.content') || Ember.A([]); 18 | }).property('field.content') 19 | }) 20 | }); 21 | -------------------------------------------------------------------------------- /packages/ember-forms/lib/helpers/buttons.js: -------------------------------------------------------------------------------- 1 | require("ember-forms/buttons"); 2 | 3 | var findFormRecursively = EF.findFormRecursively, 4 | findField = EF.findField; 5 | 6 | EF.ButtonHelper = Ember.Object.create({ 7 | helper: function(view, options){ 8 | var buttonView = EF.Buttons; 9 | var currentView = options.data.view; 10 | currentView.appendChild(buttonView, options.hash); 11 | } 12 | }); 13 | 14 | /** 15 | A helper to be used in a handlebars template. Will generate a submit button 16 | that intented to trigger the form submission. Usage: 17 | 18 | {{ form buttons }} 19 | 20 | It accepts the following options: 21 | 22 | * name: The button's text 23 | */ 24 | Ember.Handlebars.registerHelper('form', function(name, options){ 25 | if(name === 'buttons'){ 26 | EF.ButtonHelper.helper(this, options); 27 | }else{ 28 | throw "Unknown " + name + " in form helper"; 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /packages/ember-forms/lib/label.js: -------------------------------------------------------------------------------- 1 | require("ember-forms/core"); 2 | 3 | var findFieldRecursively = EF.findFieldRecursively, 4 | findFormRecursively = EF.findFormRecursively; 5 | 6 | /** 7 | @class 8 | @private 9 | 10 | Represents an input's label. Depends on the following attributes: 11 | 12 | * name: The label name. Will fallback to the raw field name 13 | 14 | @extends Ember.View 15 | */ 16 | EF.Label = Ember.View.extend({ 17 | tagName: 'label', 18 | attributeBindings: ['for'], 19 | template: Ember.Handlebars.compile("{{view.name}}"), 20 | field: Ember.computed(function(){ return findFieldRecursively(this); }), 21 | form: Ember.computed(function(){ return this.get('field.formView'); }), 22 | name: Ember.computed(function(){ 23 | return this.get('field.label') || this.get('field.name'); 24 | }), 25 | didInsertElement: function(){ 26 | // We bind it here to avoid re-rendering before the element was inserted 27 | Ember.bind(this, 'for', 'component.inputView.elementId'); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /packages/ember-forms/lib/helpers/field.js: -------------------------------------------------------------------------------- 1 | var findFormRecursively = EF.findFormRecursively, 2 | findField = EF.findField; 3 | 4 | EF.FieldHelper = Ember.Object.create({ 5 | helper: function(view, name, options){ 6 | var optionsHash = options.hash, 7 | type = optionsHash.as, 8 | currentView = options.data.view, 9 | fieldView = findField(type); 10 | 11 | if(Ember.isEmpty(optionsHash.name)){ optionsHash.name = name; } 12 | delete(optionsHash.as); 13 | currentView.appendChild(fieldView, optionsHash); 14 | } 15 | }); 16 | 17 | /** 18 | A handlebars helper that will render a field with its input and label. 19 | 20 | {{ field name }} 21 | 22 | It accepts the following options: 23 | 24 | * as: The field type. Defaults to `text`. Can be `text`, `textarea` and 25 | `select`. 26 | 27 | Any other options will be passed to the particular field instance and may 28 | modify its behavior. 29 | */ 30 | Ember.Handlebars.registerHelper('field', function(name, options){ 31 | EF.FieldHelper.helper(this, name, options); 32 | }); 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 by LivingSocial, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "console", 4 | "Ember", 5 | "EF", 6 | "Handlebars", 7 | "Metamorph", 8 | "ember_assert", 9 | "ember_warn", 10 | "ember_deprecate", 11 | "ember_deprecateFunc", 12 | "require", 13 | "equal", 14 | "test", 15 | "testBoth", 16 | "testWithDefault", 17 | "raises", 18 | "deepEqual", 19 | "start", 20 | "stop", 21 | "ok", 22 | "strictEqual", 23 | "module", 24 | "expect", 25 | "minispade" 26 | ], 27 | 28 | "node" : false, 29 | "es5" : true, 30 | "browser" : true, 31 | 32 | "boss" : true, 33 | "curly": false, 34 | "debug": false, 35 | "devel": false, 36 | "eqeqeq": true, 37 | "evil": true, 38 | "forin": false, 39 | "immed": false, 40 | "laxbreak": false, 41 | "newcap": true, 42 | "noarg": true, 43 | "noempty": false, 44 | "nonew": false, 45 | "nomen": false, 46 | "onevar": false, 47 | "plusplus": false, 48 | "regexp": false, 49 | "undef": true, 50 | "sub": true, 51 | "strict": false, 52 | "white": false 53 | } 54 | -------------------------------------------------------------------------------- /tests/minispade.js: -------------------------------------------------------------------------------- 1 | if (typeof document !== "undefined") { 2 | (function() { 3 | minispade = { 4 | root: null, 5 | modules: {}, 6 | loaded: {}, 7 | 8 | globalEval: function(data) { 9 | if ( data ) { 10 | var ev = "ev"; 11 | var execScript = "execScript"; 12 | 13 | // We use execScript on Internet Explorer 14 | // We use an anonymous function so that context is window 15 | // rather than jQuery in Firefox 16 | ( window[execScript] || function( data ) { 17 | window[ ev+"al" ].call( window, data ); 18 | } )( data ); 19 | } 20 | }, 21 | 22 | require: function(name) { 23 | var loaded = minispade.loaded[name]; 24 | var mod = minispade.modules[name]; 25 | 26 | if (!loaded) { 27 | if (mod) { 28 | minispade.loaded[name] = true; 29 | 30 | if (typeof mod === "string") { 31 | this.globalEval(mod); 32 | } else { 33 | mod(); 34 | } 35 | } else { 36 | if (minispade.root && name.substr(0,minispade.root.length) !== minispade.root) { 37 | return minispade.require(minispade.root+name); 38 | } else { 39 | throw "The module '" + name + "' could not be found"; 40 | } 41 | } 42 | } 43 | 44 | return loaded; 45 | }, 46 | 47 | register: function(name, callback) { 48 | minispade.modules[name] = callback; 49 | } 50 | }; 51 | })(); 52 | } 53 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/livingsocial/rake-pipeline.git 3 | revision: 543f4322fe70facee9572d29ddabf7f090dad68a 4 | specs: 5 | rake-pipeline (0.6.0) 6 | rake (~> 0.9.0) 7 | thor 8 | 9 | GIT 10 | remote: https://github.com/wycats/rake-pipeline-web-filters.git 11 | revision: 81a22fb0808dfdeab8ed92d5d8c898ad198b9938 12 | specs: 13 | rake-pipeline-web-filters (0.6.0) 14 | rack 15 | rake-pipeline (~> 0.6) 16 | 17 | GEM 18 | remote: http://rubygems.org/ 19 | specs: 20 | colored (1.2) 21 | execjs (1.4.0) 22 | multi_json (~> 1.0) 23 | faraday (0.8.1) 24 | multipart-post (~> 1.1) 25 | github_api (0.5.4) 26 | faraday (~> 0.8.0) 27 | hashie (~> 1.2.0) 28 | multi_json (~> 1.3) 29 | nokogiri (~> 1.5.2) 30 | oauth2 (~> 0.7) 31 | hashie (1.2.0) 32 | httpauth (0.1) 33 | kicker (2.5.0) 34 | rb-fsevent 35 | mime-types (1.18) 36 | multi_json (1.3.6) 37 | multipart-post (1.1.5) 38 | nokogiri (1.5.4) 39 | oauth2 (0.7.1) 40 | faraday (~> 0.8) 41 | httpauth (~> 0.1) 42 | multi_json (~> 1.0) 43 | rack (~> 1.4) 44 | rack (1.4.1) 45 | rake (0.9.2.2) 46 | rb-fsevent (0.9.1) 47 | rest-client (1.6.7) 48 | mime-types (>= 1.16) 49 | thor (0.15.2) 50 | uglifier (1.0.4) 51 | execjs (>= 0.3.0) 52 | multi_json (>= 1.0.2) 53 | 54 | PLATFORMS 55 | ruby 56 | 57 | DEPENDENCIES 58 | colored 59 | github_api 60 | kicker 61 | rack 62 | rake-pipeline! 63 | rake-pipeline-web-filters! 64 | rest-client 65 | uglifier (~> 1.0.3) 66 | -------------------------------------------------------------------------------- /packages/ember-forms/lib/core.js: -------------------------------------------------------------------------------- 1 | /** 2 | Default namespace. Here's where you'll find everything related 3 | to ember-forms. 4 | */ 5 | window.EF = Ember.Namespace.create({ 6 | 7 | /** 8 | Will find the container form recursively through the view hierarchy. Since 9 | forms cannot contain other forms (http://www.w3.org/TR/xhtml1/#prohibitions) 10 | this will resolve to a single EF.Form or undefined otherwise. 11 | 12 | @type EF.Form 13 | */ 14 | findFormRecursively: function(view){ 15 | var currentView = view; 16 | do{ 17 | if(currentView.get('isForm') === true){ return currentView; } 18 | }while(currentView = view.get('parentView')); 19 | }, 20 | 21 | /** 22 | Will find the first EF.BaseField in line looking recursively through the 23 | view hierarchy. 24 | 25 | @type EF.BaseField 26 | */ 27 | findFieldRecursively: function(view){ 28 | var currentView = view; 29 | do{ 30 | if(currentView.get('isField') === true){ return currentView; } 31 | }while(currentView = view.get('parentView')); 32 | }, 33 | 34 | /** 35 | Returns a field class given a particular name. For example, 36 | `findField("text")` will return `EF.TextField`. 37 | 38 | @type String 39 | */ 40 | findField: function(name){ 41 | name = name || 'text'; 42 | var fieldName = Ember.String.camelize(name); 43 | fieldName = fieldName.replace(/^./, fieldName[0].toUpperCase()); 44 | fieldName = fieldName + 'Field'; 45 | var field = EF[fieldName]; 46 | if(field){ 47 | return field; 48 | }else{ 49 | throw 'Field ' + name + ' cannot be found'; 50 | } 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /packages/ember-forms/lib/fields/base.js: -------------------------------------------------------------------------------- 1 | require("ember-forms/label"); 2 | 3 | var findFormRecursively = EF.findFormRecursively; 4 | 5 | EF.BaseField = Ember.ContainerView.extend({ 6 | name: null, 7 | formView: null, 8 | tagName: 'div', 9 | classNames: ['input'], 10 | InputView: null, 11 | value: null, 12 | isField: true, 13 | 14 | setFormView: function(){ 15 | var parentView, formView; 16 | 17 | if(parentView = this.get('parentView')){ 18 | formView = findFormRecursively(parentView); 19 | } 20 | if(formView){ 21 | formView.get('fieldViews').pushObject(this); 22 | this.set('formView', formView); 23 | this.set('content', formView.get('content')); 24 | } 25 | }, 26 | 27 | bindValue: function(){ 28 | var name = this.get('name'); 29 | var path = 'content.' + name; 30 | var value = this.get(path); 31 | this.set('value', this.get(path)); 32 | }, 33 | 34 | data: Ember.computed(function(){ 35 | var data = {}; 36 | data[this.get('name')] = this.get('inputView.value'); 37 | return data; 38 | }).volatile(), 39 | 40 | init: function(){ 41 | this._super(); 42 | var labelView = EF.Label.create(), 43 | inputView = this.get('InputView').create({ 44 | field: this, 45 | valueBinding: 'field.value', 46 | name: this.get('name'), 47 | placeholder: this.get('placeholder') 48 | }); 49 | 50 | this.set('labelView', labelView); 51 | this.set('inputView', inputView); 52 | this.pushObject(labelView); 53 | this.pushObject(inputView); 54 | this.setFormView(); 55 | this.bindValue(); 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /packages/ember-forms/tests/fields/select.js: -------------------------------------------------------------------------------- 1 | test("it creates a select", function() { 2 | var select = EF.SelectField.create({ 3 | content: Ember.A([ 4 | {id: 1, name: 'James'}, 5 | {id: 2, name: 'John'} 6 | ]), 7 | optionValuePath: 'id', 8 | optionLabelPath: 'name' 9 | }); 10 | 11 | Ember.run(function(){ 12 | select.appendTo("#qunit-fixture"); 13 | }); 14 | 15 | equal(select.$("option").length, 2, "it is populated with two options"); 16 | var firstOption = select.$("option:first"); 17 | equal(firstOption.attr('value'), "1", "it assigns a value"); 18 | equal(firstOption.text(), "James", "it assigns a name"); 19 | 20 | Ember.run(function(){ 21 | select.destroy(); 22 | }); 23 | }); 24 | 25 | test("it uses an array as both value and label", function() { 26 | var select = EF.SelectField.create({ 27 | content: Ember.A([1, 2, 3]) 28 | }); 29 | 30 | Ember.run(function(){ 31 | select.appendTo("#qunit-fixture"); 32 | }); 33 | 34 | equal(select.$("option").length, 3, "it is populated with three options"); 35 | var firstOption = select.$("option:first"); 36 | equal(firstOption.attr('value'), "1", "it assigns a value"); 37 | equal(firstOption.text(), "1", "it assigns a name"); 38 | 39 | Ember.run(function(){ 40 | select.destroy(); 41 | }); 42 | }); 43 | 44 | test("renders the prompt", function() { 45 | var select = EF.SelectField.create({ 46 | content: Ember.A([1, 2, 3]), 47 | prompt: 'Select a value...' 48 | }); 49 | 50 | Ember.run(function(){ 51 | select.appendTo("#qunit-fixture"); 52 | }); 53 | 54 | var firstOption = select.$("option:first"); 55 | equal(firstOption.text(), "Select a value...", "it assigns a prompt"); 56 | equal(firstOption.attr('value'), ""); 57 | 58 | Ember.run(function(){ 59 | select.destroy(); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/ember-forms/tests/fields/date.js: -------------------------------------------------------------------------------- 1 | test("it creates a date field", function() { 2 | var dateField = EF.DateField.create({}); 3 | 4 | Ember.run(function(){ 5 | dateField.appendTo("#qunit-fixture"); 6 | }); 7 | 8 | equal(dateField.get('value'), undefined, "it is undefined by default"); 9 | 10 | var date = new Date(), 11 | day = date.getUTCDate(), 12 | month = date.getUTCMonth(), 13 | year = date.getUTCFullYear(); 14 | 15 | Ember.run(function(){ 16 | dateField.set('value', date); 17 | }); 18 | 19 | equal(dateField.$('select:first option[selected]').val(), day + ''); 20 | equal(dateField.$('select:nth-child(2) option[selected]').val(), month + ''); 21 | equal(dateField.$('select:nth-child(3) option[selected]').val(), year + ''); 22 | 23 | Ember.run(function(){ 24 | dateField.destroy(); 25 | }); 26 | }); 27 | 28 | test("it adds some appropiate field names", function() { 29 | var dateField = EF.DateField.create({ 30 | name: 'test' 31 | }); 32 | 33 | Ember.run(function(){ 34 | dateField.appendTo("#qunit-fixture"); 35 | }); 36 | 37 | equal(dateField.$('select:first').attr('name'), 'test_day'); 38 | equal(dateField.$('select:nth-child(2)').attr('name'), 'test_month'); 39 | equal(dateField.$('select:nth-child(3)').attr('name'), 'test_year'); 40 | 41 | Ember.run(function(){ 42 | dateField.destroy(); 43 | }); 44 | }); 45 | 46 | test("options", function() { 47 | var dateField = EF.DateField.create({ 48 | startYear: 2050, 49 | endYear: 2012 50 | }); 51 | 52 | Ember.run(function(){ 53 | dateField.appendTo("#qunit-fixture"); 54 | }); 55 | 56 | equal(dateField.$('select:nth-child(3) option:nth-child(2)').val(), '2050', 'it sets the startYear'); 57 | equal(dateField.$('select:nth-child(3) option:last').val(), '2012', 'it sets the endYear'); 58 | 59 | Ember.run(function(){ 60 | dateField.destroy(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /packages/ember-forms/lib/controls/unbound_select.js: -------------------------------------------------------------------------------- 1 | var fmt = Ember.String.fmt; 2 | 3 | /** 4 | `Ember.Select` is meant to be bound to collections with a changing nature, 5 | but with big collections, it comes with a big performance penalty. In order 6 | to address this issue, we've created a "static" one - meaning that won't 7 | change as the associated collection changes. This makes it perfect for things 8 | like date selectors, gender, numerical... 9 | 10 | The collection can be an array of values, or an array of Javascript objects 11 | with `value` and `label` keys. 12 | */ 13 | EF.UnboundSelect = Ember.View.extend({ 14 | tagName: 'select', 15 | template: Ember.Handlebars.compile("{{unbound view.options}}"), 16 | 17 | /** 18 | @private 19 | 20 | Renders the options from the collection. 21 | */ 22 | options: Ember.computed(function(){ 23 | var output; 24 | if(!Ember.isEmpty(this.get('prompt'))){ 25 | output = '' + this.get('prompt') + ''; 26 | } 27 | this.get('content').forEach(function(item){ 28 | var value, label; 29 | if(!Ember.isEmpty(item.value)){ 30 | value = item.value; 31 | label = item.label; 32 | }else{ value = item; label = item; } 33 | var selected = ""; 34 | if(value === this.get('value')){ 35 | selected = ' selected="selected"'; 36 | } 37 | output += fmt('' + label + ''); 38 | }, this); 39 | return (new Handlebars.SafeString(output)); 40 | }).property('value'), 41 | 42 | setValue: Ember.observer(function(){ 43 | if(!this.$()) return; 44 | var value = this.get('value'); 45 | var option = this.$('option[value=' + value + ']'); 46 | option.siblings().attr('selected', null); 47 | option.attr('selected', 'selected'); 48 | }, 'value'), 49 | 50 | didInsertElement: function(){ 51 | var view = this; 52 | this.$().change(function(){ 53 | view.change(); 54 | }); 55 | }, 56 | 57 | change: function(){ 58 | var value = this.$().val(); 59 | this.set('value', value); 60 | } 61 | }); 62 | -------------------------------------------------------------------------------- /packages/ember-forms/lib/form.js: -------------------------------------------------------------------------------- 1 | /** 2 | @class 3 | 4 | EF.Form is a view that contains several fields and can respond to its events, 5 | providing the field's normalized data. 6 | 7 | It will automatically bind to the values of an object, if provided. 8 | 9 | myForm = EF.Form.create({ 10 | objectBinding: 'App.someObject', 11 | template: Ember.Handlebars.compile( 12 | '{{field title }}' + 13 | '{{field body as="textarea"}}' + 14 | '{{form buttons}}' 15 | ), 16 | save: function(data){ this.get('object').setProperties(data); } 17 | }); 18 | 19 | @extends Ember.View 20 | */ 21 | EF.Form = Ember.View.extend({ 22 | tagName: 'form', 23 | classNameBindings: ['name'], 24 | classNames: ['ember-form'], 25 | attributeBindings: ['action'], 26 | fieldViews: Ember.A(), 27 | buttons: ['submit'], 28 | content: null, 29 | isForm: true, 30 | submitName: 'Save', 31 | name: Ember.computed(function(){ 32 | var constructor = this.get('content.constructor'); 33 | if(constructor && constructor.isClass){ 34 | var className = constructor.toString().split('.').pop(); 35 | return Ember.String.decamelize(className); 36 | } 37 | }).property('content'), 38 | 39 | /** 40 | It returns this form fields data in an object. 41 | 42 | myForm.data(); 43 | 44 | Would return: 45 | 46 | { 47 | title: 'Some post title', 48 | content: 'The post content' 49 | } 50 | */ 51 | data: Ember.computed(function(){ 52 | var data = {}; 53 | this.get('fieldViews').forEach(function(field){ 54 | var fieldData = field.get('data'); 55 | for(var key in fieldData){ 56 | data[key] = fieldData[key]; 57 | } 58 | }); 59 | return data; 60 | }).volatile(), 61 | 62 | submit: function(){ 63 | this.save(this.get('data')); 64 | return false; 65 | }, 66 | 67 | /** 68 | This event will be fired when the form is sent, and will receive the form 69 | data as argument. Override it to perform some operation like setting the 70 | properties of an object. 71 | 72 | myForm = EF.Form.create({ 73 | [...] 74 | save: function(data){ 75 | this.get('object').setProperties(data); 76 | } 77 | }); 78 | */ 79 | save: function(data){ } 80 | }); 81 | -------------------------------------------------------------------------------- /tests/qunit/run-qunit.js: -------------------------------------------------------------------------------- 1 | // PhantomJS QUnit Test Runner 2 | 3 | /*globals QUnit phantom*/ 4 | 5 | var args = phantom.args; 6 | if (args.length < 1 || args.length > 2) { 7 | console.log("Usage: " + phantom.scriptName + " "); 8 | phantom.exit(1); 9 | } 10 | 11 | var page = require('webpage').create(); 12 | 13 | page.onConsoleMessage = function(msg) { 14 | if (msg.slice(0,8) === 'WARNING:') { return; } 15 | console.log(msg); 16 | }; 17 | 18 | page.open(args[0], function(status) { 19 | if (status !== 'success') { 20 | console.error("Unable to access network"); 21 | phantom.exit(1); 22 | } else { 23 | page.evaluate(logQUnit); 24 | 25 | var timeout = parseInt(args[1] || 60000, 10); 26 | var start = Date.now(); 27 | var interval = setInterval(function() { 28 | if (Date.now() > start + timeout) { 29 | console.error("Tests timed out"); 30 | phantom.exit(124); 31 | } else { 32 | var qunitDone = page.evaluate(function() { 33 | return window.qunitDone; 34 | }); 35 | 36 | if (qunitDone) { 37 | clearInterval(interval); 38 | if (qunitDone.failed > 0) { 39 | phantom.exit(1); 40 | } else { 41 | phantom.exit(); 42 | } 43 | } 44 | } 45 | }, 500); 46 | } 47 | }); 48 | 49 | function logQUnit() { 50 | var testErrors = []; 51 | var assertionErrors = []; 52 | 53 | console.log("Running: " + JSON.stringify(QUnit.urlParams)); 54 | 55 | QUnit.moduleDone(function(context) { 56 | if (context.failed) { 57 | var msg = "Module Failed: " + context.name + "\n" + testErrors.join("\n"); 58 | console.error(msg); 59 | testErrors = []; 60 | } 61 | }); 62 | 63 | QUnit.testDone(function(context) { 64 | if (context.failed) { 65 | var msg = " Test Failed: " + context.name + assertionErrors.join(" "); 66 | testErrors.push(msg); 67 | assertionErrors = []; 68 | } 69 | }); 70 | 71 | QUnit.log(function(context) { 72 | if (context.result) return; 73 | 74 | var msg = "\n Assertion Failed:"; 75 | if (context.message) { 76 | msg += " " + context.message; 77 | } 78 | 79 | if (context.expected) { 80 | msg += "\n Expected: " + context.expected + ", Actual: " + context.actual; 81 | } 82 | 83 | assertionErrors.push(msg); 84 | }); 85 | 86 | QUnit.done(function(context) { 87 | var stats = [ 88 | "Time: " + context.runtime + "ms", 89 | "Total: " + context.total, 90 | "Passed: " + context.passed, 91 | "Failed: " + context.failed 92 | ]; 93 | console.log(stats.join(", ")); 94 | window.qunitDone = context; 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ember Forms [](http://travis-ci.org/codegram/ember-forms) 2 | 3 | Ember forms is a library for Ember.js to assist in the creation of forms, 4 | binding them to objects and extracting their data. 5 | 6 | ## Usage 7 | 8 | ### Simple example 9 | 10 | Just declare your form as a view extending EF.Form: 11 | 12 | ```Javascript 13 | App.PostForm = EF.Form.extend({ 14 | content: person, 15 | gender: Ember.A([{id: 'm', name: 'Male'}, {id: 'f', name: 'Female'}]), 16 | template: Ember.Handlebars.compile( 17 | '{{field name label="Post title"}}' + 18 | '{{field interests as="textarea"}}' + 19 | '{{field birthday as="date"}}' + 20 | '{{field gender as="select" optionsBinding="formView.gender"}}' + 21 | '{{form buttons name="Save post"}}' 22 | ), 23 | save: function(data){ 24 | this.get('content').setProperties(data); 25 | } 26 | }); 27 | ``` 28 | 29 | ### More complex example 30 | 31 | Just declare your form as a view extending EF.Form: 32 | 33 | ```Javascript 34 | App.PostForm = EF.Form.extend({ 35 | contentBinding: 'App.someObject', 36 | save: function(data){ 37 | this.get('content').setProperties(data); 38 | } 39 | }); 40 | ``` 41 | 42 | Then create a handlebars layout more complex than that: 43 | 44 | ```Handlebars 45 | 46 | User data 47 | {{field name}} 48 | {{field gender}} 49 | 50 | 51 | Other data 52 | {{field comments as="textarea"}} 53 | 54 | {{form buttons}} 55 | ``` 56 | 57 | ## Field types 58 | 59 | Right now only three field types are supported: 60 | 61 | ### text 62 | `` 63 | 64 | ### textarea 65 | `` 66 | 67 | ### select 68 | `` tag with options. Accepts: 69 | 70 | * **content**: An array following [ember's conventions](http://docs.emberjs.com/#doc=Ember.Select&method=content&src=false) 71 | * **optionValuePath**: The name of the `property` that should be used as value. 72 | * **optionLabelPath**: The name of the `property` that should be used as label. 73 | 74 | ### date 75 | Three `` tags representing day, month and year. 76 | 77 | ### All the fields 78 | 79 | All the fields also accept the following options: 80 | * **name**: Overrides the `name` attribute. 81 | * **label**: Overrides the label name 82 | 83 | ## Contributing 84 | 85 | * Fork the project. 86 | * Make your feature addition or bug fix. 87 | * Add specs for it. This is important so we don't break it in a future 88 | version unintentionally. 89 | * Commit, do not mess with rakefile, version, or history. 90 | If you want to have your own version, that is fine but bump version 91 | in a commit by itself I can ignore when I pull. 92 | * Send me a pull request. Bonus points for topic branches. 93 | 94 | ## License 95 | 96 | MIT License. Copyright 2011 [Codegram Technologies](http://codegram.com) 97 | -------------------------------------------------------------------------------- /lib/github_uploader.rb: -------------------------------------------------------------------------------- 1 | require "rest-client" 2 | require "github_api" 3 | require 'json' 4 | 5 | class GithubUploader 6 | 7 | def initialize(login, username, repo, token=nil, root=Dir.pwd) 8 | @login = login 9 | @username = username 10 | @repo = repo 11 | @root = root 12 | @token = token || check_token 13 | end 14 | 15 | def authorized? 16 | !!@token 17 | end 18 | 19 | def token_path 20 | File.expand_path(".github-upload-token", @root) 21 | end 22 | 23 | def check_token 24 | File.exist?(token_path) ? File.open(token_path, "rb").read : nil 25 | end 26 | 27 | def authorize 28 | return if authorized? 29 | 30 | puts "There is no file named .github-upload-token in this folder. This file holds the OAuth token needed to communicate with GitHub." 31 | puts "You will be asked to enter your GitHub password so a new OAuth token will be created." 32 | print "GitHub Password: " 33 | system "stty -echo" # disable echoing of entered chars so password is not shown on console 34 | pw = STDIN.gets.chomp 35 | system "stty echo" # enable echoing of entered chars 36 | puts "" 37 | 38 | # check if the user already granted access for Ember.js Uploader by checking the available authorizations 39 | response = RestClient.get "https://#{@login}:#{pw}@api.github.com/authorizations" 40 | JSON.parse(response.to_str).each do |auth| 41 | if auth["note"] == "Ember.js Uploader" 42 | # user already granted access, so we reuse the existing token 43 | @token = auth["token"] 44 | end 45 | end 46 | 47 | ## we need to create a new token 48 | unless @token 49 | payload = { 50 | :scopes => ["public_repo"], 51 | :note => "Ember.js Uploader", 52 | :note_url => "https://github.com/#{@username}/#{@repo}" 53 | } 54 | response = RestClient.post "https://#{@login}:#{pw}@api.github.com/authorizations", payload.to_json, :content_type => :json 55 | @token = JSON.parse(response.to_str)["token"] 56 | end 57 | 58 | # finally save the token into .github-upload-token 59 | File.open(".github-upload-token", 'w') {|f| f.write(@token)} 60 | end 61 | 62 | def upload_file(filename, description, file) 63 | return false unless authorized? 64 | 65 | gh = Github.new :user => @username, :repo => @repo, :oauth_token => @token 66 | 67 | # remvove previous download with the same name 68 | gh.repos.downloads.list @username, @repo do |download| 69 | if filename == download.name 70 | gh.repos.downloads.delete @username, @repo, download.id 71 | break 72 | end 73 | end 74 | 75 | # step 1 76 | hash = gh.repos.downloads.create @username, @repo, 77 | "name" => filename, 78 | "size" => File.size(file), 79 | "description" => description 80 | 81 | # step 2 82 | gh.repos.downloads.upload hash, file 83 | 84 | return true 85 | end 86 | 87 | end 88 | -------------------------------------------------------------------------------- /packages/ember-forms/lib/fields/date.js: -------------------------------------------------------------------------------- 1 | require("ember-forms/fields/base"); 2 | require("ember-forms/controls/unbound_select"); 3 | require("ember-forms/labels"); 4 | 5 | var e = Ember.isEmpty; 6 | 7 | EF.DateComponent = Ember.ContainerView.extend({ 8 | childViews: ['dayView', 'monthView', 'yearView'], 9 | tagName: 'span', 10 | classNames: ['date'], 11 | 12 | value: Ember.computed(function(key, value){ 13 | var day, month, year; 14 | if (arguments.length === 1){ 15 | day = this.get('dayView.value'); 16 | month = this.get('monthView.value'); 17 | year = this.get('yearView.value'); 18 | if(!e(day) && !e(month) && !e(year)){ 19 | return new Date(year, month, day, 12, 0, 0); 20 | } 21 | }else if(value){ 22 | day = value.getDate() + ''; 23 | month = value.getMonth() + ''; 24 | year = value.getFullYear() + ''; 25 | this.set('dayView.value', day); 26 | this.set('monthView.value', month); 27 | this.set('yearView.value', year); 28 | } 29 | return value; 30 | }).property('dayView.value', 'monthView.value', 'yearView.value'), 31 | 32 | dayView: EF.UnboundSelect.extend({ 33 | attributeBindings: ['name'], 34 | name: Ember.computed(function(){ 35 | return this.get('parentView').get('name') + '_day'; 36 | }), 37 | promptBinding: 'EF.Labels.dayPrompt', 38 | content: Ember.computed(function(){ 39 | var days = []; 40 | for(var i=1; i<=31; i++){ 41 | days.push(i + ''); 42 | } 43 | return Ember.A(days); 44 | }) 45 | }), 46 | 47 | monthView: EF.UnboundSelect.extend({ 48 | promptBinding: 'EF.Labels.monthPrompt', 49 | attributeBindings: ['name'], 50 | name: Ember.computed(function(){ 51 | return this.get('parentView').get('name') + '_month'; 52 | }), 53 | content: Ember.computed(function(){ 54 | var months = EF.Labels.get('months'); 55 | return months.map(function(month, index){ 56 | return {value: (index + ''), label: month}; 57 | }); 58 | }) 59 | }), 60 | 61 | yearView: EF.UnboundSelect.extend({ 62 | promptBinding: 'EF.Labels.yearPrompt', 63 | attributeBindings: ['name'], 64 | name: Ember.computed(function(){ 65 | return this.get('parentView').get('name') + '_year'; 66 | }), 67 | startYear: function(){ 68 | return this.get('parentView.parentView.startYear') || new Date().getFullYear(); 69 | }, 70 | endYear: function(){ 71 | return this.get('parentView.parentView.endYear') || (this.startYear() - 100); 72 | }, 73 | content: Ember.computed(function(){ 74 | var years = []; 75 | for(var i=this.startYear(); i>=this.endYear(); i--){ 76 | years.push(i + ""); 77 | } 78 | return Ember.A(years); 79 | }) 80 | }) 81 | }); 82 | 83 | EF.DateField = EF.BaseField.extend({ 84 | InputView: EF.DateComponent.extend({ 85 | init: function(){ 86 | this._super(); 87 | }, 88 | }), 89 | value: Ember.computed(function(key, value){ 90 | if(arguments.length === 1){ 91 | return this.get('inputView.value'); 92 | }else{ 93 | this.set('inputView.value', value); 94 | return value; 95 | } 96 | }) 97 | }); 98 | -------------------------------------------------------------------------------- /Assetfile: -------------------------------------------------------------------------------- 1 | require "rake-pipeline-web-filters" 2 | require "json" 3 | require "uglifier" 4 | 5 | class EmberProductionFilter < Rake::Pipeline::Filter 6 | def generate_output(inputs, output) 7 | inputs.each do |input| 8 | result = File.read(input.fullpath) 9 | result.gsub!(%r{^(\s)*Ember\.(assert|deprecate|warn)\((.*)\).*$}, "") 10 | output.write result 11 | end 12 | end 13 | end 14 | 15 | class EmberLicenseFilter < Rake::Pipeline::Filter 16 | def generate_output(inputs, output) 17 | inputs.each do |input| 18 | file = File.read(input.fullpath) 19 | license = File.read("generators/license.js") 20 | output.write "#{license}\n\n#{file}" 21 | end 22 | end 23 | end 24 | 25 | class JSHintRC < Rake::Pipeline::Filter 26 | def generate_output(inputs, output) 27 | inputs.each do |input| 28 | file = File.read(input.fullpath) 29 | jshintrc = File.read(".jshintrc") 30 | output.write "var JSHINTRC = #{jshintrc};\n\n#{file}" 31 | end 32 | end 33 | end 34 | 35 | distros = { 36 | :full => %w(ember-forms) 37 | } 38 | 39 | output "dist" 40 | 41 | input "packages" do 42 | output "tests" 43 | 44 | match "*/tests/**/*.js" do 45 | minispade :rewrite_requires => true, :string => true, :module_id_generator => proc { |input| 46 | id = input.path.dup 47 | id.sub!(/\.js$/, '') 48 | id.sub!(/\/main$/, '') 49 | id.sub!('/tests', '/~tests') 50 | id 51 | } 52 | 53 | concat "ember-forms-tests.js" 54 | end 55 | 56 | match "ember-forms-tests.js" do 57 | filter JSHintRC 58 | end 59 | end 60 | 61 | input "packages" do 62 | match "*/lib/**/*.js" do 63 | minispade :rewrite_requires => true, :string => true, :module_id_generator => proc { |input| 64 | id = input.path.dup 65 | id.sub!('/lib/', '/') 66 | id.sub!(/\.js$/, '') 67 | id.sub!(/\/main$/, '') 68 | id 69 | } 70 | 71 | concat "ember-forms-spade.js" 72 | end 73 | end 74 | 75 | input "packages" do 76 | match "*/lib/**/main.js" do 77 | neuter( 78 | :additional_dependencies => proc { |input| 79 | Dir.glob(File.join(File.dirname(input.fullpath),'**','*.js')) 80 | }, 81 | :path_transform => proc { |path, input| 82 | package, path = path.split('/', 2) 83 | current_package = input.path.split('/', 2)[0] 84 | current_package == package && path ? File.join(package, "lib", "#{path}.js") : nil 85 | }, 86 | :closure_wrap => true 87 | ) do |filename| 88 | File.join("modules/", filename.gsub('/lib/main.js', '.js')) 89 | end 90 | end 91 | end 92 | 93 | distros.each do |name, modules| 94 | name = "ember-forms" 95 | 96 | input "dist/modules" do 97 | module_paths = modules.map{|m| "#{m}.js" } 98 | match "{#{module_paths.join(',')}}" do 99 | concat(module_paths){ ["#{name}.js", "#{name}.prod.js"] } 100 | end 101 | 102 | # Strip dev code 103 | match "#{name}.prod.js" do 104 | filter(EmberProductionFilter) { ["#{name}.prod.js", "#{name}.min.js"] } 105 | end 106 | 107 | # Minify 108 | match "#{name}.min.js" do 109 | uglify{ "#{name}.min.js" } 110 | filter EmberLicenseFilter 111 | end 112 | end 113 | end 114 | 115 | # vim: filetype=ruby 116 | -------------------------------------------------------------------------------- /packages/ember-forms/tests/integration.js: -------------------------------------------------------------------------------- 1 | test("it creates a form", function() { 2 | var form = EF.Form.create({ 3 | template: Ember.Handlebars.compile( 4 | '{{ field name label="Your name" }}'+ 5 | '{{ field email as="textarea" }}' + 6 | '{{ form buttons submitName="Create" }}' 7 | ) 8 | }); 9 | 10 | Ember.run(function(){ 11 | form.appendTo("#qunit-fixture"); 12 | }); 13 | 14 | equal(form.$('.input input').length, 1, "it has a text field"); 15 | equal(form.$('.input textarea').length, 1, "it has a text area"); 16 | 17 | equal(form.$('.input label:first').text(), "Your name", "it sets the label"); 18 | 19 | equal(form.$('button[type=submit]').text(), "Create", "it overrides the button name"); 20 | 21 | Ember.run(function(){ 22 | form.destroy(); 23 | }); 24 | }); 25 | 26 | test("it populates a form with content's values", function() { 27 | EF.SomeObjectClass = Ember.Object.extend(); 28 | var content = EF.SomeObjectClass.create({ 29 | name: 'Rafael Nadal', 30 | email: 'rafa@capybara.com', 31 | password: 'test1234' 32 | }); 33 | 34 | var form = EF.Form.create({ 35 | content: content, 36 | template: Ember.Handlebars.compile( 37 | '{{ field name placeholder="Your name"}}'+ 38 | '{{ field email as="textarea"}}' + 39 | '{{ field password as="password" }}' + 40 | '{{ form buttons }}' 41 | ) 42 | }); 43 | 44 | Ember.run(function(){ 45 | form.appendTo("#qunit-fixture"); 46 | }); 47 | 48 | var data = form.get('data'); 49 | equal(form.$("input:first").val(), "Rafael Nadal"); 50 | equal(data.name, 'Rafael Nadal'); 51 | equal(form.$("input:first").attr('placeholder'), "Your name"); 52 | equal(form.$("textarea").val(), "rafa@capybara.com"); 53 | equal(data.email, 'rafa@capybara.com'); 54 | equal(form.$("input[type=password]").val(), "test1234"); 55 | equal(form.get('name'), 'some_object_class', "it sets the class name"); 56 | }); 57 | 58 | test("it select without options", function(){ 59 | var form = EF.Form.create({ 60 | numbers: Ember.A([1, 2, 3]), 61 | template: Ember.Handlebars.compile( 62 | '{{ field age as="select" contentBinding="formView.numbers"}}' 63 | ) 64 | }); 65 | 66 | Ember.run(function(){ 67 | form.appendTo("#qunit-fixture"); 68 | }); 69 | 70 | equal(form.$("option:first").text(), "1"); 71 | 72 | Ember.run(function(){ 73 | form.destroy(); 74 | }); 75 | }); 76 | 77 | test("it allows options for a select", function(){ 78 | var form = EF.Form.create({ 79 | names: Ember.A([{id: 1, fullName: 'John'}]), 80 | template: Ember.Handlebars.compile( 81 | '{{ field age as="select" contentBinding="formView.names" optionValuePath="id" optionLabelPath="fullName"}}' 82 | ) 83 | }); 84 | 85 | Ember.run(function(){ 86 | form.appendTo("#qunit-fixture"); 87 | }); 88 | 89 | equal(form.$("option:first").text(), "John"); 90 | 91 | Ember.run(function(){ 92 | form.destroy(); 93 | }); 94 | }); 95 | 96 | test("it correctly sets a select's selected value", function(){ 97 | var form = EF.Form.create({ 98 | names: Ember.A([{id: 1, fullName: 'John'}, {id: 2, fullName: 'Peter'}]), 99 | content: Ember.Object.create({ 100 | father: 2 101 | }), 102 | template: Ember.Handlebars.compile( 103 | '{{ field father as="select" contentBinding="formView.names" optionValuePath="id" optionLabelPath="fullName"}}' 104 | ) 105 | }); 106 | 107 | Ember.run(function(){ 108 | form.appendTo("#qunit-fixture"); 109 | }); 110 | 111 | equal(form.get('data').father, 2); 112 | 113 | Ember.run(function(){ 114 | form.destroy(); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | abort "Please use Ruby 1.9 to build Ember.js!" if RUBY_VERSION !~ /^1\.9/ 2 | 3 | require "bundler/setup" 4 | require "erb" 5 | require 'rake-pipeline' 6 | require "colored" 7 | 8 | def pipeline 9 | Rake::Pipeline::Project.new("Assetfile") 10 | end 11 | 12 | desc "Strip trailing whitespace for JavaScript files in packages" 13 | task :strip_whitespace do 14 | Dir["packages/**/*.js"].each do |name| 15 | body = File.read(name) 16 | File.open(name, "w") do |file| 17 | file.write body.gsub(/ +\n/, "\n") 18 | end 19 | end 20 | end 21 | 22 | desc "Build ember-forms.js" 23 | task :dist do 24 | puts "Building Ember Forms..." 25 | pipeline.invoke 26 | puts "Done" 27 | end 28 | 29 | desc "Clean build artifacts from previous builds" 30 | task :clean do 31 | puts "Cleaning build..." 32 | pipeline.clean 33 | puts "Done" 34 | end 35 | 36 | desc "Run tests with phantomjs" 37 | task :test, [:suite] => :dist do |t, args| 38 | unless system("which phantomjs > /dev/null 2>&1") 39 | abort "PhantomJS is not installed. Download from http://phantomjs.org" 40 | end 41 | 42 | suites = { 43 | :default => ["package=all"], 44 | :all => ["package=all", 45 | "package=all&jquery=1.6.4&nojshint=true", 46 | "package=all&extendprototypes=true&nojshint=true", 47 | "package=all&extendprototypes=true&jquery=1.6.4&nojshint=true", 48 | "package=all&dist=build&nojshint=true"] 49 | } 50 | 51 | if ENV['TEST'] 52 | opts = [ENV['TEST']] 53 | else 54 | suite = args[:suite] || :default 55 | opts = suites[suite.to_sym] 56 | end 57 | 58 | unless opts 59 | abort "No suite named: #{suite}" 60 | end 61 | 62 | cmd = opts.map do |opt| 63 | "phantomjs tests/qunit/run-qunit.js \"file://localhost#{File.dirname(__FILE__)}/tests/index.html?#{opt}\"" 64 | end.join(' && ') 65 | 66 | # Run the tests 67 | puts "Running: #{opts.join(", ")}" 68 | success = system(cmd) 69 | 70 | if success 71 | puts "Tests Passed".green 72 | else 73 | puts "Tests Failed".red 74 | exit(1) 75 | end 76 | end 77 | 78 | desc "Automatically run tests (Mac OS X only)" 79 | task :autotest do 80 | system("kicker -e 'rake test' packages") 81 | end 82 | 83 | def setup_uploader(root=Dir.pwd) 84 | require './lib/github_uploader' 85 | 86 | login = origin = nil 87 | 88 | Dir.chdir(root) do 89 | # get the github user name 90 | login = `git config github.user`.chomp 91 | 92 | # get repo from git config's origin url 93 | origin = `git config remote.origin.url`.chomp # url to origin 94 | # extract USERNAME/REPO_NAME 95 | # sample urls: https://github.com/emberjs/ember.js.git 96 | # git://github.com/emberjs/ember.js.git 97 | # git@github.com:emberjs/ember.js.git 98 | # git@github.com:emberjs/ember.js 99 | end 100 | 101 | repoUrl = origin.match(/github\.com[\/:]((.+?)\/(.+?))(\.git)?$/) 102 | username = ENV['GH_USERNAME'] || repoUrl[2] # username part of origin url 103 | repo = ENV['GH_REPO'] || repoUrl[3] # repository name part of origin url 104 | 105 | token = ENV["GH_OAUTH_TOKEN"] 106 | 107 | uploader = GithubUploader.new(login, username, repo, token) 108 | uploader.authorize 109 | 110 | uploader 111 | end 112 | 113 | def upload_file(uploader, filename, description, file) 114 | print "Uploading #{filename}..." 115 | if uploader.upload_file(filename, description, file) 116 | puts "Success" 117 | else 118 | puts "Failure" 119 | end 120 | end 121 | 122 | desc "Upload the files to github" 123 | task :upload do 124 | uploader = setup_uploader 125 | upload_file(uploader, 'ember-forms.js', "" , 'dist/ember-forms.js') 126 | upload_file(uploader, 'ember-forms.min.js', "" , 'dist/ember-forms.min.js') 127 | upload_file(uploader, 'ember-forms.prod.js', "" , 'dist/ember-forms.prod.js') 128 | end 129 | 130 | task :default => :test 131 | -------------------------------------------------------------------------------- /tests/qunit/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.4.0pre - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2012 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 15px 15px 0 0; 42 | -moz-border-radius: 15px 15px 0 0; 43 | -webkit-border-top-right-radius: 15px; 44 | -webkit-border-top-left-radius: 15px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-header label { 58 | display: inline-block; 59 | } 60 | 61 | #qunit-banner { 62 | height: 5px; 63 | } 64 | 65 | #qunit-testrunner-toolbar { 66 | padding: 0.5em 0 0.5em 2em; 67 | color: #5E740B; 68 | background-color: #eee; 69 | } 70 | 71 | #qunit-userAgent { 72 | padding: 0.5em 0 0.5em 2.5em; 73 | background-color: #2b81af; 74 | color: #fff; 75 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 76 | } 77 | 78 | 79 | /** Tests: Pass/Fail */ 80 | 81 | #qunit-tests { 82 | list-style-position: inside; 83 | } 84 | 85 | #qunit-tests li { 86 | padding: 0.4em 0.5em 0.4em 2.5em; 87 | border-bottom: 1px solid #fff; 88 | list-style-position: inside; 89 | } 90 | 91 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 92 | display: none; 93 | } 94 | 95 | #qunit-tests li strong { 96 | cursor: pointer; 97 | } 98 | 99 | #qunit-tests li a { 100 | padding: 0.5em; 101 | color: #c2ccd1; 102 | text-decoration: none; 103 | } 104 | #qunit-tests li a:hover, 105 | #qunit-tests li a:focus { 106 | color: #000; 107 | } 108 | 109 | #qunit-tests ol { 110 | margin-top: 0.5em; 111 | padding: 0.5em; 112 | 113 | background-color: #fff; 114 | 115 | border-radius: 15px; 116 | -moz-border-radius: 15px; 117 | -webkit-border-radius: 15px; 118 | 119 | box-shadow: inset 0px 2px 13px #999; 120 | -moz-box-shadow: inset 0px 2px 13px #999; 121 | -webkit-box-shadow: inset 0px 2px 13px #999; 122 | } 123 | 124 | #qunit-tests table { 125 | border-collapse: collapse; 126 | margin-top: .2em; 127 | } 128 | 129 | #qunit-tests th { 130 | text-align: right; 131 | vertical-align: top; 132 | padding: 0 .5em 0 0; 133 | } 134 | 135 | #qunit-tests td { 136 | vertical-align: top; 137 | } 138 | 139 | #qunit-tests pre { 140 | margin: 0; 141 | white-space: pre-wrap; 142 | word-wrap: break-word; 143 | } 144 | 145 | #qunit-tests del { 146 | background-color: #e0f2be; 147 | color: #374e0c; 148 | text-decoration: none; 149 | } 150 | 151 | #qunit-tests ins { 152 | background-color: #ffcaca; 153 | color: #500; 154 | text-decoration: none; 155 | } 156 | 157 | /*** Test Counts */ 158 | 159 | #qunit-tests b.counts { color: black; } 160 | #qunit-tests b.passed { color: #5E740B; } 161 | #qunit-tests b.failed { color: #710909; } 162 | 163 | #qunit-tests li li { 164 | margin: 0.5em; 165 | padding: 0.4em 0.5em 0.4em 0.5em; 166 | background-color: #fff; 167 | border-bottom: none; 168 | list-style-position: inside; 169 | } 170 | 171 | /*** Passing Styles */ 172 | 173 | #qunit-tests li li.pass { 174 | color: #5E740B; 175 | background-color: #fff; 176 | border-left: 26px solid #C6E746; 177 | } 178 | 179 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 180 | #qunit-tests .pass .test-name { color: #366097; } 181 | 182 | #qunit-tests .pass .test-actual, 183 | #qunit-tests .pass .test-expected { color: #999999; } 184 | 185 | #qunit-banner.qunit-pass { background-color: #C6E746; } 186 | 187 | /*** Failing Styles */ 188 | 189 | #qunit-tests li li.fail { 190 | color: #710909; 191 | background-color: #fff; 192 | border-left: 26px solid #EE5757; 193 | white-space: pre; 194 | } 195 | 196 | #qunit-tests > li:last-child { 197 | border-radius: 0 0 15px 15px; 198 | -moz-border-radius: 0 0 15px 15px; 199 | -webkit-border-bottom-right-radius: 15px; 200 | -webkit-border-bottom-left-radius: 15px; 201 | } 202 | 203 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 204 | #qunit-tests .fail .test-name, 205 | #qunit-tests .fail .module-name { color: #000000; } 206 | 207 | #qunit-tests .fail .test-actual { color: #EE5757; } 208 | #qunit-tests .fail .test-expected { color: green; } 209 | 210 | #qunit-banner.qunit-fail { background-color: #EE5757; } 211 | 212 | 213 | /** Result */ 214 | 215 | #qunit-testresult { 216 | padding: 0.5em 0.5em 0.5em 2.5em; 217 | 218 | color: #2b81af; 219 | background-color: #D2E0E6; 220 | 221 | border-bottom: 1px solid white; 222 | } 223 | 224 | /** Fixture */ 225 | 226 | #qunit-fixture { 227 | position: absolute; 228 | top: -10000px; 229 | left: -10000px; 230 | width: 1000px; 231 | height: 1000px; 232 | } 233 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QUnit Test Suite 6 | 7 | 8 | 9 | 10 | 11 | 45 | 46 | 47 | QUnit Test Suite 48 | 49 | 50 | 51 | 52 | test markup 53 | 54 | 61 | 62 | 71 | 72 | 79 | 80 | 83 | 84 | 100 | 101 | 102 | 103 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /tests/qunit/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.4.0pre - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2012 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | (function(window) { 12 | 13 | var defined = { 14 | setTimeout: typeof window.setTimeout !== "undefined", 15 | sessionStorage: (function() { 16 | var x = "qunit-test-string"; 17 | try { 18 | sessionStorage.setItem(x, x); 19 | sessionStorage.removeItem(x); 20 | return true; 21 | } catch(e) { 22 | return false; 23 | } 24 | })() 25 | }; 26 | 27 | var testId = 0, 28 | toString = Object.prototype.toString, 29 | hasOwn = Object.prototype.hasOwnProperty; 30 | 31 | var Test = function(name, testName, expected, async, callback) { 32 | this.name = name; 33 | this.testName = testName; 34 | this.expected = expected; 35 | this.async = async; 36 | this.callback = callback; 37 | this.assertions = []; 38 | }; 39 | Test.prototype = { 40 | init: function() { 41 | var tests = id("qunit-tests"); 42 | if (tests) { 43 | var b = document.createElement("strong"); 44 | b.innerHTML = "Running " + this.name; 45 | var li = document.createElement("li"); 46 | li.appendChild( b ); 47 | li.className = "running"; 48 | li.id = this.id = "test-output" + testId++; 49 | tests.appendChild( li ); 50 | } 51 | }, 52 | setup: function() { 53 | if (this.module != config.previousModule) { 54 | if ( config.previousModule ) { 55 | runLoggingCallbacks('moduleDone', QUnit, { 56 | name: config.previousModule, 57 | failed: config.moduleStats.bad, 58 | passed: config.moduleStats.all - config.moduleStats.bad, 59 | total: config.moduleStats.all 60 | } ); 61 | } 62 | config.previousModule = this.module; 63 | config.moduleStats = { all: 0, bad: 0 }; 64 | runLoggingCallbacks( 'moduleStart', QUnit, { 65 | name: this.module 66 | } ); 67 | } else if (config.autorun) { 68 | runLoggingCallbacks( 'moduleStart', QUnit, { 69 | name: this.module 70 | } ); 71 | } 72 | 73 | config.current = this; 74 | this.testEnvironment = extend({ 75 | setup: function() {}, 76 | teardown: function() {} 77 | }, this.moduleTestEnvironment); 78 | 79 | runLoggingCallbacks( 'testStart', QUnit, { 80 | name: this.testName, 81 | module: this.module 82 | }); 83 | 84 | // allow utility functions to access the current test environment 85 | // TODO why?? 86 | QUnit.current_testEnvironment = this.testEnvironment; 87 | 88 | if ( !config.pollution ) { 89 | saveGlobal(); 90 | } 91 | if ( config.notrycatch ) { 92 | this.testEnvironment.setup.call(this.testEnvironment); 93 | return; 94 | } 95 | try { 96 | this.testEnvironment.setup.call(this.testEnvironment); 97 | } catch(e) { 98 | QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message ); 99 | } 100 | }, 101 | run: function() { 102 | config.current = this; 103 | if ( this.async ) { 104 | QUnit.stop(); 105 | } 106 | 107 | if ( config.notrycatch ) { 108 | this.callback.call(this.testEnvironment); 109 | return; 110 | } 111 | try { 112 | this.callback.call(this.testEnvironment); 113 | } catch(e) { 114 | fail("Test " + this.testName + " died, exception and test follows", e, this.callback); 115 | QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) ); 116 | // else next test will carry the responsibility 117 | saveGlobal(); 118 | 119 | // Restart the tests if they're blocking 120 | if ( config.blocking ) { 121 | QUnit.start(); 122 | } 123 | } 124 | }, 125 | teardown: function() { 126 | config.current = this; 127 | if ( config.notrycatch ) { 128 | this.testEnvironment.teardown.call(this.testEnvironment); 129 | return; 130 | } else { 131 | try { 132 | this.testEnvironment.teardown.call(this.testEnvironment); 133 | } catch(e) { 134 | QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message ); 135 | } 136 | } 137 | checkPollution(); 138 | }, 139 | finish: function() { 140 | config.current = this; 141 | if ( this.expected != null && this.expected != this.assertions.length ) { 142 | QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); 143 | } else if ( this.expected == null && !this.assertions.length ) { 144 | QUnit.ok( false, "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions." ); 145 | } 146 | 147 | var good = 0, bad = 0, 148 | tests = id("qunit-tests"); 149 | 150 | config.stats.all += this.assertions.length; 151 | config.moduleStats.all += this.assertions.length; 152 | 153 | if ( tests ) { 154 | var ol = document.createElement("ol"); 155 | 156 | for ( var i = 0; i < this.assertions.length; i++ ) { 157 | var assertion = this.assertions[i]; 158 | 159 | var li = document.createElement("li"); 160 | li.className = assertion.result ? "pass" : "fail"; 161 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); 162 | ol.appendChild( li ); 163 | 164 | if ( assertion.result ) { 165 | good++; 166 | } else { 167 | bad++; 168 | config.stats.bad++; 169 | config.moduleStats.bad++; 170 | } 171 | } 172 | 173 | // store result when possible 174 | if ( QUnit.config.reorder && defined.sessionStorage ) { 175 | if (bad) { 176 | sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad); 177 | } else { 178 | sessionStorage.removeItem("qunit-" + this.module + "-" + this.testName); 179 | } 180 | } 181 | 182 | if (bad == 0) { 183 | ol.style.display = "none"; 184 | } 185 | 186 | var b = document.createElement("strong"); 187 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 188 | 189 | var a = document.createElement("a"); 190 | a.innerHTML = "Rerun"; 191 | a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 192 | 193 | addEvent(b, "click", function() { 194 | var next = b.nextSibling.nextSibling, 195 | display = next.style.display; 196 | next.style.display = display === "none" ? "block" : "none"; 197 | }); 198 | 199 | addEvent(b, "dblclick", function(e) { 200 | var target = e && e.target ? e.target : window.event.srcElement; 201 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 202 | target = target.parentNode; 203 | } 204 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 205 | window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 206 | } 207 | }); 208 | 209 | var li = id(this.id); 210 | li.className = bad ? "fail" : "pass"; 211 | li.removeChild( li.firstChild ); 212 | li.appendChild( b ); 213 | li.appendChild( a ); 214 | li.appendChild( ol ); 215 | 216 | } else { 217 | for ( var i = 0; i < this.assertions.length; i++ ) { 218 | if ( !this.assertions[i].result ) { 219 | bad++; 220 | config.stats.bad++; 221 | config.moduleStats.bad++; 222 | } 223 | } 224 | } 225 | 226 | try { 227 | QUnit.reset(); 228 | } catch(e) { 229 | fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset); 230 | } 231 | 232 | runLoggingCallbacks( 'testDone', QUnit, { 233 | name: this.testName, 234 | module: this.module, 235 | failed: bad, 236 | passed: this.assertions.length - bad, 237 | total: this.assertions.length 238 | } ); 239 | }, 240 | 241 | queue: function() { 242 | var test = this; 243 | synchronize(function() { 244 | test.init(); 245 | }); 246 | function run() { 247 | // each of these can by async 248 | synchronize(function() { 249 | test.setup(); 250 | }); 251 | synchronize(function() { 252 | test.run(); 253 | }); 254 | synchronize(function() { 255 | test.teardown(); 256 | }); 257 | synchronize(function() { 258 | test.finish(); 259 | }); 260 | } 261 | // defer when previous test run passed, if storage is available 262 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName); 263 | if (bad) { 264 | run(); 265 | } else { 266 | synchronize(run, true); 267 | }; 268 | } 269 | 270 | }; 271 | 272 | var QUnit = { 273 | 274 | // call on start of module test to prepend name to all tests 275 | module: function(name, testEnvironment) { 276 | config.currentModule = name; 277 | config.currentModuleTestEnviroment = testEnvironment; 278 | }, 279 | 280 | asyncTest: function(testName, expected, callback) { 281 | if ( arguments.length === 2 ) { 282 | callback = expected; 283 | expected = null; 284 | } 285 | 286 | QUnit.test(testName, expected, callback, true); 287 | }, 288 | 289 | test: function(testName, expected, callback, async) { 290 | var name = '' + escapeInnerText(testName) + ''; 291 | 292 | if ( arguments.length === 2 ) { 293 | callback = expected; 294 | expected = null; 295 | } 296 | 297 | if ( config.currentModule ) { 298 | name = '' + config.currentModule + ": " + name; 299 | } 300 | 301 | if ( !validTest(config.currentModule + ": " + testName) ) { 302 | return; 303 | } 304 | 305 | var test = new Test(name, testName, expected, async, callback); 306 | test.module = config.currentModule; 307 | test.moduleTestEnvironment = config.currentModuleTestEnviroment; 308 | test.queue(); 309 | }, 310 | 311 | /** 312 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 313 | */ 314 | expect: function(asserts) { 315 | config.current.expected = asserts; 316 | }, 317 | 318 | /** 319 | * Asserts true. 320 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 321 | */ 322 | ok: function(result, msg) { 323 | if (!config.current) { 324 | throw new Error("ok() assertion outside test context, was " + sourceFromStacktrace(2)); 325 | } 326 | result = !!result; 327 | var details = { 328 | result: result, 329 | message: msg 330 | }; 331 | msg = escapeInnerText(msg || (result ? "okay" : "failed")); 332 | if ( !result ) { 333 | var source = sourceFromStacktrace(2); 334 | if (source) { 335 | details.source = source; 336 | msg += 'Source: ' + escapeInnerText(source) + ''; 337 | } 338 | } 339 | runLoggingCallbacks( 'log', QUnit, details ); 340 | config.current.assertions.push({ 341 | result: result, 342 | message: msg 343 | }); 344 | }, 345 | 346 | /** 347 | * Checks that the first two arguments are equal, with an optional message. 348 | * Prints out both actual and expected values. 349 | * 350 | * Prefered to ok( actual == expected, message ) 351 | * 352 | * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 353 | * 354 | * @param Object actual 355 | * @param Object expected 356 | * @param String message (optional) 357 | */ 358 | equal: function(actual, expected, message) { 359 | QUnit.push(expected == actual, actual, expected, message); 360 | }, 361 | 362 | notEqual: function(actual, expected, message) { 363 | QUnit.push(expected != actual, actual, expected, message); 364 | }, 365 | 366 | deepEqual: function(actual, expected, message) { 367 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 368 | }, 369 | 370 | notDeepEqual: function(actual, expected, message) { 371 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); 372 | }, 373 | 374 | strictEqual: function(actual, expected, message) { 375 | QUnit.push(expected === actual, actual, expected, message); 376 | }, 377 | 378 | notStrictEqual: function(actual, expected, message) { 379 | QUnit.push(expected !== actual, actual, expected, message); 380 | }, 381 | 382 | raises: function(block, expected, message) { 383 | var actual, ok = false; 384 | 385 | if (typeof expected === 'string') { 386 | message = expected; 387 | expected = null; 388 | } 389 | 390 | try { 391 | block(); 392 | } catch (e) { 393 | actual = e; 394 | } 395 | 396 | if (actual) { 397 | // we don't want to validate thrown error 398 | if (!expected) { 399 | ok = true; 400 | // expected is a regexp 401 | } else if (QUnit.objectType(expected) === "regexp") { 402 | ok = expected.test(actual); 403 | // expected is a constructor 404 | } else if (actual instanceof expected) { 405 | ok = true; 406 | // expected is a validation function which returns true is validation passed 407 | } else if (expected.call({}, actual) === true) { 408 | ok = true; 409 | } 410 | } 411 | 412 | QUnit.ok(ok, message); 413 | }, 414 | 415 | start: function(count) { 416 | config.semaphore -= count || 1; 417 | if (config.semaphore > 0) { 418 | // don't start until equal number of stop-calls 419 | return; 420 | } 421 | if (config.semaphore < 0) { 422 | // ignore if start is called more often then stop 423 | config.semaphore = 0; 424 | } 425 | // A slight delay, to avoid any current callbacks 426 | if ( defined.setTimeout ) { 427 | window.setTimeout(function() { 428 | if (config.semaphore > 0) { 429 | return; 430 | } 431 | if ( config.timeout ) { 432 | clearTimeout(config.timeout); 433 | } 434 | 435 | config.blocking = false; 436 | process(true); 437 | }, 13); 438 | } else { 439 | config.blocking = false; 440 | process(true); 441 | } 442 | }, 443 | 444 | stop: function(count) { 445 | config.semaphore += count || 1; 446 | config.blocking = true; 447 | 448 | if ( config.testTimeout && defined.setTimeout ) { 449 | clearTimeout(config.timeout); 450 | config.timeout = window.setTimeout(function() { 451 | QUnit.ok( false, "Test timed out" ); 452 | config.semaphore = 1; 453 | QUnit.start(); 454 | }, config.testTimeout); 455 | } 456 | } 457 | }; 458 | 459 | //We want access to the constructor's prototype 460 | (function() { 461 | function F(){}; 462 | F.prototype = QUnit; 463 | QUnit = new F(); 464 | //Make F QUnit's constructor so that we can add to the prototype later 465 | QUnit.constructor = F; 466 | })(); 467 | 468 | // deprecated; still export them to window to provide clear error messages 469 | // next step: remove entirely 470 | QUnit.equals = function() { 471 | throw new Error("QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead"); 472 | }; 473 | QUnit.same = function() { 474 | throw new Error("QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead"); 475 | }; 476 | 477 | // Maintain internal state 478 | var config = { 479 | // The queue of tests to run 480 | queue: [], 481 | 482 | // block until document ready 483 | blocking: true, 484 | 485 | // when enabled, show only failing tests 486 | // gets persisted through sessionStorage and can be changed in UI via checkbox 487 | hidepassed: false, 488 | 489 | // by default, run previously failed tests first 490 | // very useful in combination with "Hide passed tests" checked 491 | reorder: true, 492 | 493 | // by default, modify document.title when suite is done 494 | altertitle: true, 495 | 496 | urlConfig: ['noglobals', 'notrycatch'], 497 | 498 | //logging callback queues 499 | begin: [], 500 | done: [], 501 | log: [], 502 | testStart: [], 503 | testDone: [], 504 | moduleStart: [], 505 | moduleDone: [] 506 | }; 507 | 508 | // Load paramaters 509 | (function() { 510 | var location = window.location || { search: "", protocol: "file:" }, 511 | params = location.search.slice( 1 ).split( "&" ), 512 | length = params.length, 513 | urlParams = {}, 514 | current; 515 | 516 | if ( params[ 0 ] ) { 517 | for ( var i = 0; i < length; i++ ) { 518 | current = params[ i ].split( "=" ); 519 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 520 | // allow just a key to turn on a flag, e.g., test.html?noglobals 521 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 522 | urlParams[ current[ 0 ] ] = current[ 1 ]; 523 | } 524 | } 525 | 526 | QUnit.urlParams = urlParams; 527 | config.filter = urlParams.filter; 528 | 529 | // Figure out if we're running the tests from a server or not 530 | QUnit.isLocal = !!(location.protocol === 'file:'); 531 | })(); 532 | 533 | // Expose the API as global variables, unless an 'exports' 534 | // object exists, in that case we assume we're in CommonJS - export everything at the end 535 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 536 | extend(window, QUnit); 537 | window.QUnit = QUnit; 538 | } 539 | 540 | // define these after exposing globals to keep them in these QUnit namespace only 541 | extend(QUnit, { 542 | config: config, 543 | 544 | // Initialize the configuration options 545 | init: function() { 546 | extend(config, { 547 | stats: { all: 0, bad: 0 }, 548 | moduleStats: { all: 0, bad: 0 }, 549 | started: +new Date, 550 | updateRate: 1000, 551 | blocking: false, 552 | autostart: true, 553 | autorun: false, 554 | filter: "", 555 | queue: [], 556 | semaphore: 0 557 | }); 558 | 559 | var qunit = id( "qunit" ); 560 | if ( qunit ) { 561 | qunit.innerHTML = 562 | '' + escapeInnerText( document.title ) + '' + 563 | '' + 564 | '' + 565 | '' + 566 | ''; 567 | } 568 | 569 | var tests = id( "qunit-tests" ), 570 | banner = id( "qunit-banner" ), 571 | result = id( "qunit-testresult" ); 572 | 573 | if ( tests ) { 574 | tests.innerHTML = ""; 575 | } 576 | 577 | if ( banner ) { 578 | banner.className = ""; 579 | } 580 | 581 | if ( result ) { 582 | result.parentNode.removeChild( result ); 583 | } 584 | 585 | if ( tests ) { 586 | result = document.createElement( "p" ); 587 | result.id = "qunit-testresult"; 588 | result.className = "result"; 589 | tests.parentNode.insertBefore( result, tests ); 590 | result.innerHTML = 'Running... '; 591 | } 592 | }, 593 | 594 | /** 595 | * Resets the test setup. Useful for tests that modify the DOM. 596 | * 597 | * If jQuery is available, uses jQuery's html(), otherwise just innerHTML. 598 | */ 599 | reset: function() { 600 | if ( window.jQuery ) { 601 | jQuery( "#qunit-fixture" ).html( config.fixture ); 602 | } else { 603 | var main = id( 'qunit-fixture' ); 604 | if ( main ) { 605 | main.innerHTML = config.fixture; 606 | } 607 | } 608 | }, 609 | 610 | /** 611 | * Trigger an event on an element. 612 | * 613 | * @example triggerEvent( document.body, "click" ); 614 | * 615 | * @param DOMElement elem 616 | * @param String type 617 | */ 618 | triggerEvent: function( elem, type, event ) { 619 | if ( document.createEvent ) { 620 | event = document.createEvent("MouseEvents"); 621 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 622 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 623 | elem.dispatchEvent( event ); 624 | 625 | } else if ( elem.fireEvent ) { 626 | elem.fireEvent("on"+type); 627 | } 628 | }, 629 | 630 | // Safe object type checking 631 | is: function( type, obj ) { 632 | return QUnit.objectType( obj ) == type; 633 | }, 634 | 635 | objectType: function( obj ) { 636 | if (typeof obj === "undefined") { 637 | return "undefined"; 638 | 639 | // consider: typeof null === object 640 | } 641 | if (obj === null) { 642 | return "null"; 643 | } 644 | 645 | var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || ''; 646 | 647 | switch (type) { 648 | case 'Number': 649 | if (isNaN(obj)) { 650 | return "nan"; 651 | } else { 652 | return "number"; 653 | } 654 | case 'String': 655 | case 'Boolean': 656 | case 'Array': 657 | case 'Date': 658 | case 'RegExp': 659 | case 'Function': 660 | return type.toLowerCase(); 661 | } 662 | if (typeof obj === "object") { 663 | return "object"; 664 | } 665 | return undefined; 666 | }, 667 | 668 | push: function(result, actual, expected, message) { 669 | if (!config.current) { 670 | throw new Error("assertion outside test context, was " + sourceFromStacktrace()); 671 | } 672 | var details = { 673 | result: result, 674 | message: message, 675 | actual: actual, 676 | expected: expected 677 | }; 678 | 679 | message = escapeInnerText(message) || (result ? "okay" : "failed"); 680 | message = '' + message + ""; 681 | var output = message; 682 | if (!result) { 683 | expected = escapeInnerText(QUnit.jsDump.parse(expected)); 684 | actual = escapeInnerText(QUnit.jsDump.parse(actual)); 685 | output += 'Expected: ' + expected + ''; 686 | if (actual != expected) { 687 | output += 'Result: ' + actual + ''; 688 | output += 'Diff: ' + QUnit.diff(expected, actual) +''; 689 | } 690 | var source = sourceFromStacktrace(); 691 | if (source) { 692 | details.source = source; 693 | output += 'Source: ' + escapeInnerText(source) + ''; 694 | } 695 | output += ""; 696 | } 697 | 698 | runLoggingCallbacks( 'log', QUnit, details ); 699 | 700 | config.current.assertions.push({ 701 | result: !!result, 702 | message: output 703 | }); 704 | }, 705 | 706 | url: function( params ) { 707 | params = extend( extend( {}, QUnit.urlParams ), params ); 708 | var querystring = "?", 709 | key; 710 | for ( key in params ) { 711 | if ( !hasOwn.call( params, key ) ) { 712 | continue; 713 | } 714 | querystring += encodeURIComponent( key ) + "=" + 715 | encodeURIComponent( params[ key ] ) + "&"; 716 | } 717 | return window.location.pathname + querystring.slice( 0, -1 ); 718 | }, 719 | 720 | extend: extend, 721 | id: id, 722 | addEvent: addEvent 723 | }); 724 | 725 | //QUnit.constructor is set to the empty F() above so that we can add to it's prototype later 726 | //Doing this allows us to tell if the following methods have been overwritten on the actual 727 | //QUnit object, which is a deprecated way of using the callbacks. 728 | extend(QUnit.constructor.prototype, { 729 | // Logging callbacks; all receive a single argument with the listed properties 730 | // run test/logs.html for any related changes 731 | begin: registerLoggingCallback('begin'), 732 | // done: { failed, passed, total, runtime } 733 | done: registerLoggingCallback('done'), 734 | // log: { result, actual, expected, message } 735 | log: registerLoggingCallback('log'), 736 | // testStart: { name } 737 | testStart: registerLoggingCallback('testStart'), 738 | // testDone: { name, failed, passed, total } 739 | testDone: registerLoggingCallback('testDone'), 740 | // moduleStart: { name } 741 | moduleStart: registerLoggingCallback('moduleStart'), 742 | // moduleDone: { name, failed, passed, total } 743 | moduleDone: registerLoggingCallback('moduleDone') 744 | }); 745 | 746 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 747 | config.autorun = true; 748 | } 749 | 750 | QUnit.load = function() { 751 | runLoggingCallbacks( 'begin', QUnit, {} ); 752 | 753 | // Initialize the config, saving the execution queue 754 | var oldconfig = extend({}, config); 755 | QUnit.init(); 756 | extend(config, oldconfig); 757 | 758 | config.blocking = false; 759 | 760 | var urlConfigHtml = '', len = config.urlConfig.length; 761 | for ( var i = 0, val; i < len, val = config.urlConfig[i]; i++ ) { 762 | config[val] = QUnit.urlParams[val]; 763 | urlConfigHtml += '' + val + ''; 764 | } 765 | 766 | var userAgent = id("qunit-userAgent"); 767 | if ( userAgent ) { 768 | userAgent.innerHTML = navigator.userAgent; 769 | } 770 | var banner = id("qunit-header"); 771 | if ( banner ) { 772 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + urlConfigHtml; 773 | addEvent( banner, "change", function( event ) { 774 | var params = {}; 775 | params[ event.target.name ] = event.target.checked ? true : undefined; 776 | window.location = QUnit.url( params ); 777 | }); 778 | } 779 | 780 | var toolbar = id("qunit-testrunner-toolbar"); 781 | if ( toolbar ) { 782 | var filter = document.createElement("input"); 783 | filter.type = "checkbox"; 784 | filter.id = "qunit-filter-pass"; 785 | addEvent( filter, "click", function() { 786 | var ol = document.getElementById("qunit-tests"); 787 | if ( filter.checked ) { 788 | ol.className = ol.className + " hidepass"; 789 | } else { 790 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 791 | ol.className = tmp.replace(/ hidepass /, " "); 792 | } 793 | if ( defined.sessionStorage ) { 794 | if (filter.checked) { 795 | sessionStorage.setItem("qunit-filter-passed-tests", "true"); 796 | } else { 797 | sessionStorage.removeItem("qunit-filter-passed-tests"); 798 | } 799 | } 800 | }); 801 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { 802 | filter.checked = true; 803 | var ol = document.getElementById("qunit-tests"); 804 | ol.className = ol.className + " hidepass"; 805 | } 806 | toolbar.appendChild( filter ); 807 | 808 | var label = document.createElement("label"); 809 | label.setAttribute("for", "qunit-filter-pass"); 810 | label.innerHTML = "Hide passed tests"; 811 | toolbar.appendChild( label ); 812 | } 813 | 814 | var main = id('qunit-fixture'); 815 | if ( main ) { 816 | config.fixture = main.innerHTML; 817 | } 818 | 819 | if (config.autostart) { 820 | QUnit.start(); 821 | } 822 | }; 823 | 824 | addEvent(window, "load", QUnit.load); 825 | 826 | // addEvent(window, "error") gives us a useless event object 827 | window.onerror = function( message, file, line ) { 828 | if ( QUnit.config.current ) { 829 | ok( false, message + ", " + file + ":" + line ); 830 | } else { 831 | test( "global failure", function() { 832 | ok( false, message + ", " + file + ":" + line ); 833 | }); 834 | } 835 | }; 836 | 837 | function done() { 838 | config.autorun = true; 839 | 840 | // Log the last module results 841 | if ( config.currentModule ) { 842 | runLoggingCallbacks( 'moduleDone', QUnit, { 843 | name: config.currentModule, 844 | failed: config.moduleStats.bad, 845 | passed: config.moduleStats.all - config.moduleStats.bad, 846 | total: config.moduleStats.all 847 | } ); 848 | } 849 | 850 | var banner = id("qunit-banner"), 851 | tests = id("qunit-tests"), 852 | runtime = +new Date - config.started, 853 | passed = config.stats.all - config.stats.bad, 854 | html = [ 855 | 'Tests completed in ', 856 | runtime, 857 | ' milliseconds.', 858 | '', 859 | passed, 860 | ' tests of ', 861 | config.stats.all, 862 | ' passed, ', 863 | config.stats.bad, 864 | ' failed.' 865 | ].join(''); 866 | 867 | if ( banner ) { 868 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 869 | } 870 | 871 | if ( tests ) { 872 | id( "qunit-testresult" ).innerHTML = html; 873 | } 874 | 875 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 876 | // show ✖ for good, ✔ for bad suite result in title 877 | // use escape sequences in case file gets loaded with non-utf-8-charset 878 | document.title = [ 879 | (config.stats.bad ? "\u2716" : "\u2714"), 880 | document.title.replace(/^[\u2714\u2716] /i, "") 881 | ].join(" "); 882 | } 883 | 884 | // clear own sessionStorage items if all tests passed 885 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 886 | for (var key in sessionStorage) { 887 | if (sessionStorage.hasOwnProperty(key) && key.indexOf("qunit-") === 0 ) { 888 | sessionStorage.removeItem(key); 889 | } 890 | } 891 | } 892 | 893 | runLoggingCallbacks( 'done', QUnit, { 894 | failed: config.stats.bad, 895 | passed: passed, 896 | total: config.stats.all, 897 | runtime: runtime 898 | } ); 899 | } 900 | 901 | function validTest( name ) { 902 | var filter = config.filter, 903 | run = false; 904 | 905 | if ( !filter ) { 906 | return true; 907 | } 908 | 909 | var not = filter.charAt( 0 ) === "!"; 910 | if ( not ) { 911 | filter = filter.slice( 1 ); 912 | } 913 | 914 | if ( name.indexOf( filter ) !== -1 ) { 915 | return !not; 916 | } 917 | 918 | if ( not ) { 919 | run = true; 920 | } 921 | 922 | return run; 923 | } 924 | 925 | // so far supports only Firefox, Chrome and Opera (buggy) 926 | // could be extended in the future to use something like https://github.com/csnover/TraceKit 927 | function sourceFromStacktrace(offset) { 928 | offset = offset || 3; 929 | try { 930 | throw new Error(); 931 | } catch ( e ) { 932 | if (e.stacktrace) { 933 | // Opera 934 | return e.stacktrace.split("\n")[offset + 3]; 935 | } else if (e.stack) { 936 | // Firefox, Chrome 937 | var stack = e.stack.split("\n"); 938 | if (/^error$/i.test(stack[0])) { 939 | stack.shift(); 940 | } 941 | return stack[offset]; 942 | } else if (e.sourceURL) { 943 | // Safari, PhantomJS 944 | // TODO sourceURL points at the 'throw new Error' line above, useless 945 | //return e.sourceURL + ":" + e.line; 946 | } 947 | } 948 | } 949 | 950 | function escapeInnerText(s) { 951 | if (!s) { 952 | return ""; 953 | } 954 | s = s + ""; 955 | return s.replace(/[\&<>]/g, function(s) { 956 | switch(s) { 957 | case "&": return "&"; 958 | case "<": return "<"; 959 | case ">": return ">"; 960 | default: return s; 961 | } 962 | }); 963 | } 964 | 965 | function synchronize( callback, last ) { 966 | config.queue.push( callback ); 967 | 968 | if ( config.autorun && !config.blocking ) { 969 | process(last); 970 | } 971 | } 972 | 973 | function process( last ) { 974 | var start = new Date().getTime(); 975 | config.depth = config.depth ? config.depth + 1 : 1; 976 | 977 | while ( config.queue.length && !config.blocking ) { 978 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 979 | config.queue.shift()(); 980 | } else { 981 | window.setTimeout( function(){ 982 | process( last ); 983 | }, 13 ); 984 | break; 985 | } 986 | } 987 | config.depth--; 988 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 989 | done(); 990 | } 991 | } 992 | 993 | function saveGlobal() { 994 | config.pollution = []; 995 | 996 | if ( config.noglobals ) { 997 | for ( var key in window ) { 998 | if ( !hasOwn.call( window, key ) ) { 999 | continue; 1000 | } 1001 | config.pollution.push( key ); 1002 | } 1003 | } 1004 | } 1005 | 1006 | function checkPollution( name ) { 1007 | var old = config.pollution; 1008 | saveGlobal(); 1009 | 1010 | var newGlobals = diff( config.pollution, old ); 1011 | if ( newGlobals.length > 0 ) { 1012 | ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); 1013 | } 1014 | 1015 | var deletedGlobals = diff( old, config.pollution ); 1016 | if ( deletedGlobals.length > 0 ) { 1017 | ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); 1018 | } 1019 | } 1020 | 1021 | // returns a new Array with the elements that are in a but not in b 1022 | function diff( a, b ) { 1023 | var result = a.slice(); 1024 | for ( var i = 0; i < result.length; i++ ) { 1025 | for ( var j = 0; j < b.length; j++ ) { 1026 | if ( result[i] === b[j] ) { 1027 | result.splice(i, 1); 1028 | i--; 1029 | break; 1030 | } 1031 | } 1032 | } 1033 | return result; 1034 | } 1035 | 1036 | function fail(message, exception, callback) { 1037 | if ( typeof console !== "undefined" && console.error && console.warn ) { 1038 | console.error(message); 1039 | console.error(exception); 1040 | console.error(exception.stack); 1041 | console.warn(callback.toString()); 1042 | 1043 | } else if ( window.opera && opera.postError ) { 1044 | opera.postError(message, exception, callback.toString); 1045 | } 1046 | } 1047 | 1048 | function extend(a, b) { 1049 | for ( var prop in b ) { 1050 | if ( b[prop] === undefined ) { 1051 | delete a[prop]; 1052 | 1053 | // Avoid "Member not found" error in IE8 caused by setting window.constructor 1054 | } else if ( prop !== "constructor" || a !== window ) { 1055 | a[prop] = b[prop]; 1056 | } 1057 | } 1058 | 1059 | return a; 1060 | } 1061 | 1062 | function addEvent(elem, type, fn) { 1063 | if ( elem.addEventListener ) { 1064 | elem.addEventListener( type, fn, false ); 1065 | } else if ( elem.attachEvent ) { 1066 | elem.attachEvent( "on" + type, fn ); 1067 | } else { 1068 | fn(); 1069 | } 1070 | } 1071 | 1072 | function id(name) { 1073 | return !!(typeof document !== "undefined" && document && document.getElementById) && 1074 | document.getElementById( name ); 1075 | } 1076 | 1077 | function registerLoggingCallback(key){ 1078 | return function(callback){ 1079 | config[key].push( callback ); 1080 | }; 1081 | } 1082 | 1083 | // Supports deprecated method of completely overwriting logging callbacks 1084 | function runLoggingCallbacks(key, scope, args) { 1085 | //debugger; 1086 | var callbacks; 1087 | if ( QUnit.hasOwnProperty(key) ) { 1088 | QUnit[key].call(scope, args); 1089 | } else { 1090 | callbacks = config[key]; 1091 | for( var i = 0; i < callbacks.length; i++ ) { 1092 | callbacks[i].call( scope, args ); 1093 | } 1094 | } 1095 | } 1096 | 1097 | // Test for equality any JavaScript type. 1098 | // Author: Philippe Rathé 1099 | QUnit.equiv = function () { 1100 | 1101 | var innerEquiv; // the real equiv function 1102 | var callers = []; // stack to decide between skip/abort functions 1103 | var parents = []; // stack to avoiding loops from circular referencing 1104 | 1105 | // Call the o related callback with the given arguments. 1106 | function bindCallbacks(o, callbacks, args) { 1107 | var prop = QUnit.objectType(o); 1108 | if (prop) { 1109 | if (QUnit.objectType(callbacks[prop]) === "function") { 1110 | return callbacks[prop].apply(callbacks, args); 1111 | } else { 1112 | return callbacks[prop]; // or undefined 1113 | } 1114 | } 1115 | } 1116 | 1117 | var getProto = Object.getPrototypeOf || function (obj) { 1118 | return obj.__proto__; 1119 | }; 1120 | 1121 | var callbacks = function () { 1122 | 1123 | // for string, boolean, number and null 1124 | function useStrictEquality(b, a) { 1125 | if (b instanceof a.constructor || a instanceof b.constructor) { 1126 | // to catch short annotaion VS 'new' annotation of a 1127 | // declaration 1128 | // e.g. var i = 1; 1129 | // var j = new Number(1); 1130 | return a == b; 1131 | } else { 1132 | return a === b; 1133 | } 1134 | } 1135 | 1136 | return { 1137 | "string" : useStrictEquality, 1138 | "boolean" : useStrictEquality, 1139 | "number" : useStrictEquality, 1140 | "null" : useStrictEquality, 1141 | "undefined" : useStrictEquality, 1142 | 1143 | "nan" : function(b) { 1144 | return isNaN(b); 1145 | }, 1146 | 1147 | "date" : function(b, a) { 1148 | return QUnit.objectType(b) === "date" 1149 | && a.valueOf() === b.valueOf(); 1150 | }, 1151 | 1152 | "regexp" : function(b, a) { 1153 | return QUnit.objectType(b) === "regexp" 1154 | && a.source === b.source && // the regex itself 1155 | a.global === b.global && // and its modifers 1156 | // (gmi) ... 1157 | a.ignoreCase === b.ignoreCase 1158 | && a.multiline === b.multiline; 1159 | }, 1160 | 1161 | // - skip when the property is a method of an instance (OOP) 1162 | // - abort otherwise, 1163 | // initial === would have catch identical references anyway 1164 | "function" : function() { 1165 | var caller = callers[callers.length - 1]; 1166 | return caller !== Object && typeof caller !== "undefined"; 1167 | }, 1168 | 1169 | "array" : function(b, a) { 1170 | var i, j, loop; 1171 | var len; 1172 | 1173 | // b could be an object literal here 1174 | if (!(QUnit.objectType(b) === "array")) { 1175 | return false; 1176 | } 1177 | 1178 | len = a.length; 1179 | if (len !== b.length) { // safe and faster 1180 | return false; 1181 | } 1182 | 1183 | // track reference to avoid circular references 1184 | parents.push(a); 1185 | for (i = 0; i < len; i++) { 1186 | loop = false; 1187 | for (j = 0; j < parents.length; j++) { 1188 | if (parents[j] === a[i]) { 1189 | loop = true;// dont rewalk array 1190 | } 1191 | } 1192 | if (!loop && !innerEquiv(a[i], b[i])) { 1193 | parents.pop(); 1194 | return false; 1195 | } 1196 | } 1197 | parents.pop(); 1198 | return true; 1199 | }, 1200 | 1201 | "object" : function(b, a) { 1202 | var i, j, loop; 1203 | var eq = true; // unless we can proove it 1204 | var aProperties = [], bProperties = []; // collection of 1205 | // strings 1206 | 1207 | // comparing constructors is more strict than using 1208 | // instanceof 1209 | if (a.constructor !== b.constructor) { 1210 | // Allow objects with no prototype to be equivalent to 1211 | // objects with Object as their constructor. 1212 | if (!((getProto(a) === null && getProto(b) === Object.prototype) || 1213 | (getProto(b) === null && getProto(a) === Object.prototype))) 1214 | { 1215 | return false; 1216 | } 1217 | } 1218 | 1219 | // stack constructor before traversing properties 1220 | callers.push(a.constructor); 1221 | // track reference to avoid circular references 1222 | parents.push(a); 1223 | 1224 | for (i in a) { // be strict: don't ensures hasOwnProperty 1225 | // and go deep 1226 | loop = false; 1227 | for (j = 0; j < parents.length; j++) { 1228 | if (parents[j] === a[i]) 1229 | loop = true; // don't go down the same path 1230 | // twice 1231 | } 1232 | aProperties.push(i); // collect a's properties 1233 | 1234 | if (!loop && !innerEquiv(a[i], b[i])) { 1235 | eq = false; 1236 | break; 1237 | } 1238 | } 1239 | 1240 | callers.pop(); // unstack, we are done 1241 | parents.pop(); 1242 | 1243 | for (i in b) { 1244 | bProperties.push(i); // collect b's properties 1245 | } 1246 | 1247 | // Ensures identical properties name 1248 | return eq 1249 | && innerEquiv(aProperties.sort(), bProperties 1250 | .sort()); 1251 | } 1252 | }; 1253 | }(); 1254 | 1255 | innerEquiv = function() { // can take multiple arguments 1256 | var args = Array.prototype.slice.apply(arguments); 1257 | if (args.length < 2) { 1258 | return true; // end transition 1259 | } 1260 | 1261 | return (function(a, b) { 1262 | if (a === b) { 1263 | return true; // catch the most you can 1264 | } else if (a === null || b === null || typeof a === "undefined" 1265 | || typeof b === "undefined" 1266 | || QUnit.objectType(a) !== QUnit.objectType(b)) { 1267 | return false; // don't lose time with error prone cases 1268 | } else { 1269 | return bindCallbacks(a, callbacks, [ b, a ]); 1270 | } 1271 | 1272 | // apply transition with (1..n) arguments 1273 | })(args[0], args[1]) 1274 | && arguments.callee.apply(this, args.splice(1, 1275 | args.length - 1)); 1276 | }; 1277 | 1278 | return innerEquiv; 1279 | 1280 | }(); 1281 | 1282 | /** 1283 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1284 | * http://flesler.blogspot.com Licensed under BSD 1285 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1286 | * 1287 | * @projectDescription Advanced and extensible data dumping for Javascript. 1288 | * @version 1.0.0 1289 | * @author Ariel Flesler 1290 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1291 | */ 1292 | QUnit.jsDump = (function() { 1293 | function quote( str ) { 1294 | return '"' + str.toString().replace(/"/g, '\\"') + '"'; 1295 | }; 1296 | function literal( o ) { 1297 | return o + ''; 1298 | }; 1299 | function join( pre, arr, post ) { 1300 | var s = jsDump.separator(), 1301 | base = jsDump.indent(), 1302 | inner = jsDump.indent(1); 1303 | if ( arr.join ) 1304 | arr = arr.join( ',' + s + inner ); 1305 | if ( !arr ) 1306 | return pre + post; 1307 | return [ pre, inner + arr, base + post ].join(s); 1308 | }; 1309 | function array( arr, stack ) { 1310 | var i = arr.length, ret = Array(i); 1311 | this.up(); 1312 | while ( i-- ) 1313 | ret[i] = this.parse( arr[i] , undefined , stack); 1314 | this.down(); 1315 | return join( '[', ret, ']' ); 1316 | }; 1317 | 1318 | var reName = /^function (\w+)/; 1319 | 1320 | var jsDump = { 1321 | parse:function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance 1322 | stack = stack || [ ]; 1323 | var parser = this.parsers[ type || this.typeOf(obj) ]; 1324 | type = typeof parser; 1325 | var inStack = inArray(obj, stack); 1326 | if (inStack != -1) { 1327 | return 'recursion('+(inStack - stack.length)+')'; 1328 | } 1329 | //else 1330 | if (type == 'function') { 1331 | stack.push(obj); 1332 | var res = parser.call( this, obj, stack ); 1333 | stack.pop(); 1334 | return res; 1335 | } 1336 | // else 1337 | return (type == 'string') ? parser : this.parsers.error; 1338 | }, 1339 | typeOf:function( obj ) { 1340 | var type; 1341 | if ( obj === null ) { 1342 | type = "null"; 1343 | } else if (typeof obj === "undefined") { 1344 | type = "undefined"; 1345 | } else if (QUnit.is("RegExp", obj)) { 1346 | type = "regexp"; 1347 | } else if (QUnit.is("Date", obj)) { 1348 | type = "date"; 1349 | } else if (QUnit.is("Function", obj)) { 1350 | type = "function"; 1351 | } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") { 1352 | type = "window"; 1353 | } else if (obj.nodeType === 9) { 1354 | type = "document"; 1355 | } else if (obj.nodeType) { 1356 | type = "node"; 1357 | } else if ( 1358 | // native arrays 1359 | toString.call( obj ) === "[object Array]" || 1360 | // NodeList objects 1361 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1362 | ) { 1363 | type = "array"; 1364 | } else { 1365 | type = typeof obj; 1366 | } 1367 | return type; 1368 | }, 1369 | separator:function() { 1370 | return this.multiline ? this.HTML ? '' : '\n' : this.HTML ? ' ' : ' '; 1371 | }, 1372 | indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1373 | if ( !this.multiline ) 1374 | return ''; 1375 | var chr = this.indentChar; 1376 | if ( this.HTML ) 1377 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1378 | return Array( this._depth_ + (extra||0) ).join(chr); 1379 | }, 1380 | up:function( a ) { 1381 | this._depth_ += a || 1; 1382 | }, 1383 | down:function( a ) { 1384 | this._depth_ -= a || 1; 1385 | }, 1386 | setParser:function( name, parser ) { 1387 | this.parsers[name] = parser; 1388 | }, 1389 | // The next 3 are exposed so you can use them 1390 | quote:quote, 1391 | literal:literal, 1392 | join:join, 1393 | // 1394 | _depth_: 1, 1395 | // This is the list of parsers, to modify them, use jsDump.setParser 1396 | parsers:{ 1397 | window: '[Window]', 1398 | document: '[Document]', 1399 | error:'[ERROR]', //when no parser is found, shouldn't happen 1400 | unknown: '[Unknown]', 1401 | 'null':'null', 1402 | 'undefined':'undefined', 1403 | 'function':function( fn ) { 1404 | var ret = 'function', 1405 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1406 | if ( name ) 1407 | ret += ' ' + name; 1408 | ret += '('; 1409 | 1410 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); 1411 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); 1412 | }, 1413 | array: array, 1414 | nodelist: array, 1415 | arguments: array, 1416 | object:function( map, stack ) { 1417 | var ret = [ ], keys, key, val, i; 1418 | QUnit.jsDump.up(); 1419 | if (Object.keys) { 1420 | keys = Object.keys( map ); 1421 | } else { 1422 | keys = []; 1423 | for (key in map) { keys.push( key ); } 1424 | } 1425 | keys.sort(); 1426 | for (i = 0; i < keys.length; i++) { 1427 | key = keys[ i ]; 1428 | val = map[ key ]; 1429 | ret.push( QUnit.jsDump.parse( key, 'key' ) + ': ' + QUnit.jsDump.parse( val, undefined, stack ) ); 1430 | } 1431 | QUnit.jsDump.down(); 1432 | return join( '{', ret, '}' ); 1433 | }, 1434 | node:function( node ) { 1435 | var open = QUnit.jsDump.HTML ? '<' : '<', 1436 | close = QUnit.jsDump.HTML ? '>' : '>'; 1437 | 1438 | var tag = node.nodeName.toLowerCase(), 1439 | ret = open + tag; 1440 | 1441 | for ( var a in QUnit.jsDump.DOMAttrs ) { 1442 | var val = node[QUnit.jsDump.DOMAttrs[a]]; 1443 | if ( val ) 1444 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); 1445 | } 1446 | return ret + close + open + '/' + tag + close; 1447 | }, 1448 | functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function 1449 | var l = fn.length; 1450 | if ( !l ) return ''; 1451 | 1452 | var args = Array(l); 1453 | while ( l-- ) 1454 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1455 | return ' ' + args.join(', ') + ' '; 1456 | }, 1457 | key:quote, //object calls it internally, the key part of an item in a map 1458 | functionCode:'[code]', //function calls it internally, it's the content of the function 1459 | attribute:quote, //node calls it internally, it's an html attribute value 1460 | string:quote, 1461 | date:quote, 1462 | regexp:literal, //regex 1463 | number:literal, 1464 | 'boolean':literal 1465 | }, 1466 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1467 | id:'id', 1468 | name:'name', 1469 | 'class':'className' 1470 | }, 1471 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1472 | indentChar:' ',//indentation unit 1473 | multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1474 | }; 1475 | 1476 | return jsDump; 1477 | })(); 1478 | 1479 | // from Sizzle.js 1480 | function getText( elems ) { 1481 | var ret = "", elem; 1482 | 1483 | for ( var i = 0; elems[i]; i++ ) { 1484 | elem = elems[i]; 1485 | 1486 | // Get the text from text nodes and CDATA nodes 1487 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1488 | ret += elem.nodeValue; 1489 | 1490 | // Traverse everything else, except comment nodes 1491 | } else if ( elem.nodeType !== 8 ) { 1492 | ret += getText( elem.childNodes ); 1493 | } 1494 | } 1495 | 1496 | return ret; 1497 | }; 1498 | 1499 | //from jquery.js 1500 | function inArray( elem, array ) { 1501 | if ( array.indexOf ) { 1502 | return array.indexOf( elem ); 1503 | } 1504 | 1505 | for ( var i = 0, length = array.length; i < length; i++ ) { 1506 | if ( array[ i ] === elem ) { 1507 | return i; 1508 | } 1509 | } 1510 | 1511 | return -1; 1512 | } 1513 | 1514 | /* 1515 | * Javascript Diff Algorithm 1516 | * By John Resig (http://ejohn.org/) 1517 | * Modified by Chu Alan "sprite" 1518 | * 1519 | * Released under the MIT license. 1520 | * 1521 | * More Info: 1522 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1523 | * 1524 | * Usage: QUnit.diff(expected, actual) 1525 | * 1526 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1527 | */ 1528 | QUnit.diff = (function() { 1529 | function diff(o, n) { 1530 | var ns = {}; 1531 | var os = {}; 1532 | 1533 | for (var i = 0; i < n.length; i++) { 1534 | if (ns[n[i]] == null) 1535 | ns[n[i]] = { 1536 | rows: [], 1537 | o: null 1538 | }; 1539 | ns[n[i]].rows.push(i); 1540 | } 1541 | 1542 | for (var i = 0; i < o.length; i++) { 1543 | if (os[o[i]] == null) 1544 | os[o[i]] = { 1545 | rows: [], 1546 | n: null 1547 | }; 1548 | os[o[i]].rows.push(i); 1549 | } 1550 | 1551 | for (var i in ns) { 1552 | if ( !hasOwn.call( ns, i ) ) { 1553 | continue; 1554 | } 1555 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1556 | n[ns[i].rows[0]] = { 1557 | text: n[ns[i].rows[0]], 1558 | row: os[i].rows[0] 1559 | }; 1560 | o[os[i].rows[0]] = { 1561 | text: o[os[i].rows[0]], 1562 | row: ns[i].rows[0] 1563 | }; 1564 | } 1565 | } 1566 | 1567 | for (var i = 0; i < n.length - 1; i++) { 1568 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1569 | n[i + 1] == o[n[i].row + 1]) { 1570 | n[i + 1] = { 1571 | text: n[i + 1], 1572 | row: n[i].row + 1 1573 | }; 1574 | o[n[i].row + 1] = { 1575 | text: o[n[i].row + 1], 1576 | row: i + 1 1577 | }; 1578 | } 1579 | } 1580 | 1581 | for (var i = n.length - 1; i > 0; i--) { 1582 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1583 | n[i - 1] == o[n[i].row - 1]) { 1584 | n[i - 1] = { 1585 | text: n[i - 1], 1586 | row: n[i].row - 1 1587 | }; 1588 | o[n[i].row - 1] = { 1589 | text: o[n[i].row - 1], 1590 | row: i - 1 1591 | }; 1592 | } 1593 | } 1594 | 1595 | return { 1596 | o: o, 1597 | n: n 1598 | }; 1599 | } 1600 | 1601 | return function(o, n) { 1602 | o = o.replace(/\s+$/, ''); 1603 | n = n.replace(/\s+$/, ''); 1604 | var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); 1605 | 1606 | var str = ""; 1607 | 1608 | var oSpace = o.match(/\s+/g); 1609 | if (oSpace == null) { 1610 | oSpace = [" "]; 1611 | } 1612 | else { 1613 | oSpace.push(" "); 1614 | } 1615 | var nSpace = n.match(/\s+/g); 1616 | if (nSpace == null) { 1617 | nSpace = [" "]; 1618 | } 1619 | else { 1620 | nSpace.push(" "); 1621 | } 1622 | 1623 | if (out.n.length == 0) { 1624 | for (var i = 0; i < out.o.length; i++) { 1625 | str += '' + out.o[i] + oSpace[i] + ""; 1626 | } 1627 | } 1628 | else { 1629 | if (out.n[0].text == null) { 1630 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1631 | str += '' + out.o[n] + oSpace[n] + ""; 1632 | } 1633 | } 1634 | 1635 | for (var i = 0; i < out.n.length; i++) { 1636 | if (out.n[i].text == null) { 1637 | str += '' + out.n[i] + nSpace[i] + ""; 1638 | } 1639 | else { 1640 | var pre = ""; 1641 | 1642 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1643 | pre += '' + out.o[n] + oSpace[n] + ""; 1644 | } 1645 | str += " " + out.n[i].text + nSpace[i] + pre; 1646 | } 1647 | } 1648 | } 1649 | 1650 | return str; 1651 | }; 1652 | })(); 1653 | 1654 | // for CommonJS enviroments, export everything 1655 | if ( typeof exports !== "undefined" || typeof require !== "undefined" ) { 1656 | extend(exports, QUnit); 1657 | } 1658 | 1659 | // get at whatever the global object is, like window in browsers 1660 | })( (function() {return this}).call() ); 1661 | -------------------------------------------------------------------------------- /tests/handlebars.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (C) 2011 by Yehuda Katz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | */ 24 | 25 | // lib/handlebars/base.js 26 | 27 | /*jshint eqnull:true*/ 28 | this.Handlebars = {}; 29 | 30 | (function(Handlebars) { 31 | 32 | Handlebars.VERSION = "1.0.0-rc.3"; 33 | Handlebars.COMPILER_REVISION = 2; 34 | 35 | Handlebars.REVISION_CHANGES = { 36 | 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it 37 | 2: '>= 1.0.0-rc.3' 38 | }; 39 | 40 | Handlebars.helpers = {}; 41 | Handlebars.partials = {}; 42 | 43 | Handlebars.registerHelper = function(name, fn, inverse) { 44 | if(inverse) { fn.not = inverse; } 45 | this.helpers[name] = fn; 46 | }; 47 | 48 | Handlebars.registerPartial = function(name, str) { 49 | this.partials[name] = str; 50 | }; 51 | 52 | Handlebars.registerHelper('helperMissing', function(arg) { 53 | if(arguments.length === 2) { 54 | return undefined; 55 | } else { 56 | throw new Error("Could not find property '" + arg + "'"); 57 | } 58 | }); 59 | 60 | var toString = Object.prototype.toString, functionType = "[object Function]"; 61 | 62 | Handlebars.registerHelper('blockHelperMissing', function(context, options) { 63 | var inverse = options.inverse || function() {}, fn = options.fn; 64 | 65 | 66 | var ret = ""; 67 | var type = toString.call(context); 68 | 69 | if(type === functionType) { context = context.call(this); } 70 | 71 | if(context === true) { 72 | return fn(this); 73 | } else if(context === false || context == null) { 74 | return inverse(this); 75 | } else if(type === "[object Array]") { 76 | if(context.length > 0) { 77 | return Handlebars.helpers.each(context, options); 78 | } else { 79 | return inverse(this); 80 | } 81 | } else { 82 | return fn(context); 83 | } 84 | }); 85 | 86 | Handlebars.K = function() {}; 87 | 88 | Handlebars.createFrame = Object.create || function(object) { 89 | Handlebars.K.prototype = object; 90 | var obj = new Handlebars.K(); 91 | Handlebars.K.prototype = null; 92 | return obj; 93 | }; 94 | 95 | Handlebars.logger = { 96 | DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3, 97 | 98 | methodMap: {0: 'debug', 1: 'info', 2: 'warn', 3: 'error'}, 99 | 100 | // can be overridden in the host environment 101 | log: function(level, obj) { 102 | if (Handlebars.logger.level <= level) { 103 | var method = Handlebars.logger.methodMap[level]; 104 | if (typeof console !== 'undefined' && console[method]) { 105 | console[method].call(console, obj); 106 | } 107 | } 108 | } 109 | }; 110 | 111 | Handlebars.log = function(level, obj) { Handlebars.logger.log(level, obj); }; 112 | 113 | Handlebars.registerHelper('each', function(context, options) { 114 | var fn = options.fn, inverse = options.inverse; 115 | var i = 0, ret = "", data; 116 | 117 | if (options.data) { 118 | data = Handlebars.createFrame(options.data); 119 | } 120 | 121 | if(context && typeof context === 'object') { 122 | if(context instanceof Array){ 123 | for(var j = context.length; i 2) { 331 | expected.push("'" + this.terminals_[p] + "'"); 332 | } 333 | if (this.lexer.showPosition) { 334 | errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'"; 335 | } else { 336 | errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'"); 337 | } 338 | this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected}); 339 | } 340 | } 341 | if (action[0] instanceof Array && action.length > 1) { 342 | throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol); 343 | } 344 | switch (action[0]) { 345 | case 1: 346 | stack.push(symbol); 347 | vstack.push(this.lexer.yytext); 348 | lstack.push(this.lexer.yylloc); 349 | stack.push(action[1]); 350 | symbol = null; 351 | if (!preErrorSymbol) { 352 | yyleng = this.lexer.yyleng; 353 | yytext = this.lexer.yytext; 354 | yylineno = this.lexer.yylineno; 355 | yyloc = this.lexer.yylloc; 356 | if (recovering > 0) 357 | recovering--; 358 | } else { 359 | symbol = preErrorSymbol; 360 | preErrorSymbol = null; 361 | } 362 | break; 363 | case 2: 364 | len = this.productions_[action[1]][1]; 365 | yyval.$ = vstack[vstack.length - len]; 366 | yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column}; 367 | if (ranges) { 368 | yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]]; 369 | } 370 | r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); 371 | if (typeof r !== "undefined") { 372 | return r; 373 | } 374 | if (len) { 375 | stack = stack.slice(0, -1 * len * 2); 376 | vstack = vstack.slice(0, -1 * len); 377 | lstack = lstack.slice(0, -1 * len); 378 | } 379 | stack.push(this.productions_[action[1]][0]); 380 | vstack.push(yyval.$); 381 | lstack.push(yyval._$); 382 | newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; 383 | stack.push(newState); 384 | break; 385 | case 3: 386 | return true; 387 | } 388 | } 389 | return true; 390 | } 391 | }; 392 | /* Jison generated lexer */ 393 | var lexer = (function(){ 394 | var lexer = ({EOF:1, 395 | parseError:function parseError(str, hash) { 396 | if (this.yy.parser) { 397 | this.yy.parser.parseError(str, hash); 398 | } else { 399 | throw new Error(str); 400 | } 401 | }, 402 | setInput:function (input) { 403 | this._input = input; 404 | this._more = this._less = this.done = false; 405 | this.yylineno = this.yyleng = 0; 406 | this.yytext = this.matched = this.match = ''; 407 | this.conditionStack = ['INITIAL']; 408 | this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0}; 409 | if (this.options.ranges) this.yylloc.range = [0,0]; 410 | this.offset = 0; 411 | return this; 412 | }, 413 | input:function () { 414 | var ch = this._input[0]; 415 | this.yytext += ch; 416 | this.yyleng++; 417 | this.offset++; 418 | this.match += ch; 419 | this.matched += ch; 420 | var lines = ch.match(/(?:\r\n?|\n).*/g); 421 | if (lines) { 422 | this.yylineno++; 423 | this.yylloc.last_line++; 424 | } else { 425 | this.yylloc.last_column++; 426 | } 427 | if (this.options.ranges) this.yylloc.range[1]++; 428 | 429 | this._input = this._input.slice(1); 430 | return ch; 431 | }, 432 | unput:function (ch) { 433 | var len = ch.length; 434 | var lines = ch.split(/(?:\r\n?|\n)/g); 435 | 436 | this._input = ch + this._input; 437 | this.yytext = this.yytext.substr(0, this.yytext.length-len-1); 438 | //this.yyleng -= len; 439 | this.offset -= len; 440 | var oldLines = this.match.split(/(?:\r\n?|\n)/g); 441 | this.match = this.match.substr(0, this.match.length-1); 442 | this.matched = this.matched.substr(0, this.matched.length-1); 443 | 444 | if (lines.length-1) this.yylineno -= lines.length-1; 445 | var r = this.yylloc.range; 446 | 447 | this.yylloc = {first_line: this.yylloc.first_line, 448 | last_line: this.yylineno+1, 449 | first_column: this.yylloc.first_column, 450 | last_column: lines ? 451 | (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length: 452 | this.yylloc.first_column - len 453 | }; 454 | 455 | if (this.options.ranges) { 456 | this.yylloc.range = [r[0], r[0] + this.yyleng - len]; 457 | } 458 | return this; 459 | }, 460 | more:function () { 461 | this._more = true; 462 | return this; 463 | }, 464 | less:function (n) { 465 | this.unput(this.match.slice(n)); 466 | }, 467 | pastInput:function () { 468 | var past = this.matched.substr(0, this.matched.length - this.match.length); 469 | return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); 470 | }, 471 | upcomingInput:function () { 472 | var next = this.match; 473 | if (next.length < 20) { 474 | next += this._input.substr(0, 20-next.length); 475 | } 476 | return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, ""); 477 | }, 478 | showPosition:function () { 479 | var pre = this.pastInput(); 480 | var c = new Array(pre.length + 1).join("-"); 481 | return pre + this.upcomingInput() + "\n" + c+"^"; 482 | }, 483 | next:function () { 484 | if (this.done) { 485 | return this.EOF; 486 | } 487 | if (!this._input) this.done = true; 488 | 489 | var token, 490 | match, 491 | tempMatch, 492 | index, 493 | col, 494 | lines; 495 | if (!this._more) { 496 | this.yytext = ''; 497 | this.match = ''; 498 | } 499 | var rules = this._currentRules(); 500 | for (var i=0;i < rules.length; i++) { 501 | tempMatch = this._input.match(this.rules[rules[i]]); 502 | if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { 503 | match = tempMatch; 504 | index = i; 505 | if (!this.options.flex) break; 506 | } 507 | } 508 | if (match) { 509 | lines = match[0].match(/(?:\r\n?|\n).*/g); 510 | if (lines) this.yylineno += lines.length; 511 | this.yylloc = {first_line: this.yylloc.last_line, 512 | last_line: this.yylineno+1, 513 | first_column: this.yylloc.last_column, 514 | last_column: lines ? lines[lines.length-1].length-lines[lines.length-1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length}; 515 | this.yytext += match[0]; 516 | this.match += match[0]; 517 | this.matches = match; 518 | this.yyleng = this.yytext.length; 519 | if (this.options.ranges) { 520 | this.yylloc.range = [this.offset, this.offset += this.yyleng]; 521 | } 522 | this._more = false; 523 | this._input = this._input.slice(match[0].length); 524 | this.matched += match[0]; 525 | token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]); 526 | if (this.done && this._input) this.done = false; 527 | if (token) return token; 528 | else return; 529 | } 530 | if (this._input === "") { 531 | return this.EOF; 532 | } else { 533 | return this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), 534 | {text: "", token: null, line: this.yylineno}); 535 | } 536 | }, 537 | lex:function lex() { 538 | var r = this.next(); 539 | if (typeof r !== 'undefined') { 540 | return r; 541 | } else { 542 | return this.lex(); 543 | } 544 | }, 545 | begin:function begin(condition) { 546 | this.conditionStack.push(condition); 547 | }, 548 | popState:function popState() { 549 | return this.conditionStack.pop(); 550 | }, 551 | _currentRules:function _currentRules() { 552 | return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules; 553 | }, 554 | topState:function () { 555 | return this.conditionStack[this.conditionStack.length-2]; 556 | }, 557 | pushState:function begin(condition) { 558 | this.begin(condition); 559 | }}); 560 | lexer.options = {}; 561 | lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { 562 | 563 | var YYSTATE=YY_START 564 | switch($avoiding_name_collisions) { 565 | case 0: 566 | if(yy_.yytext.slice(-1) !== "\\") this.begin("mu"); 567 | if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1), this.begin("emu"); 568 | if(yy_.yytext) return 14; 569 | 570 | break; 571 | case 1: return 14; 572 | break; 573 | case 2: 574 | if(yy_.yytext.slice(-1) !== "\\") this.popState(); 575 | if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1); 576 | return 14; 577 | 578 | break; 579 | case 3: yy_.yytext = yy_.yytext.substr(0, yy_.yyleng-4); this.popState(); return 15; 580 | break; 581 | case 4: this.begin("par"); return 24; 582 | break; 583 | case 5: return 16; 584 | break; 585 | case 6: return 20; 586 | break; 587 | case 7: return 19; 588 | break; 589 | case 8: return 19; 590 | break; 591 | case 9: return 23; 592 | break; 593 | case 10: return 23; 594 | break; 595 | case 11: this.popState(); this.begin('com'); 596 | break; 597 | case 12: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return 15; 598 | break; 599 | case 13: return 22; 600 | break; 601 | case 14: return 36; 602 | break; 603 | case 15: return 35; 604 | break; 605 | case 16: return 35; 606 | break; 607 | case 17: return 39; 608 | break; 609 | case 18: /*ignore whitespace*/ 610 | break; 611 | case 19: this.popState(); return 18; 612 | break; 613 | case 20: this.popState(); return 18; 614 | break; 615 | case 21: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 30; 616 | break; 617 | case 22: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\'/g,"'"); return 30; 618 | break; 619 | case 23: yy_.yytext = yy_.yytext.substr(1); return 28; 620 | break; 621 | case 24: return 32; 622 | break; 623 | case 25: return 32; 624 | break; 625 | case 26: return 31; 626 | break; 627 | case 27: return 35; 628 | break; 629 | case 28: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 35; 630 | break; 631 | case 29: return 'INVALID'; 632 | break; 633 | case 30: /*ignore whitespace*/ 634 | break; 635 | case 31: this.popState(); return 37; 636 | break; 637 | case 32: return 5; 638 | break; 639 | } 640 | }; 641 | lexer.rules = [/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\{\{>)/,/^(?:\{\{#)/,/^(?:\{\{\/)/,/^(?:\{\{\^)/,/^(?:\{\{\s*else\b)/,/^(?:\{\{\{)/,/^(?:\{\{&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{)/,/^(?:=)/,/^(?:\.(?=[} ]))/,/^(?:\.\.)/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}\}\})/,/^(?:\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@[a-zA-Z]+)/,/^(?:true(?=[}\s]))/,/^(?:false(?=[}\s]))/,/^(?:[0-9]+(?=[}\s]))/,/^(?:[a-zA-Z0-9_$-]+(?=[=}\s\/.]))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:\s+)/,/^(?:[a-zA-Z0-9_$-/]+)/,/^(?:$)/]; 642 | lexer.conditions = {"mu":{"rules":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,32],"inclusive":false},"emu":{"rules":[2],"inclusive":false},"com":{"rules":[3],"inclusive":false},"par":{"rules":[30,31],"inclusive":false},"INITIAL":{"rules":[0,1,32],"inclusive":true}}; 643 | return lexer;})() 644 | parser.lexer = lexer; 645 | function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser; 646 | return new Parser; 647 | })();; 648 | // lib/handlebars/compiler/base.js 649 | Handlebars.Parser = handlebars; 650 | 651 | Handlebars.parse = function(input) { 652 | 653 | // Just return if an already-compile AST was passed in. 654 | if(input.constructor === Handlebars.AST.ProgramNode) { return input; } 655 | 656 | Handlebars.Parser.yy = Handlebars.AST; 657 | return Handlebars.Parser.parse(input); 658 | }; 659 | 660 | Handlebars.print = function(ast) { 661 | return new Handlebars.PrintVisitor().accept(ast); 662 | };; 663 | // lib/handlebars/compiler/ast.js 664 | (function() { 665 | 666 | Handlebars.AST = {}; 667 | 668 | Handlebars.AST.ProgramNode = function(statements, inverse) { 669 | this.type = "program"; 670 | this.statements = statements; 671 | if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); } 672 | }; 673 | 674 | Handlebars.AST.MustacheNode = function(rawParams, hash, unescaped) { 675 | this.type = "mustache"; 676 | this.escaped = !unescaped; 677 | this.hash = hash; 678 | 679 | var id = this.id = rawParams[0]; 680 | var params = this.params = rawParams.slice(1); 681 | 682 | // a mustache is an eligible helper if: 683 | // * its id is simple (a single part, not `this` or `..`) 684 | var eligibleHelper = this.eligibleHelper = id.isSimple; 685 | 686 | // a mustache is definitely a helper if: 687 | // * it is an eligible helper, and 688 | // * it has at least one parameter or hash segment 689 | this.isHelper = eligibleHelper && (params.length || hash); 690 | 691 | // if a mustache is an eligible helper but not a definite 692 | // helper, it is ambiguous, and will be resolved in a later 693 | // pass or at runtime. 694 | }; 695 | 696 | Handlebars.AST.PartialNode = function(partialName, context) { 697 | this.type = "partial"; 698 | this.partialName = partialName; 699 | this.context = context; 700 | }; 701 | 702 | var verifyMatch = function(open, close) { 703 | if(open.original !== close.original) { 704 | throw new Handlebars.Exception(open.original + " doesn't match " + close.original); 705 | } 706 | }; 707 | 708 | Handlebars.AST.BlockNode = function(mustache, program, inverse, close) { 709 | verifyMatch(mustache.id, close); 710 | this.type = "block"; 711 | this.mustache = mustache; 712 | this.program = program; 713 | this.inverse = inverse; 714 | 715 | if (this.inverse && !this.program) { 716 | this.isInverse = true; 717 | } 718 | }; 719 | 720 | Handlebars.AST.ContentNode = function(string) { 721 | this.type = "content"; 722 | this.string = string; 723 | }; 724 | 725 | Handlebars.AST.HashNode = function(pairs) { 726 | this.type = "hash"; 727 | this.pairs = pairs; 728 | }; 729 | 730 | Handlebars.AST.IdNode = function(parts) { 731 | this.type = "ID"; 732 | this.original = parts.join("."); 733 | 734 | var dig = [], depth = 0; 735 | 736 | for(var i=0,l=parts.length; i 0) { throw new Handlebars.Exception("Invalid path: " + this.original); } 741 | else if (part === "..") { depth++; } 742 | else { this.isScoped = true; } 743 | } 744 | else { dig.push(part); } 745 | } 746 | 747 | this.parts = dig; 748 | this.string = dig.join('.'); 749 | this.depth = depth; 750 | 751 | // an ID is simple if it only has one part, and that part is not 752 | // `..` or `this`. 753 | this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; 754 | 755 | this.stringModeValue = this.string; 756 | }; 757 | 758 | Handlebars.AST.PartialNameNode = function(name) { 759 | this.type = "PARTIAL_NAME"; 760 | this.name = name; 761 | }; 762 | 763 | Handlebars.AST.DataNode = function(id) { 764 | this.type = "DATA"; 765 | this.id = id; 766 | }; 767 | 768 | Handlebars.AST.StringNode = function(string) { 769 | this.type = "STRING"; 770 | this.string = string; 771 | this.stringModeValue = string; 772 | }; 773 | 774 | Handlebars.AST.IntegerNode = function(integer) { 775 | this.type = "INTEGER"; 776 | this.integer = integer; 777 | this.stringModeValue = Number(integer); 778 | }; 779 | 780 | Handlebars.AST.BooleanNode = function(bool) { 781 | this.type = "BOOLEAN"; 782 | this.bool = bool; 783 | this.stringModeValue = bool === "true"; 784 | }; 785 | 786 | Handlebars.AST.CommentNode = function(comment) { 787 | this.type = "comment"; 788 | this.comment = comment; 789 | }; 790 | 791 | })();; 792 | // lib/handlebars/utils.js 793 | 794 | var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; 795 | 796 | Handlebars.Exception = function(message) { 797 | var tmp = Error.prototype.constructor.apply(this, arguments); 798 | 799 | // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. 800 | for (var idx = 0; idx < errorProps.length; idx++) { 801 | this[errorProps[idx]] = tmp[errorProps[idx]]; 802 | } 803 | }; 804 | Handlebars.Exception.prototype = new Error(); 805 | 806 | // Build out our basic SafeString type 807 | Handlebars.SafeString = function(string) { 808 | this.string = string; 809 | }; 810 | Handlebars.SafeString.prototype.toString = function() { 811 | return this.string.toString(); 812 | }; 813 | 814 | (function() { 815 | var escape = { 816 | "&": "&", 817 | "<": "<", 818 | ">": ">", 819 | '"': """, 820 | "'": "'", 821 | "`": "`" 822 | }; 823 | 824 | var badChars = /[&<>"'`]/g; 825 | var possible = /[&<>"'`]/; 826 | 827 | var escapeChar = function(chr) { 828 | return escape[chr] || "&"; 829 | }; 830 | 831 | Handlebars.Utils = { 832 | escapeExpression: function(string) { 833 | // don't escape SafeStrings, since they're already safe 834 | if (string instanceof Handlebars.SafeString) { 835 | return string.toString(); 836 | } else if (string == null || string === false) { 837 | return ""; 838 | } 839 | 840 | if(!possible.test(string)) { return string; } 841 | return string.replace(badChars, escapeChar); 842 | }, 843 | 844 | isEmpty: function(value) { 845 | if (!value && value !== 0) { 846 | return true; 847 | } else if(Object.prototype.toString.call(value) === "[object Array]" && value.length === 0) { 848 | return true; 849 | } else { 850 | return false; 851 | } 852 | } 853 | }; 854 | })();; 855 | // lib/handlebars/compiler/compiler.js 856 | 857 | /*jshint eqnull:true*/ 858 | Handlebars.Compiler = function() {}; 859 | Handlebars.JavaScriptCompiler = function() {}; 860 | 861 | (function(Compiler, JavaScriptCompiler) { 862 | // the foundHelper register will disambiguate helper lookup from finding a 863 | // function in a context. This is necessary for mustache compatibility, which 864 | // requires that context functions in blocks are evaluated by blockHelperMissing, 865 | // and then proceed as if the resulting value was provided to blockHelperMissing. 866 | 867 | Compiler.prototype = { 868 | compiler: Compiler, 869 | 870 | disassemble: function() { 871 | var opcodes = this.opcodes, opcode, out = [], params, param; 872 | 873 | for (var i=0, l=opcodes.length; i 0) { 1361 | this.source[1] = this.source[1] + ", " + locals.join(", "); 1362 | } 1363 | 1364 | // Generate minimizer alias mappings 1365 | if (!this.isChild) { 1366 | for (var alias in this.context.aliases) { 1367 | this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias]; 1368 | } 1369 | } 1370 | 1371 | if (this.source[1]) { 1372 | this.source[1] = "var " + this.source[1].substring(2) + ";"; 1373 | } 1374 | 1375 | // Merge children 1376 | if (!this.isChild) { 1377 | this.source[1] += '\n' + this.context.programs.join('\n') + '\n'; 1378 | } 1379 | 1380 | if (!this.environment.isSimple) { 1381 | this.source.push("return buffer;"); 1382 | } 1383 | 1384 | var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"]; 1385 | 1386 | for(var i=0, l=this.environment.depths.list.length; i this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } 1918 | return this.topStackName(); 1919 | }, 1920 | topStackName: function() { 1921 | return "stack" + this.stackSlot; 1922 | }, 1923 | flushInline: function() { 1924 | var inlineStack = this.inlineStack; 1925 | if (inlineStack.length) { 1926 | this.inlineStack = []; 1927 | for (var i = 0, len = inlineStack.length; i < len; i++) { 1928 | var entry = inlineStack[i]; 1929 | if (entry instanceof Literal) { 1930 | this.compileStack.push(entry); 1931 | } else { 1932 | this.pushStack(entry); 1933 | } 1934 | } 1935 | } 1936 | }, 1937 | isInline: function() { 1938 | return this.inlineStack.length; 1939 | }, 1940 | 1941 | popStack: function(wrapped) { 1942 | var inline = this.isInline(), 1943 | item = (inline ? this.inlineStack : this.compileStack).pop(); 1944 | 1945 | if (!wrapped && (item instanceof Literal)) { 1946 | return item.value; 1947 | } else { 1948 | if (!inline) { 1949 | this.stackSlot--; 1950 | } 1951 | return item; 1952 | } 1953 | }, 1954 | 1955 | topStack: function(wrapped) { 1956 | var stack = (this.isInline() ? this.inlineStack : this.compileStack), 1957 | item = stack[stack.length - 1]; 1958 | 1959 | if (!wrapped && (item instanceof Literal)) { 1960 | return item.value; 1961 | } else { 1962 | return item; 1963 | } 1964 | }, 1965 | 1966 | quotedString: function(str) { 1967 | return '"' + str 1968 | .replace(/\\/g, '\\\\') 1969 | .replace(/"/g, '\\"') 1970 | .replace(/\n/g, '\\n') 1971 | .replace(/\r/g, '\\r') + '"'; 1972 | }, 1973 | 1974 | setupHelper: function(paramSize, name, missingParams) { 1975 | var params = []; 1976 | this.setupParams(paramSize, params, missingParams); 1977 | var foundHelper = this.nameLookup('helpers', name, 'helper'); 1978 | 1979 | return { 1980 | params: params, 1981 | name: foundHelper, 1982 | callParams: ["depth0"].concat(params).join(", "), 1983 | helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ") 1984 | }; 1985 | }, 1986 | 1987 | // the params and contexts arguments are passed in arrays 1988 | // to fill in 1989 | setupParams: function(paramSize, params, useRegister) { 1990 | var options = [], contexts = [], types = [], param, inverse, program; 1991 | 1992 | options.push("hash:" + this.popStack()); 1993 | 1994 | inverse = this.popStack(); 1995 | program = this.popStack(); 1996 | 1997 | // Avoid setting fn and inverse if neither are set. This allows 1998 | // helpers to do a check for `if (options.fn)` 1999 | if (program || inverse) { 2000 | if (!program) { 2001 | this.context.aliases.self = "this"; 2002 | program = "self.noop"; 2003 | } 2004 | 2005 | if (!inverse) { 2006 | this.context.aliases.self = "this"; 2007 | inverse = "self.noop"; 2008 | } 2009 | 2010 | options.push("inverse:" + inverse); 2011 | options.push("fn:" + program); 2012 | } 2013 | 2014 | for(var i=0; i
' + escapeInnerText(source) + '
' + expected + '
' + actual + '
' + QUnit.diff(expected, actual) +'